From a7e56d334fd4c65a1c9d79644c8020bad5757c65 Mon Sep 17 00:00:00 2001 From: ADS Merger Date: Fri, 13 Mar 2020 05:35:18 +0000 Subject: [PATCH] Merge from vscode fb5dc0083bfa9a0e3da7ed1f86e1ecb9836fcc8b --- .github/commands.json | 179 ++++++++++ .github/commands.yml | 11 - .github/copycat.yml | 5 - .github/workflows/commands.yml | 21 ++ .github/workflows/copycat.yml | 26 ++ .github/workflows/needs-version-info.yml | 20 ++ extensions/image-preview/src/preview.ts | 4 +- .../client/src/jsonMain.ts | 3 + .../src/features/previewManager.ts | 4 +- .../base/browser/ui/actionbar/actionbar.css | 3 +- src/vs/base/browser/ui/actionbar/actionbar.ts | 10 +- .../ui/codiconLabel/codicon/codicon.css | 3 +- .../ui/codiconLabel/codicon/codicon.ttf | Bin 57012 -> 57108 bytes src/vs/base/browser/ui/inputbox/inputBox.ts | 24 +- .../quickinput/browser/media/quickInput.css | 31 +- .../quickinput/browser/quickInputList.ts | 9 + .../parts/quickinput/common/quickInput.ts | 1 + src/vs/editor/common/standaloneStrings.ts | 1 + .../contrib/codelens/codelensController.ts | 2 +- .../quickAccess/commandsQuickAccess.ts | 49 +++ .../{gotoLine.ts => gotoLineQuickAccess.ts} | 0 src/vs/editor/editor.main.ts | 1 + .../standaloneCommandsQuickAccess.ts | 44 +++ .../standaloneGotoLineQuickAccess.ts | 2 +- .../browser/menuEntryActionViewItem.ts | 23 +- src/vs/platform/actions/common/actions.ts | 20 +- src/vs/platform/actions/common/menuService.ts | 3 +- .../platform/contextkey/common/contextkey.ts | 2 +- .../environment/common/environment.ts | 1 - src/vs/platform/environment/node/argv.ts | 1 - .../quickinput/browser/commandsQuickAccess.ts | 305 ++++++++++++++++++ .../platform/quickinput/common/quickAccess.ts | 10 +- .../common/resourceIdentityService.ts | 22 ++ .../node/resourceIdentityServiceImpl.ts | 54 ++++ src/vs/vscode.proposed.d.ts | 87 +++-- .../api/browser/mainThreadWebview.ts | 136 ++++++-- .../workbench/api/common/extHost.protocol.ts | 10 +- src/vs/workbench/api/common/extHostWebview.ts | 181 ++++------- .../browser/actions/layoutActions.ts | 4 +- .../parts/sidebar/media/sidebarpart.css | 5 +- .../browser/parts/views/media/paneviewlet.css | 8 +- .../browser/parts/views/viewPaneContainer.ts | 22 +- src/vs/workbench/browser/web.main.ts | 13 +- src/vs/workbench/browser/workbench.ts | 1 + .../backup/electron-browser/backupTracker.ts | 60 ++-- .../electron-browser/backupTracker.test.ts | 13 +- .../browser/accessibility/accessibility.ts | 6 +- .../quickaccess/gotoLineQuickAccess.ts | 2 +- .../contrib/customEditor/browser/commands.ts | 2 +- .../customEditor/browser/customEditorInput.ts | 8 +- .../customEditor/common/customEditor.ts | 2 - .../common/customTextEditorModel.ts | 8 - .../debug/browser/debug.contribution.ts | 2 +- .../browser/media/debug.contribution.css | 9 +- .../debug/browser/media/debugViewlet.css | 8 + .../contrib/debug/browser/variablesView.ts | 37 ++- .../debug/browser/watchExpressionsView.ts | 8 +- .../contrib/debug/browser/welcomeView.ts | 12 +- .../workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/common/debugModel.ts | 17 +- .../contrib/debug/common/debugProtocol.d.ts | 71 +++- .../extensions/browser/extensionsActions.ts | 14 +- .../browser/extensionsQuickAccess.ts | 3 +- .../contrib/files/browser/explorerViewlet.ts | 38 +-- .../files/test/browser/editorAutoSave.test.ts | 5 +- .../files/test/browser/textFileEditor.test.ts | 5 +- .../output/browser/output.contribution.ts | 35 +- .../contrib/output/browser/outputView.ts | 16 - .../browser/commandsQuickAccess.ts | 109 +++++++ .../browser/quickAccess.contribution.ts | 18 +- .../browser/quickAccessCommands.ts | 6 +- .../quickopen/browser/commandsHandler.ts | 2 +- .../contrib/scm/browser/media/scmViewlet.css | 4 + .../browser/searchEditor.contribution.ts | 2 +- .../contrib/tasks/browser/tasksQuickAccess.ts | 3 +- .../userDataSync/browser/userDataSyncView.ts | 2 +- .../electron-browser/desktop.main.ts | 56 +--- .../configuration/browser/configuration.ts | 135 ++++---- .../browser/abstractFileDialogService.ts | 4 +- .../electron-browser/fileDialogService.ts | 12 +- .../common/extensionEnablementService.ts | 4 + .../extensionEnablementService.test.ts | 24 ++ .../extensions/common/extensionsRegistry.ts | 5 + .../electron-browser/extensionService.ts | 4 +- .../common/filesConfigurationService.ts | 6 +- .../services/themes/common/colorThemeData.ts | 4 +- .../tokenStyleResolving.test.ts | 35 ++ .../test/browser/workbenchTestServices.ts | 2 +- 88 files changed, 1627 insertions(+), 553 deletions(-) create mode 100644 .github/commands.json delete mode 100644 .github/commands.yml delete mode 100644 .github/copycat.yml create mode 100644 .github/workflows/commands.yml create mode 100644 .github/workflows/copycat.yml create mode 100644 .github/workflows/needs-version-info.yml create mode 100644 src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts rename src/vs/editor/contrib/quickAccess/{gotoLine.ts => gotoLineQuickAccess.ts} (100%) create mode 100644 src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts create mode 100644 src/vs/platform/quickinput/browser/commandsQuickAccess.ts create mode 100644 src/vs/platform/resource/common/resourceIdentityService.ts create mode 100644 src/vs/platform/resource/node/resourceIdentityServiceImpl.ts create mode 100644 src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 0000000000..43c602207c --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,179 @@ +[ + { + "type": "comment", + "name": "question", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "*question" + }, + { + "type": "label", + "name": "*question", + "action": "close", + "comment": "Please ask your question on [StackOverflow](https://aka.ms/vscodestackoverflow). We have a great community over [there](https://aka.ms/vscodestackoverflow). They have already answered thousands of questions and are happy to answer yours as well. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*dev-question", + "action": "close", + "comment": "We have a great developer community [over on slack](https://aka.ms/vscode-dev-community) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*extension-candidate", + "action": "close", + "comment": "We try to keep VS Code lean and we think the functionality you're asking for is great for a VS Code extension. Maybe you can already find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace). Just in case, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*not-reproducible", + "action": "close", + "comment": "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of VS Code. If not, please ask us to reopen the issue and provide us with more detail. Our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines might help you with that.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*out-of-scope", + "action": "close", + "comment": "We closed this issue because we don't plan to address it in the foreseeable future. You can find more detailed information about our decision-making process [here](https://aka.ms/vscode-out-of-scope). 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": "comment", + "name": "causedByExtension", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "*caused-by-extension" + }, + { + "type": "label", + "name": "*caused-by-extension", + "action": "close", + "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*as-designed", + "action": "close", + "comment": "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*english-please", + "action": "close", + "comment": "This issue is being closed because its description is not in English, that makes it hard for us to work on it. Please open a new issue with an English description. You might find [Bing Translator](https://www.bing.com/translator) useful." + }, + { + "type": "comment", + "name": "duplicate", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "*duplicate" + }, + { + "type": "label", + "name": "*duplicate", + "action": "close", + "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for existing issues [here](https://aka.ms/vscodeissuesearch). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "confirm", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "confirmed", + "removeLabel": "confirmation-pending" + }, + { + "type": "comment", + "name": "confirmationPending", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "confirmation-pending", + "removeLabel": "confirmed" + }, + { + "type": "comment", + "name": "findDuplicates", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "comment": "Potential duplicates:\n${potentialDuplicates}" + }, + { + "type": "comment", + "name": "needsMoreInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "~needs more info" + }, + { + "type": "label", + "name": "~needs version info", + "action": "updateLabels", + "addLabel": "needs more info", + "removeLabel": "~needs version info", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, 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!" + }, + { + "type": "label", + "name": "~needs more info", + "action": "updateLabels", + "addLabel": "needs more info", + "removeLabel": "~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!" + }, + { + "type": "comment", + "name": "a11ymas", + "allowUsers": [ + "AccessibilityTestingTeam-TCS", + "dixitsonali95", + "Mohini78", + "ChitrarupaSharma", + "mspatil110", + "umasarath52", + "v-umnaik" + ], + "action": "updateLabels", + "addLabel": "a11ymas" + }, + { + "type": "label", + "name": "*off-topic", + "action": "close", + "comment": "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + } +] diff --git a/.github/commands.yml b/.github/commands.yml deleted file mode 100644 index 967ef134c0..0000000000 --- a/.github/commands.yml +++ /dev/null @@ -1,11 +0,0 @@ -{ - perform: true, - commands: [ - { - type: 'label', - name: 'Needs Logs', - action: 'comment', - comment: "We need more info to debug your particular issue. If you could attach your logs to the issue (ensure no private data is in them), it would help us fix the issue much faster.\n\nTo find your logs:\n\n- Open command palette (Click **View** -> **Command Palette**)\n- Run the command: **`Developer: Open Logs Folder`**\n\nThis will open the log file locally. Please include renderer.log" - } - ] -} diff --git a/.github/copycat.yml b/.github/copycat.yml deleted file mode 100644 index ef93806255..0000000000 --- a/.github/copycat.yml +++ /dev/null @@ -1,5 +0,0 @@ -{ - perform: true, - target_owner: 'anthonydresser', - target_repo: 'testissues' -} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 0000000000..9a31b98bfe --- /dev/null +++ b/.github/workflows/commands.yml @@ -0,0 +1,21 @@ +name: Commands +on: + issue_comment: + types: [created] + issues: + types: [labeled] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'JacksonKearl/vscode-triage-github-actions' + ref: v2 + - name: Run Commands + uses: ./commands + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + config-path: commands diff --git a/.github/workflows/copycat.yml b/.github/workflows/copycat.yml new file mode 100644 index 0000000000..c4b706759f --- /dev/null +++ b/.github/workflows/copycat.yml @@ -0,0 +1,26 @@ +name: CopyCat +on: + issues: + types: [opened] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'JacksonKearl/vscode-triage-github-actions' + ref: v2 + - name: Run CopyCat (JacksonKearl/testissues) + uses: ./copycat + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + owner: JacksonKearl + repo: testissues + - name: Run CopyCat (chrmarti/testissues) + uses: ./copycat + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + owner: chrmarti + repo: testissues diff --git a/.github/workflows/needs-version-info.yml b/.github/workflows/needs-version-info.yml new file mode 100644 index 0000000000..b3cb0fc0a3 --- /dev/null +++ b/.github/workflows/needs-version-info.yml @@ -0,0 +1,20 @@ +name: Needs Version Info +on: + issues: + types: [opened] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'JacksonKearl/vscode-triage-github-actions' + ref: v2 + - name: Run Needs Version Info + uses: ./needs-more-info + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + matcher: '\b(\d\.\d{2,3}\.\d|insiders?|1\.\d\d\d?)\b' + label: ~needs version info diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index e87b7bc9cd..f9971209a9 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -27,8 +27,8 @@ export class PreviewManager implements vscode.CustomEditorProvider { private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { } - public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { - return {}; + public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { + // noop } public async resolveCustomEditor( diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index e9033d16cf..38a2bbcc93 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -345,6 +345,9 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] fileMatch = [fileMatch]; } if (Array.isArray(fileMatch) && url) { + if (url[0] === '.' && url[1] === '/') { + url = Uri.file(path.join(extension.extensionPath, url)).toString(); + } fileMatch = fileMatch.map(fm => { if (fm[0] === '%') { fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User'); diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 880a679c1e..d640b21131 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -148,8 +148,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this.registerDynamicPreview(preview); } - public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { - return {}; + public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { + // noop } public async resolveCustomTextEditor( diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 358fdca90e..da795d4656 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -46,7 +46,8 @@ } .monaco-action-bar .action-item .codicon { - vertical-align: middle; + display: flex; + align-items: center; } .monaco-action-bar .action-label { diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 0ad28cccc5..115b053e77 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -510,7 +510,7 @@ export class ActionBar extends Disposable implements IActionRunner { } else if (event.equals(nextKey)) { this.focusNext(); } else if (event.equals(KeyCode.Escape)) { - this.cancel(); + this._onDidCancel.fire(); } else if (this.isTriggerKeyEvent(event)) { // Staying out of the else branch even if not triggered if (this.options.triggerKeys && this.options.triggerKeys.keyDown) { @@ -813,14 +813,6 @@ export class ActionBar extends Disposable implements IActionRunner { } } - private cancel(): void { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); // remove focus from focused action - } - - this._onDidCancel.fire(); - } - run(action: IAction, context?: unknown): Promise { return this._actionRunner.run(action, context); } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 13cfd2856c..45f6d7b424 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?df9e07bbeddc0cf98f4d7a7c92bef3d8") format("truetype"); + src: url("./codicon.ttf?5490083fcec741c6a0a08a366d2f9c98") format("truetype"); } .codicon[class*='codicon-'] { @@ -419,3 +419,4 @@ .codicon-bell-dot:before { content: "\f101" } .codicon-debug-alt-2:before { content: "\f102" } .codicon-debug-alt:before { content: "\f103" } +.codicon-run-all:before { content: "\f104" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 94b2533d5b943c8d320f9c0a0b96a906576f1788..5eac56a666cf56b9be1a6b784ba948e67711f83b 100644 GIT binary patch delta 1878 zcmYM#X>1f_90u^`+1=hJEytEZS=v&rQlO82V%bfP9!$2aMwGNdp?(IZ;08A zaw6adzZls+BN7{1I@kMLvIpJ-99Ns$mb+6smK*?LG5}k1i+g>$tqkAD`%byY)#`3p zG5+Z2%q_sh%W^?*ds|26u5)W#z@_Uzd%e?8>V{>><`A4VUd;V##^~3&rFIh4eu-}8j zX5B}m{MZ3whtXqvYnGV3X5W2>_o;v8qTx7?2RMnxNaHk^xQ_v>!UVaaW$@w{k{OL^ zgfST%sKYg!#aB4ZG1!TX=teqisAm-Z#Xbh&DT6tTq4dWNlHtC-05?6ia2vm&A9rBj zcihE2{DD957armf{>DFefn7{V|{a0Ewk6rGIWXvT6Z<2a5kj%Pd* zIe|%>$Vr^cDV)j_PUj5HWGd4*3kPushw&LQkcq-cC_*u^8HoavA`ec);5Ztv8a+%v zIObv=WM=9A>A@hm5g^nW=kPvjE2n3Bj|W$_440u>sbsidG%6W57^{>FA&k{Z1`|e; zLYic=k^zR%qGYIHv?>{N7;Opz-u95CjP_m*Nno<~K}aRXc=JN%d87iyy9)J^JCyaE zjrWvH1B~|-_DRYkV3DbTu}k5pWUr!B#MrGkOmdH+jDg`*GMO;;0&&&`)*t;qQMzb+ zsAR5PVTFM**Dwo|%sR|MCG!unNJ$f57Axrl%n~K-fLW%bCos#EGzMmclJ3B)RMH}t zRZ98tq|8YLxzxll>nVAd)r9?VyjR1oHCO3Daxk&;@%T&$#^Fke@ACb>je zkFdE^(GCyHWpW_skaR2hN!BR_NG?|ll9ZXWFhp{NVwhxuVuYkL&%zOsjfx{BrFj;P zl3Z=^$bqavW|N|*qZ zva?w+RdS1Bn&ei+S>90RHb#1To$Hw34U9<&y)Uz6u@(Yfa?E%1)MoT$+|G>5%**V~ z99S@G!QrgtDP_H71La4` z`^%qHgjU2=%&6F3@k{00_{!GG3spr`A6H$ic2?I{e_4IK1~nNqoi&dZIu{PqX4mfV z^~JVq&XP47zXg?ZO85#bKE)xW9$ZuaodJbA#qoC>Dt}L6p+_wqrzd^Cr`^6Y z?&GK31DTsAJy4aM`^lsv%I(G}?Jan)jw&pYR&cL%yw zP5)wY@lN3TUcMl?i)OghRb=BH=ZMfR+@Yx?(jvq8eyG?9Jg@f$=kto4elqaJZ3E59zlfb zBu`4>pPLu!v-|A}#%~79fjjQKQN>Hg;Vk~ZG5m!B$%BpC_#JDJ%}>(?3rCPA$!JD` z%*F<+z%WkZJDiYIypL^o2Ma~eE>B?;A4wD*NQ@*&oQy%AKoY_SqXH7TieGUJzu`J= zz+eP7aSL~F7k}a&?&EL#gNOJRkMLMr;+6=BlxT^ScuAD;GC?NFB=Ja!OqMA!Rnlad zq|0>4lq|`X9GM|AWtQa19GNTgWWFqrd?~;uIE+v68H!L0Zw6}MLz#?4HEK}_4^nUx ztI>ge$v`5W!=mt^m;{M&s=}Krw7!jP42|Qg)+i1Lc2IFtu-7UM4R(j(_+Y=PP{8a| z94YKB#o@y4Rva_zb&3Or-J>{q*y{nSFU}{))^J>=%(h138YR{8$1lou>hi8aJF`!5 z17N?WFlv1jKPeE!qYo4wFb9;xF!v})V(wKE$Fvl;5B5F<=L#Pxr?2*Y#m&u0@J4fU zVX76k8OE!);V?CdTMy$?ECGyPu?{eGip7AbSF8w3gJM}=8Wn2;)1+7+m}Vg4+_6hA zEsCvzS)$l4m{!H6!Mv#0IhdCe+Xu5$v4=1(D>f2lnPN9#UQu|=T&~205KJ2nNciP4 z0VNU46-pwRE0shuxor-KWv)^Z&s?n}k;&pYWIQvdWCD}raL7bvha<#;NnEgb4(I2` zbSg<?%cRP3fhwiDj*2=gX7I z2g;9^55I7&BCDdPqO)S4Vz4r)a4;oRgbC{dtKfl?-{tSPc|A_x?ZCP!1?U~x!b@6rGbwl;L>j&$9thehQHY7ImHk@trG}dG^9&XBQ z+SYWe>3(xb^S .quick-input-list-row .monaco-icon-label, +.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container { + flex: 1; /* make sure the icon label grows within the row */ +} + .quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon { vertical-align: sub; } @@ -194,6 +199,10 @@ opacity: 1; } +.quick-input-list .quick-input-list-entry .quick-input-list-entry-keybinding { + margin-right: 8px; /* separate from the separator label or scrollbar if any */ +} + .quick-input-list .quick-input-list-label-meta { opacity: 0.7; line-height: normal; @@ -205,17 +214,13 @@ font-weight: bold; } -.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 .quick-input-list-separator { + margin-right: 8px; /* separate from keybindings or actions */ } .quick-input-list .quick-input-list-entry-action-bar { - display: none; + display: flex; + visibility: hidden; /* not using display: none here to not flicker too much */ flex: 0; overflow: visible; } @@ -231,16 +236,16 @@ margin-top: 1px; } -.quick-input-list .quick-input-list-entry-action-bar ul:first-child .action-label.codicon { - margin-left: 2px; +.quick-input-list .quick-input-list-entry-action-bar { + margin-right: 4px; /* separate from scrollbar */ } -.quick-input-list .quick-input-list-entry-action-bar ul:last-child .action-label.codicon { - margin-right: 8px; +.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon { + margin-right: 4px; /* separate actions */ } .quick-input-list .quick-input-list-entry.always-visible-actions .quick-input-list-entry-action-bar, .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; + visibility: visible; } diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 69f3f9297d..e69f7e5171 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -26,6 +26,7 @@ import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput'; import { IListOptions, List, IListStyles, IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; const $ = dom.$; @@ -79,6 +80,7 @@ interface IListElementTemplateData { entry: HTMLDivElement; checkbox: HTMLInputElement; label: IconLabel; + keybinding: KeybindingLabel; detail: HighlightedLabel; separator: HTMLDivElement; actionBar: ActionBar; @@ -118,6 +120,10 @@ class ListElementRenderer implements IListRenderer> { + return this.getCodeEditorCommandPicks(); + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: StandaloneCommandsQuickAccessProvider, + prefix: StandaloneCommandsQuickAccessProvider.PREFIX, + helpEntries: [{ description: QuickCommandNLS.quickCommandHelp, needsEditor: true }] +}); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts index ce50aed365..d9857fb156 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoLine'; +import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoLineQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 3f9705f493..158a797f75 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -12,7 +12,7 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore, dispose } 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 { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } 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'; @@ -217,9 +217,10 @@ export class MenuEntryActionViewItem extends ActionViewItem { const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id); const keybindingLabel = keybinding && keybinding.getLabel(); + const tooltip = this._commandAction.tooltip || this._commandAction.label; this.label.title = keybindingLabel - ? localize('titleAndKb', "{0} ({1})", this._commandAction.label, keybindingLabel) - : this._commandAction.label; + ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) + : tooltip; } } @@ -238,9 +239,11 @@ export class MenuEntryActionViewItem extends ActionViewItem { _updateItemClass(item: ICommandAction): void { this._itemClassDispose.value = undefined; - if (ThemeIcon.isThemeIcon(item.icon)) { + const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon; + + if (ThemeIcon.isThemeIcon(icon)) { // theme icons - const iconClass = ThemeIcon.asClassName(item.icon); + const iconClass = ThemeIcon.asClassName(icon); if (this.label && iconClass) { addClasses(this.label, iconClass); this._itemClassDispose.value = toDisposable(() => { @@ -250,20 +253,20 @@ export class MenuEntryActionViewItem extends ActionViewItem { }); } - } else if (item.icon) { + } else if (icon) { // icon path let iconClass: string; - if (item.icon?.dark?.scheme) { + if (icon?.dark?.scheme) { - const iconPathMapKey = item.icon.dark.toString(); + const iconPathMapKey = icon.dark.toString(); if (MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { iconClass = MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; } else { iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(item.icon.light || item.icon.dark)}`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(item.icon.dark)}`); + createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`); + createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`); MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 6dcefc8ff7..838dc39632 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -20,13 +20,16 @@ export interface ILocalizedString { original: string; } +export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; + export interface ICommandAction { id: string; title: string | ILocalizedString; category?: string | ILocalizedString; - icon?: { dark?: URI; light?: URI; } | ThemeIcon; + tooltip?: string | ILocalizedString; + icon?: Icon; precondition?: ContextKeyExpression; - toggled?: ContextKeyExpression; + toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; } export type ISerializableCommandAction = UriDto; @@ -280,9 +283,20 @@ export class MenuItemAction extends ExecuteCommandAction { @ICommandService commandService: ICommandService ) { typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); + this._cssClass = undefined; this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); - this._checked = Boolean(item.toggled && contextKeyService.contextMatchesRules(item.toggled)); + this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined; + + if (item.toggled) { + const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as { + condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString + }; + this._checked = contextKeyService.contextMatchesRules(toggled.condition); + if (this._checked && toggled.tooltip) { + this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; + } + } this._options = options || {}; diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 4a0f15c72c..d05d7f5486 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -94,7 +94,8 @@ class Menu implements IMenu { // keep toggled keys for event if applicable if (isIMenuItem(item) && item.command.toggled) { - Menu._fillInKbExprKeys(item.command.toggled, this._contextKeys); + const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled as ContextKeyExpression; // {{SQL CARBON EDIT}} strict-null-checks + Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); } } this._onDidChange.fire(this); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 4a6f879fbd..e032ddb0a8 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -1051,7 +1051,7 @@ export class ContextKeyLessThanEqualsExpr implements IContextKeyExpression { } public equals(other: ContextKeyExpression): boolean { - if (other instanceof ContextKeyLessThanEqualsExpr) { + if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 4f38258ae8..31a0156dce 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -39,7 +39,6 @@ export interface ParsedArgs { 'builtin-extensions-dir'?: string; extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs extensionTestsPath?: string; // either a local path or a URI - 'extension-development-confirm-save'?: boolean; 'inspect-extensions'?: string; 'inspect-brk-extensions'?: string; debugId?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 9e11baa1b4..968460b20d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -80,7 +80,6 @@ export const OPTIONS: OptionDescriptions> = { 'locate-extension': { type: 'string[]' }, 'extensionDevelopmentPath': { type: 'string[]' }, 'extensionTestsPath': { type: 'string' }, - 'extension-development-confirm-save': { type: 'boolean' }, 'debugId': { type: 'string' }, 'inspect-search': { type: 'string', deprecates: 'debugSearch' }, 'inspect-brk-search': { type: 'string', deprecates: 'debugBrkSearch' }, diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts new file mode 100644 index 0000000000..ca42846ac7 --- /dev/null +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -0,0 +1,305 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { PickerQuickAccessProvider, IPickerQuickAccessItem } from 'vs/platform/quickinput/common/quickAccess'; +import { distinct } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { or, matchesPrefix, matchesWords, matchesContiguousSubString } from 'vs/base/common/filters'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { LRUCache } from 'vs/base/common/map'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { isFirefox } from 'vs/base/browser/browser'; +import { timeout } from 'vs/base/common/async'; + +export interface ICommandQuickPick extends IPickerQuickAccessItem { + commandId: string; + commandAlias: string | undefined; +} + +export interface ICommandsQuickAccessOptions { + showAlias: boolean; +} + +export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAccessProvider implements IDisposable { + + static PREFIX = '>'; + + private static WORD_FILTER = or(matchesPrefix, matchesWords, matchesContiguousSubString); + + private readonly disposables = new DisposableStore(); + + private readonly commandsHistory = this.disposables.add(this.instantiationService.createInstance(CommandsHistory)); + + constructor( + private options: ICommandsQuickAccessOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @ICommandService private readonly commandService: ICommandService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @INotificationService private readonly notificationService: INotificationService + ) { + super(AbstractCommandsQuickAccessProvider.PREFIX); + } + + protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + + // Ask subclass for all command picks + const allCommandPicks = await this.getCommandPicks(disposables, token); + + // Filter + const filteredCommandPicks: ICommandQuickPick[] = []; + for (const commandPick of allCommandPicks) { + const labelHighlights = withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.label)); + const aliasHighlights = commandPick.commandAlias ? withNullAsUndefined(AbstractCommandsQuickAccessProvider.WORD_FILTER(filter, commandPick.commandAlias)) : undefined; + + if (labelHighlights || aliasHighlights) { + commandPick.highlights = { + label: labelHighlights, + detail: this.options.showAlias ? aliasHighlights : undefined + }; + + filteredCommandPicks.push(commandPick); + } + } + + // Remove duplicates + const distinctCommandPicks = distinct(filteredCommandPicks, pick => `${pick.label}${pick.commandId}`); + + // Add description to commands that have duplicate labels + const mapLabelToCommand = new Map(); + for (const commandPick of distinctCommandPicks) { + const existingCommandForLabel = mapLabelToCommand.get(commandPick.label); + if (existingCommandForLabel) { + commandPick.description = commandPick.commandId; + existingCommandForLabel.description = existingCommandForLabel.commandId; + } else { + mapLabelToCommand.set(commandPick.label, commandPick); + } + } + + // Sort by MRU order and fallback to name otherwise + distinctCommandPicks.sort((commandPickA, commandPickB) => { + const commandACounter = this.commandsHistory.peek(commandPickA.commandId); + const commandBCounter = this.commandsHistory.peek(commandPickB.commandId); + + if (commandACounter && commandBCounter) { + return commandACounter > commandBCounter ? -1 : 1; // use more recently used command before older + } + + if (commandACounter) { + return -1; // first command was used, so it wins over the non used one + } + + if (commandBCounter) { + return 1; // other command was used so it wins over the command + } + + // both commands were never used, so we sort by name + return commandPickA.label.localeCompare(commandPickB.label); + }); + + const commandPicks: Array = []; + + let addSeparator = false; + for (let i = 0; i < distinctCommandPicks.length; i++) { + const commandPick = distinctCommandPicks[i]; + const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId); + const ariaLabel = keybinding ? + localize('commandPickAriaLabelWithKeybinding', "{0}, {1}, commands picker", commandPick.label, keybinding.getAriaLabel()) : + localize('commandPickAriaLabel', "{0}, commands picker", commandPick.label); + + // Separator: recently used + if (i === 0 && this.commandsHistory.peek(commandPick.commandId)) { + commandPicks.push({ type: 'separator', label: localize('recentlyUsed', "recently used") }); + addSeparator = true; + } + + // Separator: other commands + if (i !== 0 && addSeparator && !this.commandsHistory.peek(commandPick.commandId)) { + commandPicks.push({ type: 'separator', label: localize('morecCommands', "other commands") }); + addSeparator = false; // only once + } + + // Command + commandPicks.push({ + ...commandPick, + ariaLabel, + detail: this.options.showAlias ? commandPick.commandAlias : undefined, + keybinding, + accept: async () => { + + // Add to history + this.commandsHistory.push(commandPick.commandId); + + if (!isFirefox) { + // Use a timeout to give the quick open widget a chance to close itself first + // Firefox: since the browser is quite picky for certain commands, we do not + // use a timeout (https://github.com/microsoft/vscode/issues/83288) + await timeout(50); + } + + // Telementry + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: commandPick.commandId, + from: 'quick open' + }); + + // Run + try { + await this.commandService.executeCommand(commandPick.commandId); + } catch (error) { + if (!isPromiseCanceledError(error)) { + this.notificationService.error(localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error))); + } + } + } + }); + } + + return commandPicks; + } + + protected abstract getCommandPicks(disposables: DisposableStore, token: CancellationToken): Promise>; + + dispose(): void { + this.disposables.dispose(); + } +} + +interface ISerializedCommandHistory { + usesLRU?: boolean; + entries: { key: string; value: number }[]; +} + +interface ICommandsQuickAccessConfiguration { + workbench: { + commandPalette: { + history: number; + preserveInput: boolean; + } + }; +} + +class CommandsHistory extends Disposable { + + 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'; + + private static cache: LRUCache | undefined; + private static counter = 1; + + private configuredCommandsHistoryLength = 0; + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.updateConfiguration(); + this.load(); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.configurationService.onDidChangeConfiguration(() => this.updateConfiguration())); + } + + private updateConfiguration(): void { + this.configuredCommandsHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(this.configurationService); + + if (CommandsHistory.cache && CommandsHistory.cache.limit !== this.configuredCommandsHistoryLength) { + CommandsHistory.cache.limit = this.configuredCommandsHistoryLength; + + CommandsHistory.saveState(this.storageService); + } + } + + private load(): void { + const raw = this.storageService.get(CommandsHistory.PREF_KEY_CACHE, StorageScope.GLOBAL); + let serializedCache: ISerializedCommandHistory | undefined; + if (raw) { + try { + serializedCache = JSON.parse(raw); + } catch (error) { + // invalid data + } + } + + const cache = CommandsHistory.cache = new LRUCache(this.configuredCommandsHistoryLength, 1); + if (serializedCache) { + let entries: { key: string; value: number }[]; + if (serializedCache.usesLRU) { + entries = serializedCache.entries; + } else { + entries = serializedCache.entries.sort((a, b) => a.value - b.value); + } + entries.forEach(entry => cache.set(entry.key, entry.value)); + } + + CommandsHistory.counter = this.storageService.getNumber(CommandsHistory.PREF_KEY_COUNTER, StorageScope.GLOBAL, CommandsHistory.counter); + } + + push(commandId: string): void { + if (!CommandsHistory.cache) { + return; + } + + CommandsHistory.cache.set(commandId, CommandsHistory.counter++); // set counter to command + + CommandsHistory.saveState(this.storageService); + } + + peek(commandId: string): number | undefined { + return CommandsHistory.cache?.peek(commandId); + } + + static saveState(storageService: IStorageService): void { + if (!CommandsHistory.cache) { + return; + } + + const serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] }; + CommandsHistory.cache.forEach((value, key) => serializedCache.entries.push({ key, value })); + + storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL); + storageService.store(CommandsHistory.PREF_KEY_COUNTER, CommandsHistory.counter, StorageScope.GLOBAL); + } + + static getConfiguredCommandHistoryLength(configurationService: IConfigurationService): number { + const config = configurationService.getValue(); + + const configuredCommandHistoryLength = config.workbench?.commandPalette?.history; + if (typeof configuredCommandHistoryLength === 'number') { + return configuredCommandHistoryLength; + } + + return CommandsHistory.DEFAULT_COMMANDS_HISTORY_LENGTH; + } + + static clearHistory(configurationService: IConfigurationService, storageService: IStorageService): void { + const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService); + CommandsHistory.cache = new LRUCache(commandHistoryLength); + CommandsHistory.counter = 1; + + CommandsHistory.saveState(storageService); + } +} + diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index abf578dcb1..4bc2508ef8 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -205,7 +205,7 @@ export abstract class PickerQuickAccessProvider | Promise>; + protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise>; } //#endregion diff --git a/src/vs/platform/resource/common/resourceIdentityService.ts b/src/vs/platform/resource/common/resourceIdentityService.ts new file mode 100644 index 0000000000..abab283a8f --- /dev/null +++ b/src/vs/platform/resource/common/resourceIdentityService.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { hash } from 'vs/base/common/hash'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export const IResourceIdentityService = createDecorator('IResourceIdentityService'); +export interface IResourceIdentityService { + _serviceBrand: undefined; + resolveResourceIdentity(resource: URI): Promise; +} + +export class WebResourceIdentityService extends Disposable implements IResourceIdentityService { + _serviceBrand: undefined; + async resolveResourceIdentity(resource: URI): Promise { + return hash(resource.toString()).toString(16); + } +} diff --git a/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts b/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts new file mode 100644 index 0000000000..1bdc68f96b --- /dev/null +++ b/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createHash } from 'crypto'; +import { stat } from 'vs/base/node/pfs'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; + +export class NativeResourceIdentityService extends Disposable implements IResourceIdentityService { + + _serviceBrand: undefined; + + private readonly cache: ResourceMap> = new ResourceMap>(); + + resolveResourceIdentity(resource: URI): Promise { + let promise = this.cache.get(resource); + if (!promise) { + promise = this.createIdentity(resource); + this.cache.set(resource, promise); + } + return promise; + } + + private async createIdentity(resource: URI): Promise { + // Return early the folder is not local + if (resource.scheme !== Schemas.file) { + return createHash('md5').update(resource.toString()).digest('hex'); + } + + const fileStat = await stat(resource.fsPath); + let ctime: number | undefined; + if (isLinux) { + ctime = fileStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! + } else if (isMacintosh) { + ctime = fileStat.birthtime.getTime(); // macOS: birthtime is fine to use as is + } else if (isWindows) { + if (typeof fileStat.birthtimeMs === 'number') { + ctime = Math.floor(fileStat.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 = fileStat.birthtime.getTime(); + } + } + + // we use the ctime as extra salt to the ID so that we catch the case of a folder getting + // deleted and recreated. in that case we do not want to carry over previous state + return createHash('md5').update(resource.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + } +} diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index e502e50a25..d58665d6b0 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1222,81 +1222,71 @@ declare module 'vscode' { // - Should we expose edits? // - More properties from `TextDocument`? - /** - * Defines the capabilities of a custom webview editor. - */ - interface CustomEditorCapabilities { - /** - * Defines the editing capability of a custom webview document. - * - * When not provided, the document is considered readonly. - */ - readonly editing?: CustomEditorEditingCapability; - } - /** * Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard * editor events such as `undo` or `save`. * * @param EditType Type of edits. */ - interface CustomEditorEditingCapability { + interface CustomEditorEditingDelegate { /** * Save the resource. * + * @param document Document to save. * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). * * @return Thenable signaling that the save has completed. */ - save(cancellation: CancellationToken): Thenable; + save(document: CustomDocument, cancellation: CancellationToken): Thenable; /** * Save the existing resource at a new path. * + * @param document Document to save. * @param targetResource Location to save to. * * @return Thenable signaling that the save has completed. */ - saveAs(targetResource: Uri): Thenable; + saveAs(document: CustomDocument, targetResource: Uri): Thenable; /** * Event triggered by extensions to signal to VS Code that an edit has occurred. */ - readonly onDidEdit: Event; + readonly onDidEdit: Event>; /** * Apply a set of edits. * * Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit. * + * @param document Document to apply edits to. * @param edit Array of edits. Sorted from oldest to most recent. * * @return Thenable signaling that the change has completed. */ - applyEdits(edits: readonly EditType[]): Thenable; + applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; /** * Undo a set of edits. * * This is triggered when a user undoes an edit. * + * @param document Document to undo edits from. * @param edit Array of edits. Sorted from most recent to oldest. * * @return Thenable signaling that the change has completed. */ - undoEdits(edits: readonly EditType[]): Thenable; + undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; /** * Revert the file to its last saved state. * - * @param change Added or applied edits. + * @param document Document to revert. + * @param edits Added or applied edits. * * @return Thenable signaling that the change has completed. */ - revert(change: { - readonly undoneEdits: readonly EditType[]; - readonly appliedEdits: readonly EditType[]; - }): Thenable; + revert(document: CustomDocument, edits: CustomDocumentRevert): Thenable; /** * Back up the resource in its current state. @@ -1311,12 +1301,50 @@ declare module 'vscode' { * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when * `auto save` is enabled (since auto save already persists resource ). * + * @param document Document to revert. * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your * extension to decided how to respond to cancellation. If for example your extension is backing up a large file * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather * than cancelling it to ensure that VS Code has some valid backup. */ - backup(cancellation: CancellationToken): Thenable; + backup(document: CustomDocument, cancellation: CancellationToken): Thenable; + } + + /** + * Event triggered by extensions to signal to VS Code that an edit has occurred on a CustomDocument``. + */ + interface CustomDocumentEditEvent { + /** + * Document the edit is for. + */ + readonly document: CustomDocument; + + /** + * Object that describes the edit. + * + * Edit objects are passed back to your extension in `undoEdits`, `applyEdits`, and `revert`. + */ + readonly edit: EditType; + + /** + * Display name describing the edit. + */ + readonly label?: string; + } + + /** + * Data about a revert for a `CustomDocument`. + */ + interface CustomDocumentRevert { + /** + * List of edits that were undone to get the document back to its on disk state. + */ + readonly undoneEdits: readonly EditType[]; + + /** + * List of edits that were reapplied to get the document back to its on disk state. + */ + readonly appliedEdits: readonly EditType[]; } /** @@ -1375,7 +1403,7 @@ declare module 'vscode' { * * @return The capabilities of the resolved document. */ - resolveCustomDocument(document: CustomDocument): Thenable; + resolveCustomDocument(document: CustomDocument): Thenable; /** * Resolve a webview editor for a given resource. @@ -1393,6 +1421,13 @@ declare module 'vscode' { * @return Thenable indicating that the webview editor has been resolved. */ resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel): Thenable; + + /** + * Defines the editing capability of a custom webview document. + * + * When not provided, the document is considered readonly. + */ + readonly editingDelegate?: CustomEditorEditingDelegate; } /** @@ -1438,7 +1473,7 @@ declare module 'vscode' { export function registerCustomEditorProvider( viewType: string, provider: CustomEditorProvider | CustomTextEditorProvider, - webviewOptions?: WebviewPanelOptions, + webviewOptions?: WebviewPanelOptions, // TODO: move this onto provider? ): Disposable; } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 0f1adcf76e..6fd7f6bf26 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -6,7 +6,7 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, IReference, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isWeb } from 'vs/base/common/platform'; @@ -21,6 +21,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; @@ -365,12 +366,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma return this._customEditorService.models.add(resource, viewType, model); } - public async $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) { - const model = await this._customEditorService.models.get(URI.revive(resource), viewType); + public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise { + const resource = URI.revive(resourceComponents); + const model = await this._customEditorService.models.get(resource, viewType); if (!model || !(model instanceof MainThreadCustomEditorModel)) { throw new Error('Could not find model for webview editor'); } - model.setDirty(state.dirty); + + model.pushEdit(editId, label); } private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { @@ -536,7 +539,9 @@ namespace HotExitState { class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { private _hotExitState: HotExitState.State = HotExitState.Allowed; - private _dirty = false; + private _currentEditIndex: number = -1; + private _savePoint: number = -1; + private readonly _edits: Array = []; public static async create( instantiationService: IInstantiationService, @@ -556,19 +561,25 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILabelService private readonly _labelService: ILabelService, @IFileService private readonly _fileService: IFileService, + @IUndoRedoService private readonly _undoService: IUndoRedoService, ) { super(); - this._register(workingCopyService.registerWorkingCopy(this)); + if (_editable) { + this._register(workingCopyService.registerWorkingCopy(this)); + } } dispose() { + if (this._editable) { + this._undoService.removeElements(this.resource); + } this._proxy.$disposeWebviewCustomEditorDocument(this.resource, this._viewType); super.dispose(); } //#region IWorkingCopy - public get resource() { return this._resource; } + public get resource() { return this._resource; } // custom://viewType/path/file public get name() { return basename(this._labelService.getUriLabel(this._resource)); @@ -579,7 +590,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public isDirty(): boolean { - return this._dirty; + return this._edits.length > 0 && this._savePoint !== this._currentEditIndex; } private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); @@ -594,31 +605,106 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return this._viewType; } - public setDirty(dirty: boolean): void { + public pushEdit(editId: number, label: string | undefined) { + if (!this._editable) { + throw new Error('Document is not editable'); + } + + this.change(() => { + this.spliceEdits(editId); + this._currentEditIndex = this._edits.length - 1; + }); + + this._undoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: this.resource, + label: label ?? localize('defaultEditLabel', "Edit"), + undo: () => this.undo(), + redo: () => this.redo(), + }); + } + + private async undo(): Promise { + if (!this._editable) { + return; + } + + if (this._currentEditIndex < 0) { + // nothing to undo + return; + } + + const undoneEdit = this._edits[this._currentEditIndex]; + await this._proxy.$undo(this.resource, this.viewType, undoneEdit); + + this.change(() => { + --this._currentEditIndex; + }); + } + + private async redo(): Promise { + if (!this._editable) { + return; + } + + if (this._currentEditIndex >= this._edits.length - 1) { + // nothing to redo + return; + } + + const redoneEdit = this._edits[this._currentEditIndex + 1]; + await this._proxy.$redo(this.resource, this.viewType, redoneEdit); + this.change(() => { + ++this._currentEditIndex; + }); + } + + private spliceEdits(editToInsert?: number) { + const start = this._currentEditIndex + 1; + const toRemove = this._edits.length - this._currentEditIndex; + + const removedEdits = typeof editToInsert === 'number' + ? this._edits.splice(start, toRemove, editToInsert) + : this._edits.splice(start, toRemove); + + if (removedEdits.length) { + this._proxy.$disposeEdits(this.resource, this._viewType, removedEdits); + } + } + + private change(makeEdit: () => void): void { + const wasDirty = this.isDirty(); + makeEdit(); this._onDidChangeContent.fire(); - if (this._dirty !== dirty) { - this._dirty = dirty; + if (this.isDirty() !== wasDirty) { this._onDidChangeDirty.fire(); } } public async revert(_options?: IRevertOptions) { - if (this._editable) { - this._proxy.$revert(this.resource, this.viewType); + if (!this._editable) { + return; } - } - public undo() { - if (this._editable) { - this._proxy.$undo(this.resource, this.viewType); + if (this._currentEditIndex === this._savePoint) { + return; } - } - public redo() { - if (this._editable) { - this._proxy.$redo(this.resource, this.viewType); + let editsToUndo: number[] = []; + let editsToRedo: number[] = []; + + if (this._currentEditIndex >= this._savePoint) { + editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex).reverse(); + } else if (this._currentEditIndex < this._savePoint) { + editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint); } + + this._proxy.$revert(this.resource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }); + this.change(() => { + this._currentEditIndex = this._savePoint; + this.spliceEdits(); + }); } public async save(_options?: ISaveOptions): Promise { @@ -626,14 +712,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod return false; } await createCancelablePromise(token => this._proxy.$onSave(this.resource, this.viewType, token)); - this.setDirty(false); + this.change(() => { + this._savePoint = this._currentEditIndex; + }); return true; } public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { if (this._editable) { await this._proxy.$onSaveAs(this.resource, this.viewType, targetResource); - this.setDirty(false); + this.change(() => { + this._savePoint = this._currentEditIndex; + }); return true; } else { // Since the editor is readonly, just copy the file over diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d63de5db49..d71a96e909 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -596,7 +596,7 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; $unregisterEditorProvider(viewType: string): void; - $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }): void; + $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; } export interface WebviewPanelViewStateData { @@ -619,9 +619,11 @@ export interface ExtHostWebviewsShape { $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<{ editable: boolean }>; $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise; - $undo(resource: UriComponents, viewType: string): void; - $redo(resource: UriComponents, viewType: string): void; - $revert(resource: UriComponents, viewType: string): void; + $undo(resource: UriComponents, viewType: string, editId: number): Promise; + $redo(resource: UriComponents, viewType: string, editId: number): Promise; + $revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise; + $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void; + $onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise; diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 0af7fc3ec2..c9a99c3762 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; @@ -18,6 +18,7 @@ import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; +import { Cache } from './cache'; import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewExtensionDescription, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; import { Disposable as VSCodeDisposable } from './extHostTypes'; @@ -245,31 +246,33 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } } -type EditType = unknown; - class CustomDocument extends Disposable implements vscode.CustomDocument { - public static create(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { - return Object.seal(new CustomDocument(proxy, viewType, uri)); + public static create( + viewType: string, + uri: vscode.Uri, + editingDelegate: vscode.CustomEditorEditingDelegate | undefined + ) { + return Object.seal(new CustomDocument(viewType, uri, editingDelegate)); } // Explicitly initialize all properties as we seal the object after creation! - #currentEditIndex: number = -1; - #savePoint: number = -1; - readonly #edits: Array = []; + readonly #_edits = new Cache('edits'); - readonly #proxy: MainThreadWebviewsShape; readonly #viewType: string; readonly #uri: vscode.Uri; + readonly #editingDelegate: vscode.CustomEditorEditingDelegate | undefined; - #capabilities: vscode.CustomEditorCapabilities | undefined = undefined; - - private constructor(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { + private constructor( + viewType: string, + uri: vscode.Uri, + editingDelegate: vscode.CustomEditorEditingDelegate | undefined, + ) { super(); - this.#proxy = proxy; this.#viewType = viewType; this.#uri = uri; + this.#editingDelegate = editingDelegate; } dispose() { @@ -292,107 +295,54 @@ class CustomDocument extends Disposable implements vscode.CustomDocument { //#region Internal - /** @internal*/ _setCapabilities(capabilities: vscode.CustomEditorCapabilities) { - if (this.#capabilities) { - throw new Error('Capabilities already provided'); - } - - this.#capabilities = capabilities; - capabilities.editing?.onDidEdit(edit => { - this.pushEdit(edit); - }); + /** @internal*/ async _revert(changes: { undoneEdits: number[], redoneEdits: number[] }) { + const editing = this.getEditingDelegate(); + const undoneEdits = changes.undoneEdits.map(id => this.#_edits.get(id, 0)); + const appliedEdits = changes.redoneEdits.map(id => this.#_edits.get(id, 0)); + return editing.revert(this, { undoneEdits, appliedEdits }); } - /** @internal*/ async _revert() { - const editing = this.getEditingCapability(); - if (this.#currentEditIndex === this.#savePoint) { - return true; - } - - - let undoneEdits: EditType[] = []; - let appliedEdits: EditType[] = []; - if (this.#currentEditIndex >= this.#savePoint) { - undoneEdits = this.#edits.slice(this.#savePoint, this.#currentEditIndex).reverse(); - } else if (this.#currentEditIndex < this.#savePoint) { - appliedEdits = this.#edits.slice(this.#currentEditIndex, this.#savePoint); - } - - this.#currentEditIndex = this.#savePoint; - this.spliceEdits(); - - await editing.revert({ undoneEdits, appliedEdits }); - - this.updateState(); - return true; + /** @internal*/ _undo(editId: number) { + const editing = this.getEditingDelegate(); + const edit = this.#_edits.get(editId, 0); + return editing.undoEdits(this, [edit]); } - /** @internal*/ _undo() { - const editing = this.getEditingCapability(); - if (this.#currentEditIndex < 0) { - // nothing to undo - return; - } - - const undoneEdit = this.#edits[this.#currentEditIndex]; - --this.#currentEditIndex; - editing.undoEdits([undoneEdit]); - this.updateState(); - } - - /** @internal*/ _redo() { - const editing = this.getEditingCapability(); - if (this.#currentEditIndex >= this.#edits.length - 1) { - // nothing to redo - return; - } - - ++this.#currentEditIndex; - const redoneEdit = this.#edits[this.#currentEditIndex]; - editing.applyEdits([redoneEdit]); - this.updateState(); + /** @internal*/ _redo(editId: number) { + const editing = this.getEditingDelegate(); + const edit = this.#_edits.get(editId, 0); + return editing.applyEdits(this, [edit]); } /** @internal*/ _save(cancellation: CancellationToken) { - return this.getEditingCapability().save(cancellation); + return this.getEditingDelegate().save(this, cancellation); } /** @internal*/ _saveAs(target: vscode.Uri) { - return this.getEditingCapability().saveAs(target); + return this.getEditingDelegate().saveAs(this, target); } /** @internal*/ _backup(cancellation: CancellationToken) { - return this.getEditingCapability().backup(cancellation); + return this.getEditingDelegate().backup(this, cancellation); + } + + /** @internal*/ _disposeEdits(editIds: number[]) { + for (const editId of editIds) { + this.#_edits.delete(editId); + } + } + + /** @internal*/ _pushEdit(edit: unknown): number { + return this.#_edits.add([edit]); } //#endregion - private pushEdit(edit: EditType) { - this.spliceEdits(edit); - - this.#currentEditIndex = this.#edits.length - 1; - this.updateState(); - } - - private updateState() { - const dirty = this.#edits.length > 0 && this.#savePoint !== this.#currentEditIndex; - this.#proxy.$onDidChangeCustomDocumentState(this.uri, this.viewType, { dirty }); - } - - private spliceEdits(editToInsert?: EditType) { - const start = this.#currentEditIndex + 1; - const toRemove = this.#edits.length - this.#currentEditIndex; - - editToInsert - ? this.#edits.splice(start, toRemove, editToInsert) - : this.#edits.splice(start, toRemove); - } - - private getEditingCapability(): vscode.CustomEditorEditingCapability { - if (!this.#capabilities?.editing) { + private getEditingDelegate(): vscode.CustomEditorEditingDelegate { + if (!this.#editingDelegate) { throw new Error('Document is not editable'); } - return this.#capabilities.editing; + return this.#editingDelegate; } } @@ -535,17 +485,24 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options: vscode.WebviewPanelOptions | undefined = {} ): vscode.Disposable { - let disposable: vscode.Disposable; + const disposables = new DisposableStore(); if ('resolveCustomTextEditor' in provider) { - disposable = this._editorProviders.addTextProvider(viewType, extension, provider); + disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options); } else { - disposable = this._editorProviders.addCustomProvider(viewType, extension, provider); + disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options); + if (provider.editingDelegate) { + disposables.add(provider.editingDelegate.onDidEdit(e => { + const document = e.document; + const editId = (document as CustomDocument)._pushEdit(e.edit); + this._proxy.$onDidEdit(document.uri, document.viewType, editId, e.label); + })); + } } return VSCodeDisposable.from( - disposable, + disposables, new VSCodeDisposable(() => { this._proxy.$unregisterEditorProvider(viewType); })); @@ -640,12 +597,11 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } const revivedResource = URI.revive(resource); - const document = CustomDocument.create(this._proxy, viewType, revivedResource); - const capabilities = await entry.provider.resolveCustomDocument(document); - document._setCapabilities(capabilities); + const document = CustomDocument.create(viewType, revivedResource, entry.provider.editingDelegate); + await entry.provider.resolveCustomDocument(document); this._documents.add(document); return { - editable: !!capabilities.editing + editable: !!entry.provider.editingDelegate, }; } @@ -702,24 +658,29 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - async $undo(resourceComponents: UriComponents, viewType: string): Promise { + $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void { const document = this.getCustomDocument(viewType, resourceComponents); - document._undo(); + document._disposeEdits(editIds); } - async $redo(resourceComponents: UriComponents, viewType: string): Promise { + async $undo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { const document = this.getCustomDocument(viewType, resourceComponents); - document._redo(); + return document._undo(editId); } - async $revert(resourceComponents: UriComponents, viewType: string): Promise { + async $redo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { const document = this.getCustomDocument(viewType, resourceComponents); - document._revert(); + return document._redo(editId); + } + + async $revert(resourceComponents: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise { + const document = this.getCustomDocument(viewType, resourceComponents); + return document._revert(changes); } async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { const document = this.getCustomDocument(viewType, resourceComponents); - document._save(cancellation); + return document._save(cancellation); } async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise { diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index b3dda06d12..41ddf35618 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -55,7 +55,7 @@ export class CloseSidebarAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.create(CloseSidebarAction, CloseSidebarAction.ID, CloseSidebarAction.LABEL), 'View: Close Side Bar ', viewCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.create(CloseSidebarAction, CloseSidebarAction.ID, CloseSidebarAction.LABEL), 'View: Close Side Bar', viewCategory); // --- Toggle Activity Bar @@ -237,7 +237,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { export class ToggleEditorVisibilityAction extends Action { static readonly ID = 'workbench.action.toggleEditorVisibility'; - static readonly LABEL = nls.localize('toggleEditor', "Toggle Editor Area"); + static readonly LABEL = nls.localize('toggleEditor', "Toggle Editor Area Visibility"); 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 54506f8a48..676b287f72 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,9 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .sidebar > .content { +/* Removed to allow progress bar positioning to escape */ +/* .monaco-workbench .sidebar > .content { overflow: hidden; -} +} */ .monaco-workbench.nosidebar > .part.sidebar { display: none !important; diff --git a/src/vs/workbench/browser/parts/views/media/paneviewlet.css b/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 9ced3bbf37..43160a1992 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -28,10 +28,14 @@ margin-left: 7px; } -.monaco-pane-view .pane > .pane-header .monaco-progress-container { +.monaco-pane-view .pane .monaco-progress-container { position: absolute; left: 0; - bottom: 0; + top: -2px; z-index: 5; height: 2px; } + +.monaco-pane-view .pane:not(.merged-header) .monaco-progress-container { + top: 20px; +} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 36775b1d02..0d0f184403 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -225,6 +225,15 @@ export abstract class ViewPane extends Pane implements IView { this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); } + get headerVisible(): boolean { + return super.headerVisible; + } + + set headerVisible(visible: boolean) { + super.headerVisible = visible; + toggleClass(this.element, 'merged-header', !visible); + } + setVisible(visible: boolean): void { if (this._isVisible !== visible) { this._isVisible = visible; @@ -294,13 +303,6 @@ export abstract class ViewPane extends Pane implements IView { const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); this.updateActionsVisibility(); - - if (this.progressBar !== undefined) { - // Progress bar - this.progressBar = this._register(new ProgressBar(this.headerContainer)); - this._register(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.hide(); - } } protected renderTwisties(container: HTMLElement): void { @@ -333,13 +335,9 @@ export abstract class ViewPane extends Pane implements IView { } getProgressIndicator() { - if (!this.headerContainer) { - return undefined; - } - if (this.progressBar === undefined) { // Progress bar - this.progressBar = this._register(new ProgressBar(this.headerContainer)); + this.progressBar = this._register(new ProgressBar(this.element)); this._register(attachProgressBarStyler(this.progressBar, this.themeService)); this.progressBar.hide(); } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index d6d580bf75..1c28991ac5 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -33,7 +33,6 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/browser/co import { ConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/browser/signService'; -import { hash } from 'vs/base/common/hash'; import { IWorkbenchConstructionOptions, IWorkspace } from 'vs/workbench/workbench.web.api'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { BACKUPS } from 'vs/platform/environment/common/environment'; @@ -51,6 +50,7 @@ import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/wi import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; class BrowserMain extends Disposable { @@ -157,7 +157,11 @@ class BrowserMain extends Disposable { const logService = new BufferLogService(this.configuration.logLevel); serviceCollection.set(ILogService, logService); - const payload = this.resolveWorkspaceInitializationPayload(); + // Resource Identity + const resourceIdentityService = this._register(new WebResourceIdentityService()); + serviceCollection.set(IResourceIdentityService, resourceIdentityService); + + const payload = await this.resolveWorkspaceInitializationPayload(resourceIdentityService); // Environment const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: payload.id, logsPath, ...this.configuration }); @@ -292,7 +296,7 @@ class BrowserMain extends Disposable { } } - private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { + private async resolveWorkspaceInitializationPayload(resourceIdentityService: IResourceIdentityService): Promise { let workspace: IWorkspace | undefined = undefined; if (this.configuration.workspaceProvider) { workspace = this.configuration.workspaceProvider.workspace; @@ -305,7 +309,8 @@ class BrowserMain extends Disposable { // Single-folder workspace if (workspace && isFolderToOpen(workspace)) { - return { id: hash(workspace.folderUri.toString()).toString(16), folder: workspace.folderUri }; + const id = await resourceIdentityService.resolveResourceIdentity(workspace.folderUri); + return { id, folder: workspace.folderUri }; } return { id: 'empty-window' }; diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index f024942545..71c0d62c4b 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -335,6 +335,7 @@ export class Workbench extends Layout { addClasses(this.container, ...workbenchClasses); addClass(document.body, platformClass); // used by our fonts + this.container.setAttribute('role', 'application'); if (isWeb) { addClass(document.body, 'web'); diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index 85905f8b1e..e76fe92eba 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts @@ -9,7 +9,6 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ConfirmResult, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -20,6 +19,7 @@ import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker' import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { @@ -28,13 +28,13 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IDialogService private readonly dialogService: IDialogService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IElectronService private readonly electronService: IElectronService, @ILogService logService: ILogService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @IEnvironmentService private readonly environmentService: IEnvironmentService ) { super(backupFileService, filesConfigurationService, workingCopyService, logService, lifecycleService); } @@ -120,32 +120,36 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // ever activated when quit is requested. let doBackup: boolean | undefined; - switch (reason) { - case ShutdownReason.CLOSE: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else if (await this.electronService.getWindowCount() > 1 || isMacintosh) { - doBackup = false; // do not backup if a window is closed that does not cause quitting of the application - } else { - doBackup = true; // backup if last window is closed on win/linux where the application quits right after - } - break; + if (this.environmentService.isExtensionDevelopment) { + doBackup = true; // always backup closing extension development window without asking to speed up debugging + } else { + switch (reason) { + case ShutdownReason.CLOSE: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else if (await this.electronService.getWindowCount() > 1 || isMacintosh) { + doBackup = false; // do not backup if a window is closed that does not cause quitting of the application + } else { + doBackup = true; // backup if last window is closed on win/linux where the application quits right after + } + break; - case ShutdownReason.QUIT: - doBackup = true; // backup because next start we restore all backups - break; + case ShutdownReason.QUIT: + doBackup = true; // backup because next start we restore all backups + break; - case ShutdownReason.RELOAD: - doBackup = true; // backup because after window reload, backups restore - break; + case ShutdownReason.RELOAD: + doBackup = true; // backup because after window reload, backups restore + break; - case ShutdownReason.LOAD: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else { - doBackup = false; // do not backup because we are switching contexts - } - break; + case ShutdownReason.LOAD: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else { + doBackup = false; // do not backup because we are switching contexts + } + break; + } } // Perform a backup of all dirty working copies unless a backup already exists @@ -247,10 +251,6 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return false; // if editors have not restored, we are not up to speed with backups and thus should not discard them } - if (this.environmentService.isExtensionDevelopment) { - return false; // extension development does not track any backups - } - return Promise.all(backupsToDiscard.map(workingCopy => this.backupFileService.discardBackup(workingCopy.resource))).then(() => false, () => false); } } diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 79ce06c7af..b4609285d8 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -32,7 +32,6 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { ILogService } from 'vs/platform/log/common/log'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; import { IElectronService } from 'vs/platform/electron/node/electron'; @@ -42,9 +41,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -60,15 +60,15 @@ class TestBackupTracker extends NativeBackupTracker { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IFileDialogService fileDialogService: IFileDialogService, @IDialogService dialogService: IDialogService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IElectronService electronService: IElectronService, @ILogService logService: ILogService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IEnvironmentService environmentService: IEnvironmentService ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, environmentService, fileDialogService, dialogService, contextService, electronService, logService, editorService); + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, electronService, logService, editorService, environmentService); // Reduce timeout for tests BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY = 10; @@ -131,8 +131,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), - configurationService, - TestEnvironmentService + configurationService )); const part = instantiationService.createInstance(EditorPart); diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 53971e97f6..fbf27b1a53 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -284,7 +284,11 @@ class ShowAccessibilityHelpAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyMod.Alt | KeyCode.F1, - weight: KeybindingWeight.EditorContrib + weight: KeybindingWeight.EditorContrib, + linux: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F1, + secondary: [KeyMod.Alt | KeyCode.F1] + } } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index ea55d2b77f..d7327f4093 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -8,7 +8,7 @@ import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; -import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoLine'; +import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoLineQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 124183066b..2b1bf0658d 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -40,7 +40,7 @@ CommandsRegistry.registerCommand('_workbench.openWith', (accessor: ServicesAcces // #region Reopen With const REOPEN_WITH_COMMAND_ID = 'reOpenWith'; -const REOPEN_WITH_TITLE = { value: nls.localize('reopenWith.title', 'Reopen With...'), original: 'Reopen With' }; +const REOPEN_WITH_TITLE = { value: nls.localize('reopenWith.title', 'Reopen With...'), original: 'Reopen With...' }; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REOPEN_WITH_COMMAND_ID, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 4611489064..3ca2f80042 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -15,6 +15,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; @@ -44,6 +45,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IEditorService private readonly editorService: IEditorService, + @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { super(id, viewType, '', webview, webviewService, webviewWorkbenchService); this._editorResource = resource; @@ -175,10 +177,12 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } public undo(): void { - assertIsDefined(this._modelRef).object.undo(); + assertIsDefined(this._modelRef); + this.undoRedoService.undo(this.resource); } public redo(): void { - assertIsDefined(this._modelRef).object.redo(); + assertIsDefined(this._modelRef); + this.undoRedoService.redo(this.resource); } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index f533eaf85d..9541de894c 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -52,8 +52,6 @@ export interface ICustomEditorModel extends IDisposable { isDirty(): boolean; readonly onDidChangeDirty: Event; - undo(): void; - redo(): void; revert(options?: IRevertOptions): Promise; save(options?: ISaveOptions): Promise; diff --git a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index 59fd6e1ddc..99123e9bb1 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -64,14 +64,6 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return this.textFileService.revert(this.resource, options); } - public undo() { - this.textFileService.files.get(this.resource)?.textEditorModel?.undo(); - } - - public redo() { - this.textFileService.files.get(this.resource)?.textEditorModel?.redo(); - } - public async save(options?: ISaveOptions): Promise { return !!await this.textFileService.save(this.resource, options); } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index e9ede6d154..66e71a3d19 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -118,7 +118,7 @@ registerCommands(); // register action to open viewlet const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenDebugPanelAction, OpenDebugPanelAction.ID, OpenDebugPanelAction.LABEL, openPanelKb), 'View: Debug Console', nls.localize('view', "View")); -registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenDebugViewletAction, OpenDebugViewletAction.ID, OpenDebugViewletAction.LABEL, openViewletKb), 'View: Show Debug', nls.localize('view', "View")); +registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenDebugViewletAction, OpenDebugViewletAction.ID, OpenDebugViewletAction.LABEL, openViewletKb), 'View: Show Run and Debug', nls.localize('view', "View")); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolBar, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index ae69b0d958..e1f4bdd46f 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -125,7 +125,8 @@ font-style: italic; } -.monaco-workbench .monaco-list-row .expression .error { +.monaco-workbench .monaco-list-row .expression .error, +.monaco-workbench .debug-pane .debug-variables .scope .error { color: #e51400; } @@ -145,7 +146,8 @@ color: rgba(204, 204, 204, 0.6); } -.vs-dark .monaco-workbench .monaco-list-row .expression .error { +.vs-dark .monaco-workbench .monaco-list-row .expression .error, +.vs-dark .monaco-workbench .debug-pane .debug-variables .scope .error { color: #f48771; } @@ -173,7 +175,8 @@ color: #ce9178; } -.hc-black .monaco-workbench .monaco-list-row .expression .error { +.hc-black .monaco-workbench .monaco-list-row .expression .error, +.hc-black .monaco-workbench .debug-pane .debug-variables .scope .error { color: #f48771; } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 78f1496103..8f474f2e39 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -316,6 +316,14 @@ animation-name: debugViewletValueChanged; } +.debug-pane .debug-variables .scope .error { + font-style: italic; + text-overflow: ellipsis; + overflow: hidden; + font-family: var(--monaco-monospace-font); + font-weight: normal; +} + /* Breakpoints */ .debug-pane .monaco-list-row { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index c4da910364..e2f0a2a201 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Variable, Scope, ErrorScope } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; @@ -97,7 +97,7 @@ export class VariablesView extends ViewPane { const treeContainer = renderViewTree(container); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'VariablesView', treeContainer, new VariablesDelegate(), - [this.instantiationService.createInstance(VariablesRenderer), new ScopesRenderer()], + [this.instantiationService.createInstance(VariablesRenderer), new ScopesRenderer(), new ScopeErrorRenderer()], new VariablesDataSource(), { ariaLabel: nls.localize('variablesAriaTreeLabel', "Debug Variables"), accessibilityProvider: new VariablesAccessibilityProvider(), @@ -217,7 +217,7 @@ function isViewModel(obj: any): obj is IViewModel { export class VariablesDataSource implements IAsyncDataSource { hasChildren(element: IViewModel | IExpression | IScope): boolean { - if (isViewModel(element) || element instanceof Scope) { + if (isViewModel(element)) { return true; } @@ -246,6 +246,10 @@ class VariablesDelegate implements IListVirtualDelegate { } getTemplateId(element: IExpression | IScope): string { + if (element instanceof ErrorScope) { + return ScopeErrorRenderer.ID; + } + if (element instanceof Scope) { return ScopesRenderer.ID; } @@ -278,6 +282,33 @@ class ScopesRenderer implements ITreeRenderer { + + static readonly ID = 'scopeError'; + + get templateId(): string { + return ScopeErrorRenderer.ID; + } + + renderTemplate(container: HTMLElement): IScopeErrorTemplateData { + const wrapper = dom.append(container, $('.scope')); + const error = dom.append(wrapper, $('.error')); + return { error }; + } + + renderElement(element: ITreeNode, index: number, templateData: IScopeErrorTemplateData): void { + templateData.error.innerText = element.element.name; + } + + disposeTemplate(): void { + // noop + } +} + export class VariablesRenderer extends AbstractExpressionsRenderer { static readonly ID = 'variable'; diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index a00ec6c466..3c896e5a35 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -190,9 +190,7 @@ export class WatchExpressionsView extends ViewPane { this.debugService.getViewModel().setSelectedExpression(expression); return Promise.resolve(); })); - if (!expression.hasChildren) { - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression.value, 'watch')); - } + actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression.value, 'watch')); actions.push(new Separator()); actions.push(new Action('debug.removeWatchExpression', nls.localize('removeWatchExpression', "Remove Expression"), undefined, true, () => { @@ -204,9 +202,7 @@ export class WatchExpressionsView extends ViewPane { actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); if (element instanceof Variable) { const variable = element as Variable; - if (!variable.hasChildren) { - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch')); - } + actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch')); actions.push(new Separator()); } actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index a425c030f8..432b90c6fe 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -88,22 +88,26 @@ export class WelcomeView extends ViewPane { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { - content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), + content: localize({ key: 'openAFileWhichCanBeDebugged', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() }); let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { - content: localize('runAndDebugAction', "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { - content: localize('customizeRunAndDebug', "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), when: WorkbenchStateContext.notEqualsTo('empty') }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { - content: localize('customizeRunAndDebugOpenFolder', "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), + content: localize({ key: 'customizeRunAndDebugOpenFolder', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), when: WorkbenchStateContext.isEqualTo('empty') }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 2cf79c3d79..ca60a5c864 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -301,6 +301,7 @@ export interface IScope extends IExpressionContainer { readonly name: string; readonly expensive: boolean; readonly range?: IRange; + readonly hasChildren: boolean; } export interface IStackFrame extends ITreeElement { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 4b18b16ca1..ae1c9a5392 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -269,6 +269,21 @@ export class Scope extends ExpressionContainer implements IScope { } } +export class ErrorScope extends Scope { + + constructor( + stackFrame: IStackFrame, + index: number, + message: string, + ) { + super(stackFrame, index, message, 0, false); + } + + toString(): string { + return this.name; + } +} + export class StackFrame implements IStackFrame { private scopes: Promise | undefined; @@ -293,7 +308,7 @@ export class StackFrame implements IStackFrame { 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) : undefined)) : []; - }, err => []); + }, err => [new ErrorScope(this, 0, err.message)]); } return this.scopes; diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 62e54ca424..68bc287080 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -70,7 +70,9 @@ declare module DebugProtocol { } /** Cancel request; value of command field is 'cancel'. - The 'cancel' request is used by the frontend to indicate that it is no longer interested in the result produced by a specific request issued earlier. + The 'cancel' request is used by the frontend in two situations: + - to indicate that it is no longer interested in the result produced by a specific request issued earlier + - to cancel a progress indicator. This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. A frontend client should only call this request if the capability 'supportsCancelRequest' is true. @@ -85,8 +87,10 @@ declare module DebugProtocol { /** Arguments for 'cancel' request. */ export interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. */ + /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ requestId?: number; + /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ + progressId?: string; } /** Response to 'cancel' request. This is just an acknowledgement, so no body field is required. */ @@ -302,6 +306,64 @@ declare module DebugProtocol { }; } + /** Event message for 'progressStart' event type. + The event signals that a long running operation is about to start and + provides additional information for the client to set up a corresponding progress and cancellation UI. + The client is free to delay the showing of the UI in order to reduce flicker. + */ + export interface ProgressStartEvent extends Event { + // event: 'progressStart'; + body: { + /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. IDs must be unique within a debug session. */ + progressId: string; + /** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */ + title: string; + /** The request ID that this progress report is related to. If specified a debug adapter is expected to emit + progress events for the long running request until the request has been either completed or cancelled. + If the request ID is omitted, the progress report is assumed to be related to some general activity of the debug adapter. + */ + requestId?: number; + /** If true, the request that reports progress may be canceled with a 'cancel' request. + So this property basically controls whether the client should use UX that supports cancellation. + Clients that don't support cancellation are allowed to ignore the setting. + */ + cancellable?: boolean; + /** Optional, more detailed progress message. */ + message?: string; + /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. */ + percentage?: number; + }; + } + + /** Event message for 'progressUpdate' event type. + The event signals that the progress reporting needs to updated with a new message and/or percentage. + The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. + */ + export interface ProgressUpdateEvent extends Event { + // event: 'progressUpdate'; + body: { + /** The ID that was introduced in the initial 'progressStart' event. */ + progressId: string; + /** Optional, more detailed progress message. If omitted, the previous message (if any) is used. */ + message?: string; + /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. */ + percentage?: number; + }; + } + + /** Event message for 'progressEnd' event type. + The event signals the end of the progress reporting with an optional final message. + */ + export interface ProgressEndEvent extends Event { + // event: 'progressEnd'; + body: { + /** The ID that was introduced in the initial 'ProgressStartEvent'. */ + progressId: string; + /** Optional, more detailed progress message. If omitted, the previous message (if any) is used. */ + message?: string; + }; + } + /** 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. */ @@ -370,6 +432,8 @@ declare module DebugProtocol { supportsRunInTerminalRequest?: boolean; /** Client supports memory references. */ supportsMemoryReferences?: boolean; + /** Client supports progress reporting. */ + supportsProgressReporting?: boolean; } /** Response to 'initialize' request. */ @@ -1100,6 +1164,7 @@ declare module DebugProtocol { 'watch': evaluate is run in a watch. 'repl': evaluate is run from REPL console. 'hover': evaluate is run from a data hover. + 'clipboard': evaluate is run to generate the value that will be stored in the clipboard. etc. */ context?: string; @@ -1409,6 +1474,8 @@ declare module DebugProtocol { supportsCancelRequest?: boolean; /** The debug adapter supports the 'breakpointLocations' request. */ supportsBreakpointLocationsRequest?: boolean; + /** The debug adapter supports the 'clipboard' context value in the 'evaluate' request. */ + supportsClipboardContext?: boolean; } /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8f1da510cc..07da944bb1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -15,7 +15,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; -import { ExtensionsLabel, IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionTipsService, IExtensionRecommendation, IExtensionsConfigContent, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} @@ -1960,7 +1960,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: { value: `${ExtensionsLabel}: ${ConfigureWorkspaceRecommendedExtensionsAction.LABEL}`, original: 'Configure Recommended Extensions (Workspace)' }, + title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, category: localize('extensions', "Extensions") }, when: this.workspaceContextKey @@ -1972,7 +1972,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: { value: `${ExtensionsLabel}: ${ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL}`, original: 'Configure Recommended Extensions (Workspace Folder)' }, + title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, category: localize('extensions', "Extensions") }, when: this.workspaceFolderContextKey @@ -1986,7 +1986,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: AddToWorkspaceRecommendationsAction.ADD_ID, - title: { value: `${ExtensionsLabel}: ${AddToWorkspaceRecommendationsAction.ADD_LABEL}`, original: 'Add to Recommended Extensions (Workspace)' }, + title: { value: AddToWorkspaceRecommendationsAction.ADD_LABEL, original: 'Add to Recommended Extensions (Workspace)' }, category: localize('extensions', "Extensions") }, when: this.addToWorkspaceRecommendationsContextKey @@ -2000,7 +2000,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: AddToWorkspaceFolderRecommendationsAction.ADD_ID, - title: { value: `${ExtensionsLabel}: ${AddToWorkspaceFolderRecommendationsAction.ADD_LABEL}`, original: 'Extensions: Add to Recommended Extensions (Workspace Folder)' }, + title: { value: AddToWorkspaceFolderRecommendationsAction.ADD_LABEL, original: 'Extensions: Add to Recommended Extensions (Workspace Folder)' }, category: localize('extensions', "Extensions") }, when: this.addToWorkspaceFolderRecommendationsContextKey @@ -2014,7 +2014,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: AddToWorkspaceRecommendationsAction.IGNORE_ID, - title: { value: `${ExtensionsLabel}: ${AddToWorkspaceRecommendationsAction.IGNORE_LABEL}`, original: 'Extensions: Ignore Recommended Extension (Workspace)' }, + title: { value: AddToWorkspaceRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace)' }, category: localize('extensions', "Extensions") }, when: this.addToWorkspaceRecommendationsContextKey @@ -2028,7 +2028,7 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, - title: { value: `${ExtensionsLabel}: ${AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL}`, original: 'Extensions: Ignore Recommended Extension (Workspace Folder)' }, + title: { value: AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace Folder)' }, category: localize('extensions', "Extensions") }, when: this.addToWorkspaceFolderRecommendationsContextKey diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index f7819c8e35..2185426283 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -12,6 +12,7 @@ import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/e import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvider { @@ -27,7 +28,7 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid super(InstallExtensionQuickAccessProvider.PREFIX); } - protected getPicks(filter: string, token: CancellationToken): Array | Promise> { + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { // Nothing typed if (!filter) { diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 1ec24de5b1..47b577a3fc 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -64,24 +64,6 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor } private registerViews(): void { - const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - - this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { - content: localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.\n[Add Folder](command:{0})", AddRootFolderAction.ID), - when: WorkbenchStateContext.isEqualTo('workspace') - })); - - const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; - this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { - content: localize('remoteNoFolderHelp', "Connected to remote.\n[Open Folder](command:{0})", commandId), - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.notEqualsTo(''), IsWebContext.toNegated()) - })); - - this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { - content: localize('noFolderHelp', "You have not yet opened a folder.\n[Open Folder](command:{0})", commandId), - when: ContextKeyExpr.or(ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.isEqualTo('')), ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), IsWebContext)) - })); - const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER); let viewDescriptorsToRegister: IViewDescriptor[] = []; @@ -275,3 +257,23 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as(Extensions.ViewsRegistry); +viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize({ key: 'noWorkspaceHelp', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "You have not yet added a folder to the workspace.\n[Add Folder](command:{0})", AddRootFolderAction.ID), + when: WorkbenchStateContext.isEqualTo('workspace') +}); + +const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; +viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize({ key: 'remoteNoFolderHelp', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "Connected to remote.\n[Open Folder](command:{0})", commandId), + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.notEqualsTo(''), IsWebContext.toNegated()) +}); + +viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize({ key: 'noFolderHelp', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "You have not yet opened a folder.\n[Open Folder](command:{0})", commandId), + when: ContextKeyExpr.or(ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.isEqualTo('')), ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), IsWebContext)) +}); diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index f45d7aeb32..a8ff47822e 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { TestFilesConfigurationService, TestEnvironmentService, workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; @@ -56,8 +56,7 @@ suite('EditorAutoSave', () => { instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), - configurationService, - TestEnvironmentService + configurationService )); const part = instantiationService.createInstance(EditorPart); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts index baaba916bb..05e91c6443 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -56,8 +56,7 @@ suite('Files - TextFileEditor', () => { instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( instantiationService.createInstance(MockContextKeyService), - configurationService, - TestEnvironmentService + configurationService )); const part = instantiationService.createInstance(EditorPart); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index a45aba273d..c395fc6c06 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -143,39 +143,26 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.output.action.turnOffAutoScroll`, - title: nls.localize('outputScrollOff', "Turn Auto Scrolling Off"), + id: `workbench.output.action.toggleAutoScroll`, + title: { value: nls.localize('toggleAutoScroll', "Toggle Auto Scrolling"), original: 'Toggle Auto Scrolling' }, + tooltip: { value: nls.localize('outputScrollOff', "Turn Auto Scrolling Off"), original: 'Turn Auto Scrolling Off' }, menu: { id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), CONTEXT_OUTPUT_SCROLL_LOCK.negate()), + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID)), group: 'navigation', order: 3, }, - icon: { id: 'codicon/unlock' } + icon: { id: 'codicon/unlock' }, + toggled: { + condition: CONTEXT_OUTPUT_SCROLL_LOCK, + icon: { id: 'codicon/lock' }, + tooltip: { value: nls.localize('outputScrollOn', "Turn Auto Scrolling On"), original: 'Turn Auto Scrolling On' } + } }); } async run(accessor: ServicesAccessor): Promise { const outputView = accessor.get(IViewsService).getActiveViewWithId(OUTPUT_VIEW_ID)!; - outputView.scrollLock = true; - } -}); -registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.output.action.turnOnAutoScroll`, - title: nls.localize('outputScrollOn', "Turn Auto Scrolling On"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), CONTEXT_OUTPUT_SCROLL_LOCK), - group: 'navigation', - order: 3, - }, - icon: { id: 'codicon/lock' }, - }); - } - async run(accessor: ServicesAccessor): Promise { - const outputView = accessor.get(IViewsService).getActiveViewWithId(OUTPUT_VIEW_ID)!; - outputView.scrollLock = false; + outputView.scrollLock = !outputView.scrollLock; } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 199c324831..42f8cd3e81 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -116,22 +116,6 @@ export class OutputViewPane extends ViewPane { this.editor.layout({ height, width }); } - getActions(): IAction[] { - if (!this.actions) { - this.actions = [ - // this._register(this.instantiationService.createInstance(SwitchOutputAction)), - // this._register(this.instantiationService.createInstance(ClearOutputAction, ClearOutputAction.ID, ClearOutputAction.LABEL)), - // this._register(this.instantiationService.createInstance(ToggleOrSetOutputScrollLockAction, ToggleOrSetOutputScrollLockAction.ID, ToggleOrSetOutputScrollLockAction.LABEL)), - // this._register(this.instantiationService.createInstance(OpenLogOutputFile)) - ]; - } - return [...super.getActions(), ...this.actions]; - } - - getSecondaryActions(): IAction[] { - return [...super.getSecondaryActions(), ...this.editor.getSecondaryActions()]; - } - getActionViewItem(action: IAction): IActionViewItem | undefined { if (action.id === 'workbench.output.action.switchBetweenOutputs') { return this.instantiationService.createInstance(SwitchOutputActionViewItem, action); diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts new file mode 100644 index 0000000000..08c5542543 --- /dev/null +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { ICommandQuickPick } from 'vs/platform/quickinput/browser/commandsQuickAccess'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { DisposableStore, toDisposable, dispose } from 'vs/base/common/lifecycle'; +import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/commandsQuickAccess'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { Language } from 'vs/base/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { + + // If extensions are not yet registered, we wait for a little moment to give them + // a chance to register so that the complete set of commands shows up as result + // We do not want to delay functionality beyond that time though to keep the commands + // functional. + private readonly extensionRegistrationRace = Promise.race([ + timeout(800), + this.extensionService.whenInstalledExtensionsRegistered() + ]); + + get activeTextEditorControl(): IEditor | undefined { return this.editorService.activeTextEditorControl; } + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IMenuService private readonly menuService: IMenuService, + @IExtensionService private readonly extensionService: IExtensionService, + @IEnvironmentService environmentService: IEnvironmentService, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @ICommandService commandService: ICommandService, + @ITelemetryService telemetryService: ITelemetryService, + @INotificationService notificationService: INotificationService + ) { + super({ showAlias: !Language.isDefaultVariant() }, instantiationService, keybindingService, commandService, telemetryService, notificationService); + } + + protected async getCommandPicks(disposables: DisposableStore, token: CancellationToken): Promise> { + + // wait for extensions registration or 800ms once + await this.extensionRegistrationRace; + + if (token.isCancellationRequested) { + return []; + } + + return [ + ...this.getCodeEditorCommandPicks(), + ...this.getGlobalCommandPicks(disposables) + ]; + } + + private getGlobalCommandPicks(disposables: DisposableStore): ICommandQuickPick[] { + const globalCommandPicks: ICommandQuickPick[] = []; + + const globalCommandsMenu = this.editorService.invokeWithinEditorContext(accessor => + this.menuService.createMenu(MenuId.CommandPalette, accessor.get(IContextKeyService)) + ); + + const globalCommandsMenuActions = globalCommandsMenu.getActions() + .reduce((r, [, actions]) => [...r, ...actions], >[]) + .filter(action => action instanceof MenuItemAction) as MenuItemAction[]; + + for (const action of globalCommandsMenuActions) { + + // Label + let label = (typeof action.item.title === 'string' ? action.item.title : action.item.title.value) || action.item.id; + + // Category + const category = typeof action.item.category === 'string' ? action.item.category : action.item.category?.value; + if (category) { + label = localize('commandWithCategory', "{0}: {1}", category, label); + } + + // Alias + const aliasLabel = typeof action.item.title !== 'string' ? action.item.title.original : undefined; + const aliasCategory = (category && action.item.category && typeof action.item.category !== 'string') ? action.item.category.original : undefined; + const commandAlias = (aliasLabel && category) ? + aliasCategory ? `${aliasCategory}: ${aliasLabel}` : `${category}: ${aliasLabel}` : + aliasLabel; + + globalCommandPicks.push({ + commandId: action.item.id, + commandAlias, + label + }); + } + + // Cleanup + globalCommandsMenu.dispose(); + disposables.add(toDisposable(() => dispose(globalCommandsMenuActions))); + + return globalCommandPicks; + } +} diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 9fcc4730d2..62ed1fd136 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -8,8 +8,10 @@ import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/ import { Registry } from 'vs/platform/registry/common/platform'; import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; import { ViewQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/viewQuickAccess'; -import { QUICK_ACCESS_COMMAND_ID } from 'vs/workbench/contrib/quickaccess/browser/quickAccessCommands'; +import { QUICK_ACCESS_COMMAND_ID, quickAccessCommand } from 'vs/workbench/contrib/quickaccess/browser/quickAccessCommands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { CommandsQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const registry = Registry.as(Extensions.Quickaccess); @@ -34,6 +36,13 @@ registry.registerQuickAccessProvider({ helpEntries: [{ description: localize('viewQuickAccess', "Open View"), needsEditor: false }] }); +registry.registerQuickAccessProvider({ + ctor: CommandsQuickAccessProvider, + prefix: CommandsQuickAccessProvider.PREFIX, + placeholder: localize('commandsQuickAccessPlaceholder', "Type the name of a command to run."), + helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), needsEditor: false }] +}); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: QUICK_ACCESS_COMMAND_ID, title: { @@ -42,3 +51,10 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { category: localize('quickAccess', "Quick Access") } }); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: QUICK_ACCESS_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + handler: quickAccessCommand.handler +}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts index 308c2f9ac6..cc4e6a0a66 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ICommand } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export const QUICK_ACCESS_COMMAND_ID = 'workbench.action.openQuickAccess'; -CommandsRegistry.registerCommand({ +export const quickAccessCommand: ICommand = { id: QUICK_ACCESS_COMMAND_ID, handler: async function (accessor: ServicesAccessor, prefix: string | null = null) { const quickInputService = accessor.get(IQuickInputService); @@ -25,4 +25,4 @@ CommandsRegistry.registerCommand({ } }] } -}); +}; diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index a14f0c1ba4..e233c94d0f 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -213,7 +213,7 @@ class CommandPaletteEditorAction extends EditorAction { super({ id: ShowAllCommandsAction.ID, label: localize('showCommands.label', "Command Palette..."), - alias: 'Command Palette', + alias: 'Command Palette...', precondition: EditorContextKeys.editorSimpleInput.toNegated(), contextMenuOpts: { group: 'z_commands', diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 9ba3c58e37..8e39e7ef8e 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -153,6 +153,10 @@ background-repeat: no-repeat; } +.scm-viewlet .monaco-list-row .resource > .name > .monaco-icon-label > .actions .action-label.codicon { + height: 22px; +} + .scm-viewlet .scm-editor { box-sizing: border-box; padding: 5px 12px 5px 16px; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 63d357c5d7..d268e6c876 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -207,7 +207,7 @@ registry.registerWorkbenchAction( registry.registerWorkbenchAction(SyncActionDescriptor.create(RerunSearchEditorSearchAction, RerunSearchEditorSearchAction.ID, RerunSearchEditorSearchAction.LABEL, { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R } }, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)), - 'Search Editor: Rerun', category); + 'Search Editor: Search Again', category); //#endregion diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts index abdf24b638..0e521be882 100644 --- a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -12,6 +12,7 @@ import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { CustomTask, ContributedTask } from 'vs/workbench/contrib/tasks/common/tasks'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class TasksQuickAccessProvider extends PickerQuickAccessProvider { @@ -28,7 +29,7 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider> { + protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { // always await extensions await this.activationPromise; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 08e08f5895..d2adb7c5dd 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -30,7 +30,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { @IUserDataSyncBackupStoreService private readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, ) { const container = this.registerSyncViewContainer(); - // Disable remote backup view until server is upgraded. + // Disable until server returns the correct timestamp // this.registerBackupView(container, true); this.registerBackupView(container, false); } diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index cf173fbd7b..9dec535d22 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -5,7 +5,6 @@ import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -import { createHash } from 'crypto'; import { webFrame } from 'electron'; import { importEntries, mark } from 'vs/base/common/performance'; import { Workbench } from 'vs/workbench/browser/workbench'; @@ -13,13 +12,11 @@ import { NativeWindow } from 'vs/workbench/electron-browser/window'; import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; import { domContentLoaded, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { stat } from 'vs/base/node/pfs'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -51,6 +48,8 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file import { basename } from 'vs/base/common/resources'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; +import { NativeResourceIdentityService } from 'vs/platform/resource/node/resourceIdentityServiceImpl'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; class DesktopMain extends Disposable { @@ -214,7 +213,10 @@ class DesktopMain extends Disposable { fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } - const payload = await this.resolveWorkspaceInitializationPayload(); + const resourceIdentityService = this._register(new NativeResourceIdentityService()); + serviceCollection.set(IResourceIdentityService, resourceIdentityService); + + const payload = await this.resolveWorkspaceInitializationPayload(resourceIdentityService); const services = await Promise.all([ this.createWorkspaceService(payload, fileService, remoteAgentService, logService).then(service => { @@ -240,7 +242,7 @@ class DesktopMain extends Disposable { return { serviceCollection, logService, storageService: services[1] }; } - private async resolveWorkspaceInitializationPayload(): Promise { + private async resolveWorkspaceInitializationPayload(resourceIdentityService: IResourceIdentityService): Promise { // Multi-root workspace if (this.environmentService.configuration.workspace) { @@ -250,7 +252,7 @@ class DesktopMain extends Disposable { // Single-folder workspace let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined; if (this.environmentService.configuration.folderUri) { - workspaceInitializationPayload = await this.resolveSingleFolderWorkspaceInitializationPayload(this.environmentService.configuration.folderUri); + workspaceInitializationPayload = await this.resolveSingleFolderWorkspaceInitializationPayload(this.environmentService.configuration.folderUri, resourceIdentityService); } // Fallback to empty workspace if we have no payload yet. @@ -270,46 +272,16 @@ class DesktopMain extends Disposable { return workspaceInitializationPayload; } - private async resolveSingleFolderWorkspaceInitializationPayload(folderUri: ISingleFolderWorkspaceIdentifier): Promise { - - // Return early the folder is not local - if (folderUri.scheme !== Schemas.file) { - return { id: createHash('md5').update(folderUri.toString()).digest('hex'), folder: folderUri }; - } - - function computeLocalDiskFolderId(folder: URI, stat: fs.Stats): string { - let ctime: number | undefined; - if (isLinux) { - ctime = stat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! - } else if (isMacintosh) { - ctime = stat.birthtime.getTime(); // macOS: birthtime is fine to use as is - } else if (isWindows) { - if (typeof stat.birthtimeMs === 'number') { - ctime = Math.floor(stat.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 = stat.birthtime.getTime(); - } - } - - // we use the ctime as extra salt to the ID so that we catch the case of a folder getting - // deleted and recreated. in that case we do not want to carry over previous state - return createHash('md5').update(folder.fsPath).update(ctime ? String(ctime) : '').digest('hex'); - } - - // For local: ensure path is absolute and exists + private async resolveSingleFolderWorkspaceInitializationPayload(folderUri: ISingleFolderWorkspaceIdentifier, resourceIdentityService: IResourceIdentityService): Promise { try { - const sanitizedFolderPath = sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd()); - const fileStat = await stat(sanitizedFolderPath); - - const sanitizedFolderUri = URI.file(sanitizedFolderPath); - return { - id: computeLocalDiskFolderId(sanitizedFolderUri, fileStat), - folder: sanitizedFolderUri - }; + const folder = folderUri.scheme === Schemas.file + ? URI.file(sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd())) // For local: ensure path is absolute + : folderUri; + const id = await resourceIdentityService.resolveResourceIdentity(folderUri); + return { id, folder }; } catch (error) { onUnexpectedError(error); } - return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check } diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 68fd49c545..ffd87e2500 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -17,7 +17,7 @@ import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/worksp import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { extname, join } from 'vs/base/common/path'; +import { join } from 'vs/base/common/path'; import { equals } from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationModel } from 'vs/platform/configuration/common/configuration'; @@ -30,7 +30,7 @@ export class UserConfiguration extends Disposable { private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; - private readonly userConfiguration: MutableDisposable = this._register(new MutableDisposable()); + private readonly userConfiguration: MutableDisposable = this._register(new MutableDisposable()); private readonly reloadConfigurationScheduler: RunOnceScheduler; constructor( @@ -52,8 +52,10 @@ export class UserConfiguration extends Disposable { } async reload(): Promise { - if (!(this.userConfiguration.value instanceof FileServiceBasedConfigurationWithNames)) { - this.userConfiguration.value = new FileServiceBasedConfigurationWithNames(resources.dirname(this.userSettingsResource), [FOLDER_SETTINGS_NAME, TASKS_CONFIGURATION_KEY], this.scopes, this.fileService); + if (!(this.userConfiguration.value instanceof FileServiceBasedConfiguration)) { + const folder = resources.dirname(this.userSettingsResource); + const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(folder, `${name}.json`)])); + this.userConfiguration.value = new FileServiceBasedConfiguration(folder.toString(), [this.userSettingsResource], standAloneConfigurationResources, this.scopes, this.fileService); this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule())); } return this.userConfiguration.value!.loadConfiguration(); @@ -64,24 +66,27 @@ export class UserConfiguration extends Disposable { } } -class FileServiceBasedConfigurationWithNames extends Disposable { +class FileServiceBasedConfiguration extends Disposable { + private readonly allResources: URI[]; private _folderSettingsModelParser: ConfigurationModelParser; private _standAloneConfigurations: ConfigurationModel[]; private _cache: ConfigurationModel; - protected readonly configurationResources: URI[]; - protected changeEventTriggerScheduler: RunOnceScheduler; - protected readonly _onDidChange: Emitter = this._register(new Emitter()); + private readonly changeEventTriggerScheduler: RunOnceScheduler; + private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - constructor(protected readonly configurationFolder: URI, - private readonly configurationNames: string[], + constructor( + name: string, + private readonly settingsResources: URI[], + private readonly standAloneConfigurationResources: [string, URI][], private readonly scopes: ConfigurationScope[] | undefined, - private fileService: IFileService) { + private fileService: IFileService + ) { super(); - this.configurationResources = this.configurationNames.map(name => resources.joinPath(this.configurationFolder, `${name}.json`)); - this._folderSettingsModelParser = new ConfigurationModelParser(this.configurationFolder.toString(), this.scopes); + this.allResources = [...this.settingsResources, ...this.standAloneConfigurationResources.map(([, resource]) => resource)]; + this._folderSettingsModelParser = new ConfigurationModelParser(name, this.scopes); this._standAloneConfigurations = []; this._cache = new ConfigurationModel(); @@ -90,30 +95,37 @@ class FileServiceBasedConfigurationWithNames extends Disposable { } async loadConfiguration(): Promise { - const configurationContents = await Promise.all(this.configurationResources.map(async resource => { - try { - const content = await this.fileService.readFile(resource); - return content.value.toString(); - } catch (error) { - if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { - errors.onUnexpectedError(error); + const resolveContents = async (resources: URI[]): Promise<(string | undefined)[]> => { + return Promise.all(resources.map(async resource => { + try { + const content = await this.fileService.readFile(resource); + return content.value.toString(); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + errors.onUnexpectedError(error); + } } - } - return undefined; - })); + return undefined; + })); + }; + + const [settingsContents, standAloneConfigurationContents] = await Promise.all([ + resolveContents(this.settingsResources), + resolveContents(this.standAloneConfigurationResources.map(([, resource]) => resource)), + ]); // reset this._standAloneConfigurations = []; this._folderSettingsModelParser.parseContent(''); // parse - if (configurationContents[0]) { - this._folderSettingsModelParser.parseContent(configurationContents[0]); + if (settingsContents[0]) { + this._folderSettingsModelParser.parseContent(settingsContents[0]); } - for (let index = 1; index < configurationContents.length; index++) { - const contents = configurationContents[index]; + for (let index = 0; index < standAloneConfigurationContents.length; index++) { + const contents = standAloneConfigurationContents[index]; if (contents) { - const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.configurationResources[index].toString(), this.configurationNames[index]); + const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.standAloneConfigurationResources[index][1].toString(), this.standAloneConfigurationResources[index][0]); standAloneConfigurationModelParser.parseContent(contents); this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); } @@ -139,49 +151,22 @@ class FileServiceBasedConfigurationWithNames extends Disposable { } protected async handleFileEvents(event: FileChangesEvent): Promise { - const events = event.changes; - let affectedByChanges = false; - - // Find changes that affect workspace configuration files - for (let i = 0, len = events.length; i < len; i++) { - const resource = events[i].resource; - const basename = resources.basename(resource); - const isJson = extname(basename) === '.json'; - const isConfigurationFolderDeleted = (events[i].type === FileChangeType.DELETED && resources.isEqual(resource, this.configurationFolder)); - - if (!isJson && !isConfigurationFolderDeleted) { - continue; // only JSON files or the actual settings folder + const isAffectedByChanges = (): boolean => { + // One of the resources has changed + if (this.allResources.some(resource => event.contains(resource))) { + return true; } - - const folderRelativePath = this.toFolderRelativePath(resource); - if (!folderRelativePath) { - continue; // event is not inside folder + // One of the resource's parent got deleted + if (this.allResources.some(resource => event.contains(resources.dirname(resource), FileChangeType.DELETED))) { + return true; } - - // Handle case where ".vscode" got deleted - if (isConfigurationFolderDeleted) { - affectedByChanges = true; - break; - } - - // only valid workspace config files - if (this.configurationResources.some(configurationResource => resources.isEqual(configurationResource, resource))) { - affectedByChanges = true; - break; - } - } - - if (affectedByChanges) { + return false; + }; + if (isAffectedByChanges()) { this.changeEventTriggerScheduler.schedule(); } } - private toFolderRelativePath(resource: URI): string | undefined { - if (resources.isEqualOrParent(resource, this.configurationFolder)) { - return resources.relativePath(this.configurationFolder, resource); - } - return undefined; - } } export class RemoteUserConfiguration extends Disposable { @@ -667,14 +652,6 @@ export interface IFolderConfiguration extends IDisposable { reprocess(): ConfigurationModel; } -class FileServiceBasedFolderConfiguration extends FileServiceBasedConfigurationWithNames implements IFolderConfiguration { - - constructor(configurationFolder: URI, workbenchState: WorkbenchState, fileService: IFileService) { - super(configurationFolder, [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY], WorkbenchState.WORKSPACE === workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES, fileService); - } - -} - class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { private readonly _onDidChange: Emitter = this._register(new Emitter()); @@ -742,13 +719,13 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat this.configurationFolder = resources.joinPath(workspaceFolder.uri, configFolderRelativePath); this.folderConfiguration = this.cachedFolderConfiguration = new CachedFolderConfiguration(workspaceFolder.uri, configFolderRelativePath, configurationCache); if (workspaceFolder.uri.scheme === Schemas.file) { - this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService); + this.folderConfiguration = this.createFileServiceBasedConfiguration(fileService); } else { whenProviderRegistered(workspaceFolder.uri, fileService) .then(() => { this.folderConfiguration.dispose(); this.folderConfigurationDisposable.dispose(); - this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService); + this.folderConfiguration = this.createFileServiceBasedConfiguration(fileService); this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); this.onDidFolderConfigurationChange(); }); @@ -769,8 +746,14 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat this._onDidChange.fire(); } + private createFileServiceBasedConfiguration(fileService: IFileService) { + const settingsResources = [resources.joinPath(this.configurationFolder, `${FOLDER_SETTINGS_NAME}.json`)]; + const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(this.configurationFolder, `${name}.json`)])); + return new FileServiceBasedConfiguration(this.configurationFolder.toString(), settingsResources, standAloneConfigurationResources, WorkbenchState.WORKSPACE === this.workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES, fileService); + } + private updateCache(): Promise { - if (this.configurationFolder.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) { + if (this.configurationFolder.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedConfiguration) { return this.folderConfiguration.loadConfiguration() .then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel)); } diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 63013052e8..db35e0e726 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -88,8 +88,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { - if (this.environmentService.isExtensionDevelopment) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev testing mode because we cannot assume we run interactive } return this.doShowSaveConfirm(fileNamesOrResources); diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts index 720ffc9993..5df7d04d32 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts @@ -6,7 +6,7 @@ import { SaveDialogOptions, OpenDialogOptions } from 'electron'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -190,16 +190,6 @@ export class FileDialogService extends AbstractFileDialogService implements IFil // Don't allow untitled schema through. return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]); } - - async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { - if (this.environmentService.isExtensionDevelopment) { - if (!this.environmentService.args['extension-development-confirm-save']) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) - } - } - - return super.doShowSaveConfirm(fileNamesOrResources); - } } registerSingleton(IFileDialogService, FileDialogService, true); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts index b5682ec163..7732692642 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -146,6 +146,10 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } } + if (extensionKind === 'web') { + // Web extensions are not yet supported to be disabled by kind + return false; + } } return true; } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index d8c6403b27..0f1e76675c 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -490,6 +490,30 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true); }); + test('test web extension on local server is not disabled by kind', async () => { + instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`) }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(localWorkspaceExtension)); + assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); + }); + + test('test web extension on remote server is not disabled by kind', async () => { + instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(localWorkspaceExtension)); + assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); + }); + + test('test web extension with no server is not disabled by kind', async () => { + instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(localWorkspaceExtension)); + assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); + }); + }); function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer { diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index cfcb6c0297..d7155c55a0 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -289,6 +289,11 @@ export const schema: IJSONSchema = { body: 'onUri', description: nls.localize('vscode.extension.activationEvents.onUri', 'An activation event emitted whenever a system-wide Uri directed towards this extension is open.'), }, + { + label: 'onCustomEditor', + body: 'onCustomEditor:${9:viewType}', + description: nls.localize('vscode.extension.activationEvents.onCustomEditor', 'An activation event emitted whenever the specified custom editor becomes visible.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 81e22eedd2..8886e18b2f 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -671,7 +671,7 @@ registerSingleton(IExtensionService, ExtensionService); class RestartExtensionHostAction extends Action { public static readonly ID = 'workbench.action.restartExtensionHost'; - public static readonly LABEL = nls.localize('restartExtensionHost', "Developer: Restart Extension Host"); + public static readonly LABEL = nls.localize('restartExtensionHost', "Restart Extension Host"); constructor( id: string, @@ -687,4 +687,4 @@ class RestartExtensionHostAction extends Action { } const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.create(RestartExtensionHostAction, RestartExtensionHostAction.ID, RestartExtensionHostAction.LABEL), 'Developer: Restart Extension Host'); +registry.registerWorkbenchAction(SyncActionDescriptor.create(RestartExtensionHostAction, RestartExtensionHostAction.ID, RestartExtensionHostAction.LABEL), 'Developer: Restart Extension Host', nls.localize('developer', "Developer")); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index e234bca978..18eb551d90 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -11,7 +11,6 @@ import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/cont import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { isWeb } from 'vs/base/common/platform'; @@ -83,8 +82,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi constructor( @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -203,7 +201,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi } get isHotExitEnabled(): boolean { - return !this.environmentService.isExtensionDevelopment && this.currentHotExitConfig !== HotExitConfiguration.OFF; + return this.currentHotExitConfig !== HotExitConfiguration.OFF; } get hotExitConfiguration(): string { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 7253ed84b5..c094dfaac1 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -293,9 +293,11 @@ export class ColorThemeData implements IWorkbenchColorTheme { const settings = tokenColors[i].settings; if (score >= foregroundScore && settings.foreground) { foreground = settings.foreground; + foregroundScore = score; } if (score >= fontStyleScore && types.isString(settings.fontStyle)) { fontStyle = settings.fontStyle; + fontStyleScore = score; } } } @@ -657,7 +659,7 @@ function nameMatcher(identifers: string[], scope: ProbeScope): number { let lastScopeIndex = scope.length - 1; let lastIdentifierIndex = findInIdents(scope[lastScopeIndex--], identifers.length); if (lastIdentifierIndex >= 0) { - const score = (lastIdentifierIndex + 1) * 0x10000 + scope.length; + const score = (lastIdentifierIndex + 1) * 0x10000 + identifers[lastIdentifierIndex].length; while (lastScopeIndex >= 0) { lastIdentifierIndex = findInIdents(scope[lastScopeIndex--], lastIdentifierIndex); if (lastIdentifierIndex === -1) { diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts index a66d61f1d9..7693d980a3 100644 --- a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -289,6 +289,41 @@ suite('Themes - TokenStyleResolving', () => { }); + + test('resolveScopes - match most specific', async () => { + const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test'); + + const customTokenColors: ITokenColorCustomizations = { + textMateRules: [ + { + scope: 'entity.name.type', + settings: { + fontStyle: 'underline', + foreground: '#A6E22E' + } + }, + { + scope: 'entity.name.type.class', + settings: { + foreground: '#FF00FF' + } + }, + { + scope: 'entity.name', + settings: { + foreground: '#FFFFFF' + } + }, + ] + }; + + themeData.setCustomTokenColors(customTokenColors); + + const tokenStyle = themeData.resolveScopes([['entity.name.type.class']]); + assertTokenStyle(tokenStyle, ts('#FF00FF', { underline: true }), 'entity.name.type.class'); + + }); + test('rule matching', async () => { const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test'); themeData.setCustomColors({ 'editor.foreground': '#000000' }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 3ce372364c..67aacf6300 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -126,7 +126,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IWorkspaceContextService, workspaceContextService); const configService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, configService); - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService(contextKeyService, configService, TestEnvironmentService)); + instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService(contextKeyService, configService)); instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); instantiationService.stub(IStorageService, new TestStorageService());