diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2dc1460b16f..012854c7309 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,3 +8,4 @@ about: Suggest an idea for this project + diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 00000000000..10d2daa7da4 --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,167 @@ +[ + { + "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": "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 index 24ac951d6f0..d2bac8edb1f 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -1,3 +1,16 @@ +# { +# perform: true, +# commands: [ +# { +# type: 'comment', +# name: 'findDuplicates', +# allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], +# action: 'comment', +# comment: "Potential duplicates:\n${potentialDuplicates}" +# } +# ] +# } + { perform: true, commands: [ diff --git a/.github/copycat.yml b/.github/copycat.yml deleted file mode 100644 index 690c803bd0a..00000000000 --- a/.github/copycat.yml +++ /dev/null @@ -1,5 +0,0 @@ -{ - perform: true, - target_owner: 'chrmarti', - target_repo: 'testissues' -} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 00000000000..35974e43535 --- /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 00000000000..34fb291329a --- /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 00000000000..3b62b3c6947 --- /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/build/builtInExtensions.json b/build/builtInExtensions.json index 63c41b403da..c8607da9d4c 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.3.317", + "version": "2020.3.1117", "forQualities": [ "insider" ], diff --git a/build/lib/i18n.js b/build/lib/i18n.js index ea1758ee57e..2e7415cd721 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -114,7 +114,7 @@ let XLF = /** @class */ (() => { for (let file in this.files) { this.appendNewLine(``, 2); for (let item of this.files[file]) { - this.addStringItem(item); + this.addStringItem(file, item); } this.appendNewLine('', 2); } @@ -154,9 +154,12 @@ let XLF = /** @class */ (() => { this.files[original].push({ id: realKey, message: message, comment: comment }); } } - addStringItem(item) { - if (!item.id || !item.message) { - throw new Error(`No item ID or value specified: ${JSON.stringify(item)}`); + addStringItem(file, item) { + if (!item.id || item.message === undefined || item.message === null) { + throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`); + } + if (item.message.length === 0) { + log(`Item with id ${item.id} in file ${file} has an empty message.`); } this.appendNewLine(``, 4); this.appendNewLine(`${item.message}`, 6); diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index d69109a9d05..b9fb3879872 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -201,7 +201,7 @@ export class XLF { for (let file in this.files) { this.appendNewLine(``, 2); for (let item of this.files[file]) { - this.addStringItem(item); + this.addStringItem(file, item); } this.appendNewLine('', 2); } @@ -243,9 +243,12 @@ export class XLF { } } - private addStringItem(item: Item): void { - if (!item.id || !item.message) { - throw new Error(`No item ID or value specified: ${JSON.stringify(item)}`); + private addStringItem(file: string, item: Item): void { + if (!item.id || item.message === undefined || item.message === null) { + throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`); + } + if (item.message.length === 0) { + log(`Item with id ${item.id} in file ${file} has an empty message.`); } this.appendNewLine(``, 4); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index bc739120444..bf16c0fa839 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -333,7 +333,7 @@ function markNodes(languageService, options) { } setColor(node, 2 /* Black */); black_queue.push(node); - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); if (references) { for (let i = 0, len = references.length; i < len; i++) { diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index c1551e8143d..16d9ab6e3e9 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -436,7 +436,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt setColor(node, NodeColor.Black); black_queue.push(node); - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); if (references) { for (let i = 0, len = references.length; i < len; i++) { diff --git a/build/package.json b/build/package.json index 2abecaa1279..dbaed69b320 100644 --- a/build/package.json +++ b/build/package.json @@ -40,10 +40,10 @@ "iconv-lite": "0.4.23", "mime": "^1.3.4", "minimatch": "3.0.4", - "minimist": "^1.2.0", + "minimist": "^1.2.2", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "3.9.0-dev.20200304", + "typescript": "^3.9.0-dev.20200313", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index ef17978a534..9b0fe8e7861 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -1780,10 +1780,10 @@ minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.0, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.2.tgz#b00a00230a1108c48c169e69a291aafda3aacd63" + integrity sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA== minimist@~0.0.1: version "0.0.10" @@ -2453,16 +2453,16 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@3.9.0-dev.20200304: - version "3.9.0-dev.20200304" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200304.tgz#3cc35357eff29dc5604b4fa56d6597e13daf86ed" - integrity sha512-eUip/GgJmjp4qtHiJDxVhE5SDDiPzBUg7KBAFUgb7HgL/tv10JAHej7fnS1i+7xrq1eDtbkJyPaYOVnhL9db7Q== - typescript@^3.0.1: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@^3.9.0-dev.20200313: + version "3.9.0-dev.20200313" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200313.tgz#f66aeb2c08268f2b1fc6d1d96e15554c6e7ed29b" + integrity sha512-85/IJPm1nEUbQDxK3aN+svIy4X3kPcAipihB3704NY1HXncJ1daNLJW1OktOacb8tD/URpIGs9nMgbUrKvglGg== + typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 7f439dd686a..593ecdff80a 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.1.0", + "vscode-css-languageservice": "^4.1.1", "vscode-languageserver": "^6.1.1" }, "devDependencies": { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index e57ff40f62e..902123900d0 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -689,10 +689,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.1.0.tgz#144c8274e0bf1719fa6f773ca684bd1c7ffd634f" - integrity sha512-iTX3dTp0Y0RFWhIux5jasI8r9swdiWVB1Z3OrZ10iDHxzkETjVPxAQ5BEQU4ag0Awc8TTg1C7sJriHQY2LO14g== +vscode-css-languageservice@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.1.1.tgz#9131dd465e4b20f3ba78ab9734b2c7cdb9237443" + integrity sha512-2r2bYbhscivRu1zqh5kNe8aYpFnfksMYC7wTpKX2HqFsSzSJYXk0sCqPaWsP5ptqz0OFBTXnzx2JlE+Nb5Edgw== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index fc43a136a01..38f6f5231b5 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 5d41458931c..0f934004cdd 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -188,6 +188,9 @@ export function activate(context: ExtensionContext) { // handle content request client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { const uri = Uri.parse(uriPath); + if (uri.scheme === 'untitled') { + return Promise.reject(new Error(localize('untitled.schema', 'Unable to load {0}', uri.toString()))); + } if (uri.scheme !== 'http' && uri.scheme !== 'https') { return workspace.openTextDocument(uri).then(doc => { schemaDocuments[uri.toString()] = true; @@ -342,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 a6fc8ec528c..a9403313931 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/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 5c7a22c74f0..23cd1744005 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -191,9 +191,9 @@ abbrev@1: integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== acorn@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" - integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== + version "6.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" + integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== ajv-errors@^1.0.0: version "1.0.1" diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 1df39d31c90..39f93305b8d 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -434,5 +434,6 @@ "terminal.ansiBrightMagenta": "#d778ff", "terminal.ansiBrightCyan": "#78ffff", "terminal.ansiBrightWhite": "#ffffff" - } + }, + "semanticHighlighting": true } diff --git a/extensions/theme-defaults/themes/dark_defaults.json b/extensions/theme-defaults/themes/dark_defaults.json index 00c2ac8c36b..89f0a5beec8 100644 --- a/extensions/theme-defaults/themes/dark_defaults.json +++ b/extensions/theme-defaults/themes/dark_defaults.json @@ -18,5 +18,6 @@ "menu.foreground": "#CCCCCC", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D" - } -} \ No newline at end of file + }, + "semanticHighlighting": true +} diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 9d11138a99b..1a03010abff 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -337,5 +337,6 @@ "foreground": "#569cd6" } } - ] -} \ No newline at end of file + ], + "semanticHighlighting": true +} diff --git a/extensions/theme-defaults/themes/light_defaults.json b/extensions/theme-defaults/themes/light_defaults.json index 018da38e939..9ea03a9e317 100644 --- a/extensions/theme-defaults/themes/light_defaults.json +++ b/extensions/theme-defaults/themes/light_defaults.json @@ -18,5 +18,6 @@ "settings.numberInputBorder": "#CECECE", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D" - } + }, + "semanticHighlighting": true } diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index 111a4a23d9b..cdd22307117 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -394,5 +394,6 @@ "foreground": "#dc3958" } } - ] + ], + "semanticHighlighting": true } diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index f0b6126d5fd..8b1fe2dd80e 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -572,5 +572,6 @@ "foreground": "#c7444a" } } - ] + ], + "semanticHighlighting": true } diff --git a/extensions/theme-monokai/themes/monokai-color-theme.json b/extensions/theme-monokai/themes/monokai-color-theme.json index c695640f299..a3050894657 100644 --- a/extensions/theme-monokai/themes/monokai-color-theme.json +++ b/extensions/theme-monokai/themes/monokai-color-theme.json @@ -476,5 +476,6 @@ "foreground": "#FD971F" } } - ] + ], + "semanticHighlighting": true } diff --git a/extensions/theme-quietlight/themes/quietlight-color-theme.json b/extensions/theme-quietlight/themes/quietlight-color-theme.json index ae19ba7889b..ffcb30cff03 100644 --- a/extensions/theme-quietlight/themes/quietlight-color-theme.json +++ b/extensions/theme-quietlight/themes/quietlight-color-theme.json @@ -494,5 +494,6 @@ "walkThrough.embeddedEditorBackground": "#00000014", "editorIndentGuide.background": "#aaaaaa60", "editorIndentGuide.activeBackground": "#777777b0" - } + }, + "semanticHighlighting": true } diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index 277e7a8db3f..8ebbf48c22b 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -385,5 +385,6 @@ "foreground": "#ec0d1e" } } - ] + ], + "semanticHighlighting": true } diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index 682444485d5..b23ff8bb85c 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -477,5 +477,6 @@ "terminal.ansiBrightMagenta": "#6c71c4", "terminal.ansiBrightCyan": "#93a1a1", "terminal.ansiBrightWhite": "#fdf6e3" - } + }, + "semanticHighlighting": true } diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index a29c8fb32f0..2c1f501d850 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -484,5 +484,6 @@ // Interactive Playground "walkThrough.embeddedEditorBackground": "#00000014" - } + }, + "semanticHighlighting": true } diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json index f8c47a29e7b..0baee6822ef 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json @@ -255,5 +255,6 @@ "foreground": "#b267e6" } } - ] + ], + "semanticHighlighting": true } diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index b3f12439bc8..e803011bfb4 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -19,7 +19,7 @@ "jsonc-parser": "^2.2.1", "rimraf": "^2.6.3", "semver": "5.5.1", - "typescript-vscode-sh-plugin": "^0.6.8", + "typescript-vscode-sh-plugin": "^0.6.10", "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.1.1" }, diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index e2f8ba9fd37..6758b85b0f6 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -216,7 +216,7 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.function: case PConst.Kind.localFunction: return vscode.CompletionItemKind.Function; - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.constructSignature: case PConst.Kind.callSignature: case PConst.Kind.indexSignature: @@ -272,7 +272,7 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.memberVariable: case PConst.Kind.class: case PConst.Kind.function: - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.keyword: case PConst.Kind.parameter: commitCharacters.push('.', ',', ';'); diff --git a/extensions/typescript-language-features/src/features/documentSymbol.ts b/extensions/typescript-language-features/src/features/documentSymbol.ts index 02c1be917d3..e119b005bab 100644 --- a/extensions/typescript-language-features/src/features/documentSymbol.ts +++ b/extensions/typescript-language-features/src/features/documentSymbol.ts @@ -16,7 +16,7 @@ const getSymbolKind = (kind: string): vscode.SymbolKind => { case PConst.Kind.class: return vscode.SymbolKind.Class; case PConst.Kind.enum: return vscode.SymbolKind.Enum; case PConst.Kind.interface: return vscode.SymbolKind.Interface; - case PConst.Kind.memberFunction: return vscode.SymbolKind.Method; + case PConst.Kind.method: return vscode.SymbolKind.Method; case PConst.Kind.memberVariable: return vscode.SymbolKind.Property; case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Property; case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Property; diff --git a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts b/extensions/typescript-language-features/src/features/implementationsCodeLens.ts index f7e325c819d..c6ea7ca6dee 100644 --- a/extensions/typescript-language-features/src/features/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/features/implementationsCodeLens.ts @@ -75,7 +75,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip return getSymbolRange(document, item); case PConst.Kind.class: - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.memberVariable: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: diff --git a/extensions/typescript-language-features/src/features/referencesCodeLens.ts b/extensions/typescript-language-features/src/features/referencesCodeLens.ts index 7aa228f8f8e..0cf8d3f0a51 100644 --- a/extensions/typescript-language-features/src/features/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/features/referencesCodeLens.ts @@ -94,7 +94,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.enum: return getSymbolRange(document, item); - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: case PConst.Kind.constructorImplementation: diff --git a/extensions/typescript-language-features/src/features/workspaceSymbols.ts b/extensions/typescript-language-features/src/features/workspaceSymbols.ts index 7b23d385c1f..e23c21eeb37 100644 --- a/extensions/typescript-language-features/src/features/workspaceSymbols.ts +++ b/extensions/typescript-language-features/src/features/workspaceSymbols.ts @@ -9,15 +9,21 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import * as fileSchemes from '../utils/fileSchemes'; import { doesResourceLookLikeAJavaScriptFile, doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription'; import * as typeConverters from '../utils/typeConverters'; +import * as PConst from '../protocol.const'; function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind { switch (item.kind) { - case 'method': return vscode.SymbolKind.Method; - case 'enum': return vscode.SymbolKind.Enum; - case 'function': return vscode.SymbolKind.Function; - case 'class': return vscode.SymbolKind.Class; - case 'interface': return vscode.SymbolKind.Interface; - case 'var': return vscode.SymbolKind.Variable; + case PConst.Kind.method: return vscode.SymbolKind.Method; + case PConst.Kind.enum: return vscode.SymbolKind.Enum; + case PConst.Kind.enumMember: return vscode.SymbolKind.EnumMember; + case PConst.Kind.function: return vscode.SymbolKind.Function; + case PConst.Kind.class: return vscode.SymbolKind.Class; + case PConst.Kind.interface: return vscode.SymbolKind.Interface; + case PConst.Kind.type: return vscode.SymbolKind.Class; + case PConst.Kind.memberVariable: return vscode.SymbolKind.Field; + case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Field; + case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Field; + case PConst.Kind.variable: return vscode.SymbolKind.Variable; default: return vscode.SymbolKind.Variable; } } @@ -25,7 +31,7 @@ function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind { class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { public constructor( private readonly client: ITypeScriptServiceClient, - private readonly modeIds: string[] + private readonly modeIds: readonly string[] ) { } public async provideWorkspaceSymbols( @@ -54,7 +60,7 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide } return response.body - .filter(item => item.containerName && item.kind !== 'alias') + .filter(item => item.containerName || item.kind !== 'alias') .map(item => this.toSymbolInformation(item)); } @@ -115,7 +121,8 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide export function register( client: ITypeScriptServiceClient, - modeIds: string[], + modeIds: readonly string[], ) { - return vscode.languages.registerWorkspaceSymbolProvider(new TypeScriptWorkspaceSymbolProvider(client, modeIds)); + return vscode.languages.registerWorkspaceSymbolProvider( + new TypeScriptWorkspaceSymbolProvider(client, modeIds)); } diff --git a/extensions/typescript-language-features/src/protocol.const.ts b/extensions/typescript-language-features/src/protocol.const.ts index a44c175f295..37fe42fd830 100644 --- a/extensions/typescript-language-features/src/protocol.const.ts +++ b/extensions/typescript-language-features/src/protocol.const.ts @@ -21,7 +21,7 @@ export class Kind { public static readonly let = 'let'; public static readonly localFunction = 'local function'; public static readonly localVariable = 'local var'; - public static readonly memberFunction = 'method'; + public static readonly method = 'method'; public static readonly memberGetAccessor = 'getter'; public static readonly memberSetAccessor = 'setter'; public static readonly memberVariable = 'property'; diff --git a/extensions/typescript-language-features/src/utils/typeConverters.ts b/extensions/typescript-language-features/src/utils/typeConverters.ts index 0026eaa1232..78333b2da0b 100644 --- a/extensions/typescript-language-features/src/utils/typeConverters.ts +++ b/extensions/typescript-language-features/src/utils/typeConverters.ts @@ -107,7 +107,7 @@ export namespace SymbolKind { case PConst.Kind.interface: return vscode.SymbolKind.Interface; case PConst.Kind.indexSignature: return vscode.SymbolKind.Method; case PConst.Kind.callSignature: return vscode.SymbolKind.Method; - case PConst.Kind.memberFunction: return vscode.SymbolKind.Method; + case PConst.Kind.method: return vscode.SymbolKind.Method; case PConst.Kind.memberVariable: return vscode.SymbolKind.Property; case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Property; case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Property; diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 82daf1829dd..1934b8a6810 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -626,10 +626,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -typescript-vscode-sh-plugin@^0.6.8: - version "0.6.8" - resolved "https://registry.yarnpkg.com/typescript-vscode-sh-plugin/-/typescript-vscode-sh-plugin-0.6.8.tgz#60d5025f2ab814496824ee997b5e9fc12c5b7f1a" - integrity sha512-XEh/GwBRsZKWQjPTODqWWiW8o8DyF7Yzfp/xvq1vyK5Z9JykFKAkx95BEmALv9x9dpc2RcLZHgVsKFXrtDABCw== +typescript-vscode-sh-plugin@^0.6.10: + version "0.6.10" + resolved "https://registry.yarnpkg.com/typescript-vscode-sh-plugin/-/typescript-vscode-sh-plugin-0.6.10.tgz#f9fdac506a3adb698d52fd01723ec78e8a5fc09e" + integrity sha512-cYycpwLnYT2oS48tac+UvVRtIFHHTcHAz/g3N2HpYftuMEBvBcsGfe2SrlnrGCa1gMheTbo+twIHhsQu9ygdvg== uri-js@^4.2.2: version "4.2.2" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 8ac6b2806ca..c6fec9f7fb2 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -10,7 +10,7 @@ "onFileSystem:memfs", "onDebug" ], - "main": "./out/extension", + "main": "./out/web-playground/extension", "engines": { "vscode": "^1.25.0" }, diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index be3509a2bfa..7a60146353b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -37,8 +37,7 @@ suite('Debug', function () { disposeAll(toDispose); }); - this.retries(2); - test('start debugging', async function () { + test.skip('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; let initializedReceived: () => void; diff --git a/extensions/vscode-api-tests/src/extension.ts b/extensions/vscode-api-tests/src/web-playground/extension.ts similarity index 100% rename from extensions/vscode-api-tests/src/extension.ts rename to extensions/vscode-api-tests/src/web-playground/extension.ts diff --git a/package.json b/package.json index 6fc79f7aba1..308165d4aa4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.44.0", - "distro": "07db0bb3dc2da82ce33f2871e0093df1b9085d06", + "distro": "de617fbc2d2b5e151b9e4f1713fcd5d29dda04ea", "author": { "name": "Microsoft Corporation" }, @@ -150,7 +150,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "3.9.0-dev.20200304", + "typescript": "^3.9.0-dev.20200313", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index a8ab18355b9..9b304e81a80 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 523a242daaa..a226019523b 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 998b1e698a2..66d3d01872d 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 94b2533d5b9..5eac56a666c 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index af06f86e2fa..3266a55df19 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -23,6 +23,7 @@ export interface IIconLabelValueOptions { hideIcon?: boolean; extraClasses?: string[]; italic?: boolean; + strikethrough?: boolean; matches?: IMatch[]; labelEscapeNewLines?: boolean; descriptionMatches?: IMatch[]; @@ -136,6 +137,10 @@ export class IconLabel extends Disposable { if (options.italic) { classes.push('italic'); } + + if (options.strikethrough) { + classes.push('strikethrough'); + } } this.domNode.className = classes.join(' '); diff --git a/src/vs/base/browser/ui/iconLabel/iconlabel.css b/src/vs/base/browser/ui/iconLabel/iconlabel.css index 8ee16195b5c..63a9056621c 100644 --- a/src/vs/base/browser/ui/iconLabel/iconlabel.css +++ b/src/vs/base/browser/ui/iconLabel/iconlabel.css @@ -60,6 +60,11 @@ font-style: italic; } +.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name, +.monaco-icon-label.strikethrough > .monaco-icon-description-container > .label-description { + text-decoration: line-through; +} + .monaco-icon-label::after { opacity: 0.75; font-size: 90%; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e5ccf0f4bf2..b188a00df7e 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -295,6 +295,10 @@ export class InputBox extends Widget { } } + public isSelectionAtEnd(): boolean { + return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd; + } + public enable(): void { this.input.removeAttribute('disabled'); } @@ -373,18 +377,6 @@ export class InputBox extends Widget { const styles = this.stylesForType(this.message.type); this.element.style.border = styles.border ? `1px solid ${styles.border}` : ''; - // ARIA Support - let alertText: string; - if (message.type === MessageType.ERROR) { - alertText = nls.localize('alertErrorMessage', "Error: {0}", message.content); - } else if (message.type === MessageType.WARNING) { - alertText = nls.localize('alertWarningMessage', "Warning: {0}", message.content); - } else { - alertText = nls.localize('alertInfoMessage', "Info: {0}", message.content); - } - - aria.alert(alertText); - if (this.hasFocus() || force) { this._showMessage(); } @@ -485,6 +477,18 @@ export class InputBox extends Widget { layout: layout }); + // ARIA Support + let alertText: string; + if (this.message.type === MessageType.ERROR) { + alertText = nls.localize('alertErrorMessage', "Error: {0}", this.message.content); + } else if (this.message.type === MessageType.WARNING) { + alertText = nls.localize('alertWarningMessage', "Warning: {0}", this.message.content); + } else { + alertText = nls.localize('alertInfoMessage', "Info: {0}", this.message.content); + } + + aria.alert(alertText); + this.state = 'open'; } diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 5f86ea5989b..bb35d213a06 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -65,7 +65,7 @@ export interface IIdentityProvider { export enum ListAriaRootRole { /** default list structure role */ - LIST = 'list', + LIST = 'listbox', /** default tree structure role */ TREE = 'tree', diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index dd943d0ed32..5fc898e82f0 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -22,6 +22,7 @@ export interface IPaneOptions { minimumBodySize?: number; maximumBodySize?: number; expanded?: boolean; + orientation?: Orientation; title: string; } @@ -50,6 +51,7 @@ export abstract class Pane extends Disposable implements IView { private body!: HTMLElement; protected _expanded: boolean; + protected _orientation: Orientation; protected _preventCollapse?: boolean; private expandedSize: number | undefined = undefined; @@ -117,11 +119,12 @@ export abstract class Pane extends Disposable implements IView { return headerSize + maximumBodySize; } - width: number = 0; + orthogonalSize: number = 0; constructor(options: IPaneOptions) { super(); this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; + this._orientation = typeof options.orientation === 'undefined' ? Orientation.VERTICAL : options.orientation; this.ariaHeaderLabel = localize('viewSection', "{0} Section", options.title); this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; @@ -206,14 +209,21 @@ export abstract class Pane extends Disposable implements IView { this.body = append(this.element, $('.pane-body')); this.renderBody(this.body); + + if (!this.isExpanded()) { + this.body.remove(); + } } - layout(height: number): void { + layout(size: number): void { const headerSize = this.headerVisible ? Pane.HEADER_SIZE : 0; + const width = this._orientation === Orientation.VERTICAL ? this.orthogonalSize : size; + const height = this._orientation === Orientation.VERTICAL ? size - headerSize : this.orthogonalSize - headerSize; + if (this.isExpanded()) { - this.layoutBody(height - headerSize, this.width); - this.expandedSize = height; + this.layoutBody(height, width); + this.expandedSize = size; } } @@ -391,7 +401,7 @@ export class PaneView extends Disposable { private dndContext: IDndContext = { draggable: null }; private el: HTMLElement; private paneItems: IPaneItem[] = []; - private width: number = 0; + private orthogonalSize: number = 0; private splitview: SplitView; private orientation: Orientation; private animationTimer: number | undefined = undefined; @@ -417,7 +427,7 @@ export class PaneView extends Disposable { const paneItem = { pane: pane, disposable: disposables }; this.paneItems.splice(index, 0, paneItem); - pane.width = this.width; + pane.orthogonalSize = this.orthogonalSize; this.splitview.addView(pane, size, index); if (this.dnd) { @@ -474,10 +484,10 @@ export class PaneView extends Disposable { } layout(height: number, width: number): void { - this.width = width; + this.orthogonalSize = this.orientation === Orientation.VERTICAL ? width : height; for (const paneItem of this.paneItems) { - paneItem.pane.width = width; + paneItem.pane.orthogonalSize = this.orthogonalSize; } this.splitview.layout(this.orientation === Orientation.HORIZONTAL ? width : height); diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 3740a128f1a..e16f6f4c708 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -392,7 +392,7 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe //#region --- fuzzyScore --- -export function createMatches(score: undefined | FuzzyScore): IMatch[] { +export function createMatches(score: undefined | FuzzyScore, offset = 0): IMatch[] { if (typeof score === 'undefined') { return []; } @@ -407,7 +407,7 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] { if (last && last.end === pos) { last.end = pos + 1; } else { - res.push({ start: pos, end: pos + 1 }); + res.push({ start: pos + offset, end: pos + 1 + offset }); } } } diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index 1902e82c312..4b47073d8e5 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as strings from 'vs/base/common/strings'; + /** * Return a hash value for an object. */ @@ -70,3 +72,235 @@ export class Hasher { return this._value; } } + +const enum SHA1Constant { + BLOCK_SIZE = 64, // 512 / 8 + UNICODE_REPLACEMENT = 0xFFFD, +} + +function leftRotate(value: number, bits: number, totalBits: number = 32): number { + // delta + bits = totalBits + const delta = totalBits - bits; + + // All ones, expect `delta` zeros aligned to the right + const mask = ~((1 << delta) - 1); + + // Join (value left-shifted `bits` bits) with (masked value right-shifted `delta` bits) + return ((value << bits) | ((mask & value) >>> delta)) >>> 0; +} + +function fill(dest: Uint8Array, index: number = 0, count: number = dest.byteLength, value: number = 0): void { + for (let i = 0; i < count; i++) { + dest[index + i] = value; + } +} + +function leftPad(value: string, length: number, char: string = '0'): string { + while (value.length < length) { + value = char + value; + } + return value; +} + +function toHexString(value: number, bitsize: number = 32): string { + return leftPad((value >>> 0).toString(16), bitsize / 4); +} + +/** + * A SHA1 implementation that works with strings and does not allocate. + */ +export class StringSHA1 { + private static _bigBlock32 = new DataView(new ArrayBuffer(320)); // 80 * 4 = 320 + + private _h0 = 0x67452301; + private _h1 = 0xEFCDAB89; + private _h2 = 0x98BADCFE; + private _h3 = 0x10325476; + private _h4 = 0xC3D2E1F0; + + private readonly _buff: Uint8Array; + private readonly _buffDV: DataView; + private _buffLen: number; + private _totalLen: number; + private _leftoverHighSurrogate: number; + private _finished: boolean; + + constructor() { + this._buff = new Uint8Array(SHA1Constant.BLOCK_SIZE + 3 /* to fit any utf-8 */); + this._buffDV = new DataView(this._buff.buffer); + this._buffLen = 0; + this._totalLen = 0; + this._leftoverHighSurrogate = 0; + this._finished = false; + } + + public update(str: string): void { + const strLen = str.length; + if (strLen === 0) { + return; + } + + const buff = this._buff; + let buffLen = this._buffLen; + let leftoverHighSurrogate = this._leftoverHighSurrogate; + let charCode: number; + let offset: number; + + if (leftoverHighSurrogate !== 0) { + charCode = leftoverHighSurrogate; + offset = -1; + leftoverHighSurrogate = 0; + } else { + charCode = str.charCodeAt(0); + offset = 0; + } + + while (true) { + let codePoint = charCode; + if (strings.isHighSurrogate(charCode)) { + if (offset + 1 < strLen) { + const nextCharCode = str.charCodeAt(offset + 1); + if (strings.isLowSurrogate(nextCharCode)) { + offset++; + codePoint = strings.computeCodePoint(charCode, nextCharCode); + } else { + // illegal => unicode replacement character + codePoint = SHA1Constant.UNICODE_REPLACEMENT; + } + } else { + // last character is a surrogate pair + leftoverHighSurrogate = charCode; + break; + } + } else if (strings.isLowSurrogate(charCode)) { + // illegal => unicode replacement character + codePoint = SHA1Constant.UNICODE_REPLACEMENT; + } + + buffLen = this._push(buff, buffLen, codePoint); + offset++; + if (offset < strLen) { + charCode = str.charCodeAt(offset); + } else { + break; + } + } + + this._buffLen = buffLen; + this._leftoverHighSurrogate = leftoverHighSurrogate; + } + + private _push(buff: Uint8Array, buffLen: number, codePoint: number): number { + if (codePoint < 0x0080) { + buff[buffLen++] = codePoint; + } else if (codePoint < 0x0800) { + buff[buffLen++] = 0b11000000 | ((codePoint & 0b00000000000000000000011111000000) >>> 6); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); + } else if (codePoint < 0x10000) { + buff[buffLen++] = 0b11100000 | ((codePoint & 0b00000000000000001111000000000000) >>> 12); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); + } else { + buff[buffLen++] = 0b11110000 | ((codePoint & 0b00000000000111000000000000000000) >>> 18); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000111111000000000000) >>> 12); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6); + buff[buffLen++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0); + } + + if (buffLen >= SHA1Constant.BLOCK_SIZE) { + this._step(); + buffLen -= SHA1Constant.BLOCK_SIZE; + this._totalLen += SHA1Constant.BLOCK_SIZE; + // take last 3 in case of UTF8 overflow + buff[0] = buff[SHA1Constant.BLOCK_SIZE + 0]; + buff[1] = buff[SHA1Constant.BLOCK_SIZE + 1]; + buff[2] = buff[SHA1Constant.BLOCK_SIZE + 2]; + } + + return buffLen; + } + + public digest(): string { + if (!this._finished) { + this._finished = true; + if (this._leftoverHighSurrogate) { + // illegal => unicode replacement character + this._leftoverHighSurrogate = 0; + this._buffLen = this._push(this._buff, this._buffLen, SHA1Constant.UNICODE_REPLACEMENT); + } + this._totalLen += this._buffLen; + this._wrapUp(); + } + + return toHexString(this._h0) + toHexString(this._h1) + toHexString(this._h2) + toHexString(this._h3) + toHexString(this._h4); + } + + private _wrapUp(): void { + this._buff[this._buffLen++] = 0x80; + fill(this._buff, this._buffLen); + + if (this._buffLen > 56) { + this._step(); + fill(this._buff); + } + + // this will fit because the mantissa can cover up to 52 bits + const ml = 8 * this._totalLen; + + this._buffDV.setUint32(56, Math.floor(ml / 4294967296), false); + this._buffDV.setUint32(60, ml % 4294967296, false); + + this._step(); + } + + private _step(): void { + const bigBlock32 = StringSHA1._bigBlock32; + const data = this._buffDV; + + for (let j = 0; j < 64 /* 16*4 */; j += 4) { + bigBlock32.setUint32(j, data.getUint32(j, false), false); + } + + for (let j = 64; j < 320 /* 80*4 */; j += 4) { + bigBlock32.setUint32(j, leftRotate((bigBlock32.getUint32(j - 12, false) ^ bigBlock32.getUint32(j - 32, false) ^ bigBlock32.getUint32(j - 56, false) ^ bigBlock32.getUint32(j - 64, false)), 1), false); + } + + let a = this._h0; + let b = this._h1; + let c = this._h2; + let d = this._h3; + let e = this._h4; + + let f: number, k: number; + let temp: number; + + for (let j = 0; j < 80; j++) { + if (j < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999; + } else if (j < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (j < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + + temp = (leftRotate(a, 5) + f + e + k + bigBlock32.getUint32(j * 4, false)) & 0xffffffff; + e = d; + d = c; + c = leftRotate(b, 30); + b = a; + a = temp; + } + + this._h0 = (this._h0 + a) & 0xffffffff; + this._h1 = (this._h1 + b) & 0xffffffff; + this._h2 = (this._h2 + c) & 0xffffffff; + this._h3 = (this._h3 + d) & 0xffffffff; + this._h4 = (this._h4 + e) & 0xffffffff; + } +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 4a26a87ee2f..21277fc2e70 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -428,29 +428,27 @@ export function commonSuffixLength(a: string, b: string): number { return len; } -// --- unicode -// http://en.wikipedia.org/wiki/Surrogate_pair -// Returns the code point starting at a specified index in a string -// Code points U+0000 to U+D7FF and U+E000 to U+FFFF are represented on a single character -// Code points U+10000 to U+10FFFF are represented on two consecutive characters -//export function getUnicodePoint(str:string, index:number, len:number):number { -// const chrCode = str.charCodeAt(index); -// if (0xD800 <= chrCode && chrCode <= 0xDBFF && index + 1 < len) { -// const nextChrCode = str.charCodeAt(index + 1); -// if (0xDC00 <= nextChrCode && nextChrCode <= 0xDFFF) { -// return (chrCode - 0xD800) << 10 + (nextChrCode - 0xDC00) + 0x10000; -// } -// } -// return chrCode; -//} +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ export function isHighSurrogate(charCode: number): boolean { return (0xD800 <= charCode && charCode <= 0xDBFF); } +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ export function isLowSurrogate(charCode: number): boolean { return (0xDC00 <= charCode && charCode <= 0xDFFF); } +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ +export function computeCodePoint(highSurrogate: number, lowSurrogate: number): number { + return ((highSurrogate - 0xD800) << 10) + (lowSurrogate - 0xDC00) + 0x10000; +} + /** * get the code point that begins at offset `offset` */ @@ -459,7 +457,7 @@ export function getNextCodePoint(str: string, len: number, offset: number): numb if (isHighSurrogate(charCode) && offset + 1 < len) { const nextCharCode = str.charCodeAt(offset + 1); if (isLowSurrogate(nextCharCode)) { - return ((charCode - 0xD800) << 10) + (nextCharCode - 0xDC00) + 0x10000; + return computeCodePoint(charCode, nextCharCode); } } return charCode; @@ -473,7 +471,7 @@ function getPrevCodePoint(str: string, offset: number): number { if (isLowSurrogate(charCode) && offset > 1) { const prevCharCode = str.charCodeAt(offset - 2); if (isHighSurrogate(prevCharCode)) { - return ((prevCharCode - 0xD800) << 10) + (charCode - 0xDC00) + 0x10000; + return computeCodePoint(prevCharCode, charCode); } } return charCode; diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index bdc79116318..0108b531460 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -85,6 +85,7 @@ const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation * and encoding. * + * ```txt * foo://example.com:8042/over/there?name=ferret#nose * \_/ \______________/\_________/ \_________/ \__/ * | | | | | @@ -92,6 +93,7 @@ const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; * | _____________________|__ * / \ / \ * urn:example:animal:ferret:nose + * ``` */ export class URI implements UriComponents { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index 1a5f4d35f6c..45c205f92a5 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -186,6 +186,11 @@ align-items: center; } +.quick-input-list .quick-input-list-rows > .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,15 +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/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 7defd5cf056..3479a6bbead 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList } from './quickInputList'; @@ -379,11 +379,12 @@ class QuickPick extends QuickInput implements IQuickPi private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); - private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onDidAcceptEmitter = this._register(new Emitter()); private readonly onDidCustomEmitter = this._register(new Emitter()); private _items: Array = []; private itemsUpdated = false; private _canSelectMany = false; + private _canAcceptInBackground = false; private _matchOnDescription = false; private _matchOnDetail = false; private _matchOnLabel = true; @@ -462,6 +463,14 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } + get canAcceptInBackground() { + return this._canAcceptInBackground; + } + + set canAcceptInBackground(canAcceptInBackground: boolean) { + this._canAcceptInBackground = canAcceptInBackground; + } + get matchOnDescription() { return this._matchOnDescription; } @@ -663,6 +672,22 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.list.domFocus(); } event.preventDefault(); + break; + case KeyCode.RightArrow: + if (!this._canAcceptInBackground) { + return; // needs to be enabled + } + + if (!this.ui.inputBox.isSelectionAtEnd()) { + return; // ensure input box selection at end + } + + if (this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + this.onDidAcceptEmitter.fire({ inBackground: true }); + } + break; } })); @@ -671,7 +696,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); } - this.onDidAcceptEmitter.fire(undefined); + this.onDidAcceptEmitter.fire({ inBackground: false }); })); this.visibleDisposables.add(this.ui.onDidCustom(() => { this.onDidCustomEmitter.fire(undefined); @@ -686,7 +711,7 @@ class QuickPick extends QuickInput implements IQuickPi this._activeItems = focusedItems as T[]; this.onDidChangeActiveEmitter.fire(focusedItems as T[]); })); - this.visibleDisposables.add(this.ui.list.onDidChangeSelection(selectedItems => { + this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => { if (this.canSelectMany) { if (selectedItems.length) { this.ui.list.setSelectedElements([]); @@ -699,7 +724,7 @@ class QuickPick extends QuickInput implements IQuickPi this._selectedItems = selectedItems as T[]; this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); if (selectedItems.length) { - this.onDidAcceptEmitter.fire(undefined); + this.onDidAcceptEmitter.fire({ inBackground: event instanceof MouseEvent && event.button === 1 /* mouse middle click */ }); } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { @@ -762,7 +787,7 @@ class QuickPick extends QuickInput implements IQuickPi if (wasTriggerKeyPressed && this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire(undefined); + this.onDidAcceptEmitter.fire({ inBackground: false }); } }); } diff --git a/src/vs/base/parts/quickinput/browser/quickInputBox.ts b/src/vs/base/parts/quickinput/browser/quickInputBox.ts index 6ae2b873034..2e796764a6e 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputBox.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputBox.ts @@ -54,7 +54,11 @@ export class QuickInputBox extends Disposable { this.inputBox.select(range); } - setPlaceholder(placeholder: string) { + isSelectionAtEnd(): boolean { + return this.inputBox.isSelectionAtEnd(); + } + + setPlaceholder(placeholder: string): void { this.inputBox.setPlaceHolder(placeholder); } @@ -90,11 +94,11 @@ export class QuickInputBox extends Disposable { return this.inputBox.hasFocus(); } - setAttribute(name: string, value: string) { + setAttribute(name: string, value: string): void { this.inputBox.inputElement.setAttribute(name, value); } - removeAttribute(name: string) { + removeAttribute(name: string): void { this.inputBox.inputElement.removeAttribute(name); } @@ -118,7 +122,7 @@ export class QuickInputBox extends Disposable { this.inputBox.layout(); } - style(styles: IInputBoxStyles) { + style(styles: IInputBoxStyles): void { this.inputBox.style(styles); } } diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index c816f564689..d339c421185 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -25,7 +25,8 @@ import { Action } from 'vs/base/common/actions'; 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 } from 'vs/base/browser/ui/list/listWidget'; +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 element.saneLabel }, openController: { shouldOpen: () => false }, // Workaround #58124 setRowLineHeight: false, multipleSelectionSupport: false, horizontalScrolling: false, + accessibilityProvider } as IListOptions); this.list.getHTMLElement().id = id; this.disposables.push(this.list); @@ -303,7 +318,7 @@ export class QuickInputList { @memoize get onDidChangeSelection() { - return Event.map(this.list.onDidChangeSelection, e => e.elements.map(e => e.item)); + return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent })); } getAllVisibleChecked() { @@ -606,3 +621,9 @@ function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: s return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor); } + +class QuickInputAccessibilityProvider implements IAccessibilityProvider { + getAriaLabel(element: ListElement): string | null { + return element.saneAriaLabel; + } +} diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 7a7d14cb0c6..c6ad0c93b76 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -24,10 +24,22 @@ export interface IQuickPickItem { ariaLabel?: string; description?: string; detail?: string; + /** + * Allows to show a keybinding next to the item to indicate + * how the item can be triggered outside of the picker using + * keyboard shortcut. + */ + keybinding?: ResolvedKeybinding; iconClasses?: string[]; italic?: boolean; + strikethrough?: boolean; highlights?: IQuickPickItemHighlights; buttons?: IQuickInputButton[]; + /** + * Wether to always show the buttons. By default buttons + * are only visible when hovering over them with the mouse + */ + buttonsAlwaysVisible?: boolean; picked?: boolean; alwaysShow?: boolean; } @@ -164,6 +176,15 @@ export interface IQuickInput extends IDisposable { hide(): void; } +export interface IQuickPickAcceptEvent { + + /** + * Signals if the picker item is to be accepted + * in the background while keeping the picker open. + */ + inBackground: boolean; +} + export interface IQuickPick extends IQuickInput { value: string; @@ -180,7 +201,14 @@ export interface IQuickPick extends IQuickInput { readonly onDidChangeValue: Event; - readonly onDidAccept: Event; + readonly onDidAccept: Event; + + /** + * If enabled, will fire the `onDidAccept` event when + * pressing the arrow-right key with the idea of accepting + * the selected item without closing the picker. + */ + canAcceptInBackground: boolean; ok: boolean | 'default'; diff --git a/src/vs/base/parts/tree/browser/treeViewModel.ts b/src/vs/base/parts/tree/browser/treeViewModel.ts index 823f0bb0a9d..b78c4d8184f 100644 --- a/src/vs/base/parts/tree/browser/treeViewModel.ts +++ b/src/vs/base/parts/tree/browser/treeViewModel.ts @@ -45,8 +45,7 @@ export class HeightMap { totalSize = viewItem.top + viewItem.height; } - let boundSplice = this.heightMap.splice.bind(this.heightMap, i, 0); - + const startingIndex = i; let itemsToInsert: IViewItem[] = []; while (item = iterator.next()) { @@ -58,7 +57,7 @@ export class HeightMap { sizeDiff += viewItem.height; } - boundSplice.apply(this.heightMap, itemsToInsert); + this.heightMap.splice(startingIndex, 0, ...itemsToInsert); for (j = i; j < this.heightMap.length; j++) { viewItem = this.heightMap[j]; diff --git a/src/vs/base/test/common/hash.test.ts b/src/vs/base/test/common/hash.test.ts index 58b5904b63d..3225caf7b23 100644 --- a/src/vs/base/test/common/hash.test.ts +++ b/src/vs/base/test/common/hash.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { hash } from 'vs/base/common/hash'; +import { hash, StringSHA1 } from 'vs/base/common/hash'; suite('Hash', () => { test('string', () => { @@ -53,4 +53,28 @@ suite('Hash', () => { assert.notEqual(a, b); }); + function checkSHA1(strings: string[], expected: string) { + const hash = new StringSHA1(); + for (const str of strings) { + hash.update(str); + } + const actual = hash.digest(); + assert.equal(actual, expected); + } + + test('sha1-1', () => { + checkSHA1(['\udd56'], '9bdb77276c1852e1fb067820472812fcf6084024'); + }); + + test('sha1-2', () => { + checkSHA1(['\udb52'], '9bdb77276c1852e1fb067820472812fcf6084024'); + }); + + test('sha1-3', () => { + checkSHA1(['\uda02ꑍ'], '9b483a471f22fe7e09d83f221871a987244bbd3f'); + }); + + test('sha1-4', () => { + checkSHA1(['hello'], 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'); + }); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 0e6c2358199..e8532f91590 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -49,17 +49,16 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel, UserDataSyncBackupStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel, UserDataSyncBackupStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-browser/userDataAutoSyncService'; -import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -197,7 +196,6 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService)); - services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); @@ -229,10 +227,6 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const userDataSyncBackupStoreServiceChannel = new UserDataSyncBackupStoreServiceChannel(userDataSyncBackupStoreService); server.registerChannel('userDataSyncBackupStoreService', userDataSyncBackupStoreServiceChannel); - const settingsSyncService = accessor.get(ISettingsSyncService); - const settingsSyncChannel = new SettingsSyncChannel(settingsSyncService); - server.registerChannel('settingsSync', settingsSyncChannel); - const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); server.registerChannel('userDataSync', userDataSyncChannel); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index de0a7dff13a..5ee3e79c644 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -28,12 +28,13 @@ import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/commo import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { endsWith } from 'vs/base/common/strings'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { IFileService } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { IStorageMainService } from 'vs/platform/storage/node/storageMainService'; +import { IFileService } from 'vs/platform/files/common/files'; const RUN_TEXTMATE_IN_WORKER = false; @@ -97,6 +98,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IFileService private readonly fileService: IFileService, + @IStorageMainService private readonly storageService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, @@ -226,7 +228,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService); + const that = this; + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { + get(key) { return that.storageService.get(key); }, + store(key, value) { that.storageService.store(key, value); } + }); // Eventing this.registerListeners(); diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 9a7011dc1d4..46d36912654 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -616,7 +616,15 @@ class RenderedViewLine implements IRenderedViewLine { if (!r || r.length === 0) { return -1; } - return r[0].left; + const result = r[0].left; + if (this.input.isBasicASCII) { + const charOffset = this._characterMapping.getAbsoluteOffsets(); + const expectedResult = Math.round(this.input.spaceWidth * charOffset[column - 1]); + if (Math.abs(expectedResult - result) <= 1) { + return expectedResult; + } + } + return result; } private _readRawVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index deb082c6c3c..59e61125845 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1759,6 +1759,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (ranges.length > 0) { this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, + semanticTokensApplied: false, ranges: ranges }); } @@ -1769,6 +1770,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, + semanticTokensApplied: tokens !== null, ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] }); } @@ -1783,6 +1785,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._tokens.flush(); this._emitModelTokensChangedEvent({ tokenizationSupportChanged: true, + semanticTokensApplied: false, ranges: [{ fromLineNumber: 1, toLineNumber: this._buffer.getLineCount() @@ -1795,6 +1798,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, + semanticTokensApplied: false, ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] }); } diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 17f45db65ad..6511d02173f 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -87,6 +87,7 @@ export interface IModelDecorationsChangedEvent { */ export interface IModelTokensChangedEvent { readonly tokenizationSupportChanged: boolean; + readonly semanticTokensApplied: boolean; readonly ranges: { /** * The start of the range (inclusive) diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 288ba00dedf..0db7b1795d6 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -466,15 +466,16 @@ class SemanticColoringFeature extends Disposable { private _watchers: Record; private _semanticStyling: SemanticStyling; - private _configurationService: IConfigurationService; constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, logService: ILogService) { super(); - this._configurationService = configurationService; this._watchers = Object.create(null); this._semanticStyling = this._register(new SemanticStyling(themeService, logService)); const isSemanticColoringEnabled = (model: ITextModel) => { + if (!themeService.getColorTheme().semanticHighlighting) { + return false; + } const options = configurationService.getValue(SemanticColoringFeature.SETTING_ID, { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }); return options && options.enabled; }; @@ -485,6 +486,20 @@ class SemanticColoringFeature extends Disposable { modelSemanticColoring.dispose(); delete this._watchers[model.uri.toString()]; }; + const handleSettingOrThemeChange = () => { + for (let model of modelService.getModels()) { + const curr = this._watchers[model.uri.toString()]; + if (isSemanticColoringEnabled(model)) { + if (!curr) { + register(model); + } + } else { + if (curr) { + deregister(model, curr); + } + } + } + }; this._register(modelService.onModelAdded((model) => { if (isSemanticColoringEnabled(model)) { register(model); @@ -496,22 +511,12 @@ class SemanticColoringFeature extends Disposable { deregister(model, curr); } })); - this._configurationService.onDidChangeConfiguration(e => { + this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(SemanticColoringFeature.SETTING_ID)) { - for (let model of modelService.getModels()) { - const curr = this._watchers[model.uri.toString()]; - if (isSemanticColoringEnabled(model)) { - if (!curr) { - register(model); - } - } else { - if (curr) { - deregister(model, curr); - } - } - } + handleSettingOrThemeChange(); } - }); + })); + this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange)); } } @@ -525,12 +530,9 @@ class SemanticStyling extends Disposable { ) { super(); this._caches = new WeakMap(); - if (this._themeService) { - // workaround for tests which use undefined... :/ - this._register(this._themeService.onDidColorThemeChange(() => { - this._caches = new WeakMap(); - })); - } + this._register(this._themeService.onDidColorThemeChange(() => { + this._caches = new WeakMap(); + })); } public get(provider: DocumentSemanticTokensProvider): SemanticColoringProviderStyling { @@ -768,14 +770,12 @@ class ModelSemanticColoring extends Disposable { this._fetchSemanticTokens.schedule(); })); - if (themeService) { - // workaround for tests which use undefined... :/ - this._register(themeService.onDidColorThemeChange(_ => { - // clear out existing tokens - this._setSemanticTokens(null, null, null, []); - this._fetchSemanticTokens.schedule(); - })); - } + this._register(themeService.onDidColorThemeChange(_ => { + // clear out existing tokens + this._setSemanticTokens(null, null, null, []); + this._fetchSemanticTokens.schedule(); + })); + this._fetchSemanticTokens.schedule(0); } diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 164708bd894..e915d340fb3 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -36,12 +36,6 @@ export namespace InspectTokensNLS { } export namespace GoToLineNLS { - export const gotoLineLabelValidLineAndColumn = nls.localize('gotoLineLabelValidLineAndColumn', "Go to line {0} and character {1}"); - export const gotoLineLabelValidLine = nls.localize('gotoLineLabelValidLine', "Go to line {0}"); - export const gotoLineLabelEmptyWithLineLimit = nls.localize('gotoLineLabelEmptyWithLineLimit', "Type a line number between 1 and {0} to navigate to"); - export const gotoLineLabelEmptyWithLineAndColumnLimit = nls.localize('gotoLineLabelEmptyWithLineAndColumnLimit', "Type a character between 1 and {0} to navigate to"); - export const gotoLineAriaLabel = nls.localize('gotoLineAriaLabel', "Current Line: {0}. Go to line {1}."); - export const gotoLineActionInput = nls.localize('gotoLineActionInput', "Type a line number, followed by an optional colon and a character number to navigate to"); export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line..."); } @@ -50,27 +44,13 @@ export namespace QuickHelpNLS { } export namespace QuickCommandNLS { - export const ariaLabelEntryWithKey = nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands"); - export const ariaLabelEntry = nls.localize('ariaLabelEntry', "{0}, commands"); - export const quickCommandActionInput = nls.localize('quickCommandActionInput', "Type the name of an action you want to execute"); export const quickCommandActionLabel = nls.localize('quickCommandActionLabel', "Command Palette"); + export const quickCommandHelp = nls.localize('quickCommandActionHelp', "Show And Run Commands"); } export namespace QuickOutlineNLS { - export const entryAriaLabel = nls.localize('entryAriaLabel', "{0}, symbols"); - export const quickOutlineActionInput = nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to"); export const quickOutlineActionLabel = nls.localize('quickOutlineActionLabel', "Go to Symbol..."); - export const _symbols_ = nls.localize('symbols', "symbols ({0})"); - export const _modules_ = nls.localize('modules', "modules ({0})"); - export const _class_ = nls.localize('class', "classes ({0})"); - export const _interface_ = nls.localize('interface', "interfaces ({0})"); - export const _method_ = nls.localize('method', "methods ({0})"); - export const _function_ = nls.localize('function', "functions ({0})"); - export const _property_ = nls.localize('property', "properties ({0})"); - export const _variable_ = nls.localize('variable', "variables ({0})"); - export const _variable2_ = nls.localize('variable2', "variables ({0})"); - export const _constructor_ = nls.localize('_constructor', "constructors ({0})"); - export const _call_ = nls.localize('call', "calls ({0})"); + export const quickOutlineByCategoryActionLabel = nls.localize('quickOutlineByCategoryActionLabel', "Go to Symbol by Category..."); } export namespace StandaloneCodeEditorNLS { diff --git a/src/vs/editor/contrib/caretOperations/caretOperations.ts b/src/vs/editor/contrib/caretOperations/caretOperations.ts index 3b715b871e7..abd6ab267fd 100644 --- a/src/vs/editor/contrib/caretOperations/caretOperations.ts +++ b/src/vs/editor/contrib/caretOperations/caretOperations.ts @@ -42,8 +42,8 @@ class MoveCaretLeftAction extends MoveCaretAction { constructor() { super(true, { id: 'editor.action.moveCarretLeftAction', - label: nls.localize('caret.moveLeft', "Move Caret Left"), - alias: 'Move Caret Left', + label: nls.localize('caret.moveLeft', "Move Selected Text Left"), + alias: 'Move Selected Text Left', precondition: EditorContextKeys.writable }); } @@ -53,8 +53,8 @@ class MoveCaretRightAction extends MoveCaretAction { constructor() { super(false, { id: 'editor.action.moveCarretRightAction', - label: nls.localize('caret.moveRight', "Move Caret Right"), - alias: 'Move Caret Right', + label: nls.localize('caret.moveRight', "Move Selected Text Right"), + alias: 'Move Selected Text Right', precondition: EditorContextKeys.writable }); } diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 84f1ac852c1..2f2721cf150 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -419,7 +419,7 @@ registerEditorAction(class ShowLensesInCurrentLine extends EditorAction { super({ id: 'codelens.showLensesInCurrentLine', precondition: EditorContextKeys.hasCodeLensProvider, - label: localize('showLensOnLine', "Show Code Lens Command For Current Line"), + label: localize('showLensOnLine', "Show Code Lens Commands For Current Line"), alias: 'Show Code Lens Commands For Current Line', }); } diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts new file mode 100644 index 00000000000..3354dd93e55 --- /dev/null +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractCommandsQuickAccessProvider, ICommandQuickPick, ICommandsQuickAccessOptions } from 'vs/platform/quickinput/browser/commandsQuickAccess'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ICommandService } from 'vs/platform/commands/common/commands'; + +export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider { + + constructor( + options: ICommandsQuickAccessOptions, + instantiationService: IInstantiationService, + keybindingService: IKeybindingService, + commandService: ICommandService, + telemetryService: ITelemetryService, + notificationService: INotificationService + ) { + super(options, instantiationService, keybindingService, commandService, telemetryService, notificationService); + } + + /** + * Subclasses to provide the current active editor control. + */ + protected abstract activeTextEditorControl: IEditor | undefined; + + protected getCodeEditorCommandPicks(): ICommandQuickPick[] { + const activeTextEditorControl = this.activeTextEditorControl; + if (!activeTextEditorControl) { + return []; + } + + const editorCommandPicks: ICommandQuickPick[] = []; + for (const editorAction of activeTextEditorControl.getSupportedActions()) { + editorCommandPicks.push({ + commandId: editorAction.id, + commandAlias: editorAction.alias, + label: editorAction.label || editorAction.id, + }); + } + + return editorCommandPicks; + } +} diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts new file mode 100644 index 00000000000..5926c74dce5 --- /dev/null +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, OverviewRulerLane, ITextModel } from 'vs/editor/common/model'; +import { IRange } from 'vs/editor/common/core/range'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; +import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { once } from 'vs/base/common/functional'; + +interface IEditorLineDecoration { + rangeHighlightId: string; + overviewRulerDecorationId: string; +} + +/** + * A reusable quick access provider for the editor with support + * for adding decorations for navigating in the currently active file + * (for example "Go to line", "Go to symbol"). + */ +export abstract class AbstractEditorNavigationQuickAccessProvider implements IQuickAccessProvider { + + //#region Provider methods + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Disable filtering & sorting, we control the results + picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + + // Provide based on current active editor + let pickerDisposable = this.doProvide(picker, token); + disposables.add(toDisposable(() => pickerDisposable.dispose())); + + // Re-create whenever the active editor changes + disposables.add(this.onDidActiveTextEditorControlChange(() => { + pickerDisposable.dispose(); + pickerDisposable = this.doProvide(picker, token); + })); + + return disposables; + } + + private doProvide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // With text control + const editor = this.activeTextEditorControl; + if (editor && this.canProvideWithTextEditor(editor)) { + + // Restore any view state if this picker was closed + // without actually going to a line + const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + once(token.onCancellationRequested)(() => { + if (lastKnownEditorViewState) { + editor.restoreViewState(lastKnownEditorViewState); + } + }); + + // Clean up decorations on dispose + disposables.add(toDisposable(() => this.clearDecorations(editor))); + + // Ask subclass for entries + disposables.add(this.provideWithTextEditor(editor, picker, token)); + } + + // Without text control + else { + disposables.add(this.provideWithoutTextEditor(picker, token)); + } + + return disposables; + } + + /** + * Subclasses to implement if they can operate on the text editor. + */ + protected canProvideWithTextEditor(editor: IEditor): boolean { + return true; + } + + /** + * Subclasses to implement to provide picks for the picker when an editor is active. + */ + protected abstract provideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable; + + /** + * Subclasses to implement to provide picks for the picker when no editor is active. + */ + protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; + + protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + editor.setSelection(range); + editor.revealRangeInCenter(range, ScrollType.Smooth); + editor.focus(); + } + + protected getModel(editor: IEditor | IDiffEditor): ITextModel | undefined { + return isDiffEditor(editor) ? + editor.getModel()?.modified : + editor.getModel() as ITextModel; + } + + //#endregion + + + //#region Editor access + + /** + * Subclasses to provide an event when the active editor control changes. + */ + protected abstract readonly onDidActiveTextEditorControlChange: Event; + + /** + * Subclasses to provide the current active editor control. + */ + protected abstract activeTextEditorControl: IEditor | undefined; + + //#endregion + + + //#region Decorations Utils + + private rangeHighlightDecorationId: IEditorLineDecoration | undefined = undefined; + + protected addDecorations(editor: IEditor, range: IRange): void { + editor.changeDecorations(changeAccessor => { + + // Reset old decorations if any + const deleteDecorations: string[] = []; + if (this.rangeHighlightDecorationId) { + deleteDecorations.push(this.rangeHighlightDecorationId.overviewRulerDecorationId); + deleteDecorations.push(this.rangeHighlightDecorationId.rangeHighlightId); + + this.rangeHighlightDecorationId = undefined; + } + + // Add new decorations for the range + const newDecorations: IModelDeltaDecoration[] = [ + + // highlight the entire line on the range + { + range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }, + + // also add overview ruler highlight + { + range, + options: { + overviewRuler: { + color: themeColorFromId(overviewRulerRangeHighlight), + position: OverviewRulerLane.Full + } + } + } + ]; + + const [rangeHighlightId, overviewRulerDecorationId] = changeAccessor.deltaDecorations(deleteDecorations, newDecorations); + + this.rangeHighlightDecorationId = { rangeHighlightId, overviewRulerDecorationId }; + }); + } + + protected clearDecorations(editor: IEditor): void { + const rangeHighlightDecorationId = this.rangeHighlightDecorationId; + if (rangeHighlightDecorationId) { + editor.changeDecorations(changeAccessor => { + changeAccessor.deltaDecorations([ + rangeHighlightDecorationId.overviewRulerDecorationId, + rangeHighlightDecorationId.rangeHighlightId + ], []); + }); + + this.rangeHighlightDecorationId = undefined; + } + } + + //#endregion +} diff --git a/src/vs/editor/contrib/quickAccess/gotoLine.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts similarity index 60% rename from src/vs/editor/contrib/quickAccess/gotoLine.ts rename to src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index 941c3f654f9..e24cf811a69 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLine.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -4,74 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { once } from 'vs/base/common/functional'; -import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IRange } from 'vs/editor/common/core/range'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { AbstractEditorQuickAccessProvider } from 'vs/editor/contrib/quickAccess/quickAccess'; +import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { IPosition } from 'vs/editor/common/core/position'; -export const GOTO_LINE_PREFIX = ':'; - interface IGotoLineQuickPickItem extends IQuickPickItem, Partial { } -export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorQuickAccessProvider { +export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider { - provide(picker: IQuickPick, token: CancellationToken): IDisposable { - const disposables = new DisposableStore(); + static PREFIX = ':'; - // Disable filtering & sorting, we control the results - picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line."); - // Provide based on current active editor - let pickerDisposable = this.doProvide(picker, token); - disposables.add(toDisposable(() => pickerDisposable.dispose())); - - // Re-create whenever the active editor changes - disposables.add(this.onDidActiveTextEditorControlChange(() => { - pickerDisposable.dispose(); - pickerDisposable = this.doProvide(picker, token); - })); - - return disposables; - } - - private doProvide(picker: IQuickPick, token: CancellationToken): IDisposable { - - // With text control - if (this.activeTextEditorControl) { - return this.doProvideWithTextEditor(this.activeTextEditorControl, picker, token); - } - - // Without text control - return this.doProvideWithoutTextEditor(picker); - } - - private doProvideWithoutTextEditor(picker: IQuickPick): IDisposable { - const label = localize('cannotRunGotoLine', "Open a text file first to go to a line."); picker.items = [{ label }]; picker.ariaLabel = label; return Disposable.None; } - private doProvideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable { + protected provideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable { const disposables = new DisposableStore(); - // Restore any view state if this picker was closed - // without actually going to a line - const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); - once(token.onCancellationRequested)(() => { - if (lastKnownEditorViewState) { - editor.restoreViewState(lastKnownEditorViewState); - } - }); - // Goto line once picked disposables.add(picker.onDidAccept(() => { const [item] = picker.selectedItems; @@ -80,7 +38,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor return; } - this.gotoLine(editor, this.toRange(item.lineNumber, item.column), picker.keyMods); + this.gotoLocation(editor, this.toRange(item.lineNumber, item.column), picker.keyMods); picker.hide(); } @@ -88,7 +46,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor // React to picker changes const updatePickerAndEditor = () => { - const position = this.parsePosition(editor, picker.value.trim().substr(GOTO_LINE_PREFIX.length)); + const position = this.parsePosition(editor, picker.value.trim().substr(AbstractGotoLineQuickAccessProvider.PREFIX.length)); const label = this.getPickLabel(editor, position.lineNumber, position.column); // Picker @@ -117,9 +75,6 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor updatePickerAndEditor(); disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor())); - // Clean up decorations on dispose - disposables.add(toDisposable(() => this.clearDecorations(editor))); - return disposables; } @@ -191,16 +146,4 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor private lineCount(editor: IEditor): number { return this.getModel(editor)?.getLineCount() ?? 0; } - - private getModel(editor: IEditor | IDiffEditor): ITextModel | undefined { - return isDiffEditor(editor) ? - editor.getModel()?.modified : - editor.getModel() as ITextModel; - } - - protected gotoLine(editor: IEditor, range: IRange, keyMods: IKeyMods): void { - editor.setSelection(range); - editor.revealRangeInCenter(range, ScrollType.Smooth); - editor.focus(); - } } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts new file mode 100644 index 00000000000..c70d4e06383 --- /dev/null +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -0,0 +1,420 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; +import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { values } from 'vs/base/common/collections'; +import { trim, format } from 'vs/base/common/strings'; +import { fuzzyScore, FuzzyScore, createMatches } from 'vs/base/common/filters'; + +interface IGotoSymbolQuickPickItem extends IQuickPickItem { + kind: SymbolKind, + index: number, + score?: FuzzyScore; + range?: { decoration: IRange, selection: IRange }, +} + +export interface IGotoSymbolQuickAccessProviderOptions { + openSideBySideDirection: () => undefined | 'right' | 'down' +} + +export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider { + + static PREFIX = '@'; + static SCOPE_PREFIX = ':'; + static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`; + + constructor(private options?: IGotoSymbolQuickAccessProviderOptions) { + super(); + } + + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + const label = localize('cannotRunGotoSymbolWithoutEditor', "Open a text editor first to go to a symbol."); + + picker.items = [{ label, index: 0, kind: SymbolKind.String }]; + picker.ariaLabel = label; + + return Disposable.None; + } + + protected provideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable { + const model = this.getModel(editor); + if (!model) { + return Disposable.None; + } + + // Provide symbols from model if available in registry + if (DocumentSymbolProviderRegistry.has(model)) { + return this.doProvideWithEditorSymbols(editor, model, picker, token); + } + + // Otherwise show an entry for a model without registry + // But give a chance to resolve the symbols at a later + // point if possible + return this.doProvideWithoutEditorSymbols(editor, model, picker, token); + } + + private doProvideWithoutEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Generic pick for not having any symbol information + const label = localize('cannotRunGotoSymbolWithoutSymbolProvider', "Open a text editor with symbol information first to go to a symbol."); + picker.items = [{ label, index: 0, kind: SymbolKind.String }]; + picker.ariaLabel = label; + + // Listen to changes to the registry and see if eventually + // we do get symbols. This can happen if the picker is opened + // very early after the model has loaded but before the + // language registry is ready. + // https://github.com/microsoft/vscode/issues/70607 + const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => { + if (DocumentSymbolProviderRegistry.has(model)) { + symbolProviderListener.dispose(); + + disposables.add(this.doProvideWithEditorSymbols(editor, model, picker, token)); + } + })); + + return disposables; + } + + private doProvideWithEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Goto symbol once picked + disposables.add(picker.onDidAccept(() => { + const [item] = picker.selectedItems; + if (item && item.range) { + this.gotoLocation(editor, item.range.selection, picker.keyMods); + + picker.hide(); + } + })); + + // Goto symbol side by side if enabled + disposables.add(picker.onDidTriggerItemButton(({ item }) => { + if (item && item.range) { + this.gotoLocation(editor, item.range.selection, picker.keyMods, true); + + picker.hide(); + } + })); + + // Resolve symbols from document once and reuse this + // request for all filtering and typing then on + const symbolsPromise = this.getDocumentSymbols(model, true, token); + + // Set initial picks and update on type + let picksCts: CancellationTokenSource | undefined = undefined; + const updatePickerItems = async () => { + + // Cancel any previous ask for picks and busy + picksCts?.dispose(true); + picker.busy = false; + + // Create new cancellation source for this run + picksCts = new CancellationTokenSource(token); + + // Collect symbol picks + picker.busy = true; + try { + const items = await this.getSymbolPicks(symbolsPromise, picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim(), picksCts.token); + if (token.isCancellationRequested) { + return; + } + + picker.items = items; + } finally { + if (!token.isCancellationRequested) { + picker.busy = false; + } + } + }; + disposables.add(picker.onDidChangeValue(() => updatePickerItems())); + updatePickerItems(); + + // Reveal and decorate when active item changes + // However, ignore the very first event so that + // opening the picker is not immediately revealing + // and decorating the first entry. + let ignoreFirstActiveEvent = true; + disposables.add(picker.onDidChangeActive(() => { + const [item] = picker.activeItems; + if (item && item.range) { + if (ignoreFirstActiveEvent) { + ignoreFirstActiveEvent = false; + return; + } + + // Reveal + editor.revealRangeInCenter(item.range.selection, ScrollType.Smooth); + + // Decorate + this.addDecorations(editor, item.range.decoration); + } + })); + + return disposables; + } + + private async getSymbolPicks(symbolsPromise: Promise, filter: string, token: CancellationToken): Promise> { + const symbols = await symbolsPromise; + if (token.isCancellationRequested) { + return []; + } + + // Normalize filter + const filterBySymbolKind = filter.indexOf(AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX) === 0; + const filterPos = filterBySymbolKind ? 1 : 0; + const [symbolFilter, containerFilter] = filter.split(' ') as [string, string | undefined]; + const symbolFilterLow = symbolFilter.toLowerCase(); + const containerFilterLow = containerFilter?.toLowerCase(); + + // Convert to symbol picks and apply filtering + const filteredSymbolPicks: IGotoSymbolQuickPickItem[] = []; + for (let index = 0; index < symbols.length; index++) { + const symbol = symbols[index]; + + const symbolLabel = trim(symbol.name); + const containerLabel = symbol.containerName; + + let symbolScore: FuzzyScore | undefined = undefined; + let containerScore: FuzzyScore | undefined = undefined; + + let includeSymbol = true; + if (filter.length > filterPos) { + + // Score by symbol + symbolScore = fuzzyScore(symbolFilter, symbolFilterLow, filterPos, symbolLabel, symbolLabel.toLowerCase(), 0, true); + includeSymbol = !!symbolScore; + + // Score by container if specified + if (includeSymbol && containerFilter && containerFilterLow) { + if (containerLabel) { + containerScore = fuzzyScore(containerFilter, containerFilterLow, filterPos, containerLabel, containerLabel.toLowerCase(), 0, true); + } + + includeSymbol = !!containerScore; + } + } + + if (includeSymbol) { + const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`; + const deprecated = symbol.tags && symbol.tags.indexOf(SymbolTag.Deprecated) >= 0; + + filteredSymbolPicks.push({ + index, + kind: symbol.kind, + score: symbolScore, + label: symbolLabelWithIcon, + ariaLabel: localize('symbolsAriaLabel', "{0}, symbols picker", symbolLabel), + description: containerLabel, + highlights: deprecated ? undefined : { + label: createMatches(symbolScore, symbolLabelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */), + description: createMatches(containerScore) + }, + range: { + selection: Range.collapseToStart(symbol.selectionRange), + decoration: symbol.range + }, + strikethrough: deprecated, + buttons: (() => { + const openSideBySideDirection = this.options?.openSideBySideDirection(); + if (!openSideBySideDirection) { + return undefined; + } + + return [ + { + iconClass: openSideBySideDirection === 'right' ? 'codicon-split-horizontal' : 'codicon-split-vertical', + tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom") + } + ]; + })() + }); + } + } + + // Sort by score + const sortedFilteredSymbolPicks = filteredSymbolPicks.sort((symbolA, symbolB) => filterBySymbolKind ? + this.compareByKindAndScore(symbolA, symbolB) : + this.compareByScore(symbolA, symbolB) + ); + + // Add separator for types + // - @ only total number of symbols + // - @: grouped by symbol kind + let symbolPicks: Array = []; + if (filterBySymbolKind) { + let lastSymbolKind: SymbolKind | undefined = undefined; + let lastSeparator: IQuickPickSeparator | undefined = undefined; + let lastSymbolKindCounter = 0; + + function updateLastSeparatorLabel(): void { + if (lastSeparator && typeof lastSymbolKind === 'number' && lastSymbolKindCounter > 0) { + lastSeparator.label = format(NLS_SYMBOL_KIND_CACHE[lastSymbolKind] || FALLBACK_NLS_SYMBOL_KIND, lastSymbolKindCounter); + } + } + + for (const symbolPick of sortedFilteredSymbolPicks) { + + // Found new kind + if (lastSymbolKind !== symbolPick.kind) { + + // Update last separator with number of symbols we found for kind + updateLastSeparatorLabel(); + + lastSymbolKind = symbolPick.kind; + lastSymbolKindCounter = 1; + + // Add new separator for new kind + lastSeparator = { type: 'separator' }; + symbolPicks.push(lastSeparator); + } + + // Existing kind, keep counting + else { + lastSymbolKindCounter++; + } + + // Add to final result + symbolPicks.push(symbolPick); + } + + // Update last separator with number of symbols we found for kind + updateLastSeparatorLabel(); + } else { + symbolPicks = [ + { label: localize('symbols', "symbols ({0})", filteredSymbolPicks.length), type: 'separator' }, + ...sortedFilteredSymbolPicks + ]; + } + + return symbolPicks; + } + + private compareByScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number { + if (!symbolA.score && symbolB.score) { + return 1; + } else if (symbolA.score && !symbolB.score) { + return -1; + } + + if (symbolA.score && symbolB.score) { + if (symbolA.score[0] > symbolB.score[0]) { + return -1; + } else if (symbolA.score[0] < symbolB.score[0]) { + return 1; + } + } + + if (symbolA.index < symbolB.index) { + return -1; + } else if (symbolA.index > symbolB.index) { + return 1; + } + + return 0; + } + + private compareByKindAndScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number { + const kindA = NLS_SYMBOL_KIND_CACHE[symbolA.kind] || FALLBACK_NLS_SYMBOL_KIND; + const kindB = NLS_SYMBOL_KIND_CACHE[symbolB.kind] || FALLBACK_NLS_SYMBOL_KIND; + + // Sort by type first if scoped search + const result = kindA.localeCompare(kindB); + if (result === 0) { + return this.compareByScore(symbolA, symbolB); + } + + return result; + } + + private async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + const model = await OutlineModel.create(document, token); + if (token.isCancellationRequested) { + return []; + } + + const roots: DocumentSymbol[] = []; + for (const child of values(model.children)) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); + } else { + roots.push(...values(child.children).map(child => child.symbol)); + } + } + + let flatEntries: DocumentSymbol[] = []; + if (flatten) { + this.flattenDocumentSymbols(flatEntries, roots, ''); + } else { + flatEntries = roots; + } + + return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range)); + } + + private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { + for (const entry of entries) { + bucket.push({ + kind: entry.kind, + tags: entry.tags, + name: entry.name, + detail: entry.detail, + containerName: entry.containerName || overrideContainerLabel, + range: entry.range, + selectionRange: entry.selectionRange, + children: undefined, // we flatten it... + }); + + // Recurse over children + if (entry.children) { + this.flattenDocumentSymbols(bucket, entry.children, entry.name); + } + } + } +} + +// #region NLS Helpers + +const FALLBACK_NLS_SYMBOL_KIND = localize('property', "properties ({0})"); +const NLS_SYMBOL_KIND_CACHE: { [type: number]: string } = { + [SymbolKind.Method]: localize('method', "methods ({0})"), + [SymbolKind.Function]: localize('function', "functions ({0})"), + [SymbolKind.Constructor]: localize('_constructor', "constructors ({0})"), + [SymbolKind.Variable]: localize('variable', "variables ({0})"), + [SymbolKind.Class]: localize('class', "classes ({0})"), + [SymbolKind.Struct]: localize('struct', "structs ({0})"), + [SymbolKind.Event]: localize('event', "events ({0})"), + [SymbolKind.Operator]: localize('operator', "operators ({0})"), + [SymbolKind.Interface]: localize('interface', "interfaces ({0})"), + [SymbolKind.Namespace]: localize('namespace', "namespaces ({0})"), + [SymbolKind.Package]: localize('package', "packages ({0})"), + [SymbolKind.TypeParameter]: localize('typeParameter', "type parameters ({0})"), + [SymbolKind.Module]: localize('modules', "modules ({0})"), + [SymbolKind.Property]: localize('property', "properties ({0})"), + [SymbolKind.Enum]: localize('enum', "enumerations ({0})"), + [SymbolKind.EnumMember]: localize('enumMember', "enumeration members ({0})"), + [SymbolKind.String]: localize('string', "strings ({0})"), + [SymbolKind.File]: localize('file', "files ({0})"), + [SymbolKind.Array]: localize('array', "arrays ({0})"), + [SymbolKind.Number]: localize('number', "numbers ({0})"), + [SymbolKind.Boolean]: localize('boolean', "booleans ({0})"), + [SymbolKind.Object]: localize('object', "objects ({0})"), + [SymbolKind.Key]: localize('key', "keys ({0})"), + [SymbolKind.Field]: localize('field', "fields ({0})"), + [SymbolKind.Constant]: localize('constant', "constants ({0})") +}; + +//#endregion diff --git a/src/vs/editor/contrib/quickAccess/quickAccess.ts b/src/vs/editor/contrib/quickAccess/quickAccess.ts deleted file mode 100644 index 3b315010d9c..00000000000 --- a/src/vs/editor/contrib/quickAccess/quickAccess.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; -import { IEditor } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, OverviewRulerLane } from 'vs/editor/common/model'; -import { IRange } from 'vs/editor/common/core/range'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; -import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; - -interface IEditorLineDecoration { - rangeHighlightId: string; - overviewRulerDecorationId: string; -} - -/** - * A reusable quick access provider for the editor with support for adding decorations. - */ -export abstract class AbstractEditorQuickAccessProvider implements IQuickAccessProvider { - - /** - * Subclasses to provide an event when the active editor control changes. - */ - abstract readonly onDidActiveTextEditorControlChange: Event; - - /** - * Subclasses to provide the current active editor control. - */ - abstract activeTextEditorControl: IEditor | undefined; - - /** - * Subclasses to implement the quick access picker. - */ - abstract provide(picker: IQuickPick, token: CancellationToken): IDisposable; - - - //#region Decorations Utils - - private rangeHighlightDecorationId: IEditorLineDecoration | undefined = undefined; - - protected addDecorations(editor: IEditor, range: IRange): void { - editor.changeDecorations(changeAccessor => { - - // Reset old decorations if any - const deleteDecorations: string[] = []; - if (this.rangeHighlightDecorationId) { - deleteDecorations.push(this.rangeHighlightDecorationId.overviewRulerDecorationId); - deleteDecorations.push(this.rangeHighlightDecorationId.rangeHighlightId); - - this.rangeHighlightDecorationId = undefined; - } - - // Add new decorations for the range - const newDecorations: IModelDeltaDecoration[] = [ - - // highlight the entire line on the range - { - range, - options: { - className: 'rangeHighlight', - isWholeLine: true - } - }, - - // also add overview ruler highlight - { - range, - options: { - overviewRuler: { - color: themeColorFromId(overviewRulerRangeHighlight), - position: OverviewRulerLane.Full - } - } - } - ]; - - const [rangeHighlightId, overviewRulerDecorationId] = changeAccessor.deltaDecorations(deleteDecorations, newDecorations); - - this.rangeHighlightDecorationId = { rangeHighlightId, overviewRulerDecorationId }; - }); - } - - protected clearDecorations(editor: IEditor): void { - const rangeHighlightDecorationId = this.rangeHighlightDecorationId; - if (rangeHighlightDecorationId) { - editor.changeDecorations(changeAccessor => { - changeAccessor.deltaDecorations([ - rangeHighlightDecorationId.overviewRulerDecorationId, - rangeHighlightDecorationId.rangeHighlightId - ], []); - }); - - this.rangeHighlightDecorationId = undefined; - } - } - - //#endregion -} diff --git a/src/vs/editor/editor.main.ts b/src/vs/editor/editor.main.ts index 875233ba7a8..0dceb4a9e8a 100644 --- a/src/vs/editor/editor.main.ts +++ b/src/vs/editor/editor.main.ts @@ -7,11 +7,10 @@ import 'vs/editor/editor.all'; import 'vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp'; import 'vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard'; import 'vs/editor/standalone/browser/inspectTokens/inspectTokens'; -import 'vs/editor/standalone/browser/quickOpen/gotoLine'; -import 'vs/editor/standalone/browser/quickOpen/quickCommand'; -import 'vs/editor/standalone/browser/quickOpen/quickOutline'; import 'vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess'; import 'vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess'; +import 'vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess'; +import 'vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess'; import 'vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch'; import 'vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast'; diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index 57912317852..274ad8a2060 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -330,7 +330,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/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts new file mode 100644 index 00000000000..f4839b34f21 --- /dev/null +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { QuickCommandNLS } from 'vs/editor/common/standaloneStrings'; +import { ICommandQuickPick } from 'vs/platform/quickinput/browser/commandsQuickAccess'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/commandsQuickAccess'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IInstantiationService, ServicesAccessor } 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'; +import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +export class StandaloneCommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { + + protected get activeTextEditorControl(): IEditor | undefined { return withNullAsUndefined(this.codeEditorService.getFocusedCodeEditor()); } + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IKeybindingService keybindingService: IKeybindingService, + @ICommandService commandService: ICommandService, + @ITelemetryService telemetryService: ITelemetryService, + @INotificationService notificationService: INotificationService + ) { + super({ showAlias: false }, instantiationService, keybindingService, commandService, telemetryService, notificationService); + } + + protected async getCommandPicks(): Promise> { + return this.getCodeEditorCommandPicks(); + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: StandaloneCommandsQuickAccessProvider, + prefix: StandaloneCommandsQuickAccessProvider.PREFIX, + helpEntries: [{ description: QuickCommandNLS.quickCommandHelp, needsEditor: true }] +}); + +export class GotoLineAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.quickCommand', + label: QuickCommandNLS.quickCommandActionLabel, + alias: 'Command Palette', + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyCode.F1, + weight: KeybindingWeight.EditorContrib + }, + contextMenuOpts: { + group: 'z_commands', + order: 1 + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IQuickInputService).quickAccess.show(StandaloneCommandsQuickAccessProvider.PREFIX); + } +} + +registerEditorAction(GotoLineAction); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts index d513431044b..ab7735e221c 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -3,29 +3,58 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AbstractGotoLineQuickAccessProvider, GOTO_LINE_PREFIX } 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'; import { withNullAsUndefined } from 'vs/base/common/types'; import { GoToLineNLS } from 'vs/editor/common/standaloneStrings'; import { Event } from 'vs/base/common/event'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class StandaloneGotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { - readonly onDidActiveTextEditorControlChange = Event.None; + protected readonly onDidActiveTextEditorControlChange = Event.None; constructor(@ICodeEditorService private readonly editorService: ICodeEditorService) { super(); } - get activeTextEditorControl() { + protected get activeTextEditorControl() { return withNullAsUndefined(this.editorService.getFocusedCodeEditor()); } } Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ ctor: StandaloneGotoLineQuickAccessProvider, - prefix: GOTO_LINE_PREFIX, + prefix: StandaloneGotoLineQuickAccessProvider.PREFIX, helpEntries: [{ description: GoToLineNLS.gotoLineActionLabel, needsEditor: true }] }); + +export class GotoLineAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.gotoLine', + label: GoToLineNLS.gotoLineActionLabel, + alias: 'Go to Line...', + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyMod.CtrlCmd | KeyCode.KEY_G, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }, + weight: KeybindingWeight.EditorContrib + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IQuickInputService).quickAccess.show(StandaloneGotoLineQuickAccessProvider.PREFIX); + } +} + +registerEditorAction(GotoLineAction); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts new file mode 100644 index 00000000000..b5850c3f761 --- /dev/null +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; +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'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { QuickOutlineNLS } from 'vs/editor/common/standaloneStrings'; +import { Event } from 'vs/base/common/event'; +import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +export class StandaloneGotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { + + protected readonly onDidActiveTextEditorControlChange = Event.None; + + constructor(@ICodeEditorService private readonly editorService: ICodeEditorService) { + super(); + } + + protected get activeTextEditorControl() { + return withNullAsUndefined(this.editorService.getFocusedCodeEditor()); + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: StandaloneGotoSymbolQuickAccessProvider, + prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, + helpEntries: [ + { description: QuickOutlineNLS.quickOutlineActionLabel, prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, + { description: QuickOutlineNLS.quickOutlineByCategoryActionLabel, prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true } + ] +}); + +export class GotoLineAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.quickOutline', + label: QuickOutlineNLS.quickOutlineActionLabel, + alias: 'Go to Symbol...', + precondition: EditorContextKeys.hasDocumentSymbolProvider, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O, + weight: KeybindingWeight.EditorContrib + }, + contextMenuOpts: { + group: 'navigation', + order: 3 + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IQuickInputService).quickAccess.show(AbstractGotoSymbolQuickAccessProvider.PREFIX); + } +} + +registerEditorAction(GotoLineAction); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts index 94deb2ba64e..9eecd4a9a4c 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts @@ -8,8 +8,8 @@ import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/ import { QuickHelpNLS } from 'vs/editor/common/standaloneStrings'; import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; -Registry.as(Extensions.Quickaccess).defaultProvider = { +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ ctor: HelpQuickAccessProvider, prefix: '', helpEntries: [{ description: QuickHelpNLS.helpQuickAccessActionLabel, needsEditor: true }] -}; +}); diff --git a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.css b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.css deleted file mode 100644 index fdc3b7e9a24..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.css +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight, -.monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { - color: #0066BF; -} - -.vs-dark .monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight, -.vs-dark .monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { - color: #0097fb; -} - -.hc-black .monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight, -.hc-black .monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { - color: #F38518; -} \ No newline at end of file diff --git a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts deleted file mode 100644 index b5d6aea1470..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./editorQuickOpen'; -import { QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, IActionOptions, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution, ScrollType, IEditor } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { QuickOpenEditorWidget } from 'vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; - -export interface IQuickOpenControllerOpts { - inputAriaLabel: string; - getModel(value: string): QuickOpenModel; - getAutoFocus(searchValue: string): IAutoFocus; -} - -export class QuickOpenController implements IEditorContribution, IDecorator { - - public static readonly ID = 'editor.controller.quickOpenController'; - - public static get(editor: ICodeEditor): QuickOpenController { - return editor.getContribution(QuickOpenController.ID); - } - - private readonly editor: ICodeEditor; - private widget: QuickOpenEditorWidget | null = null; - private rangeHighlightDecorationId: string | null = null; - private lastKnownEditorSelection: Selection | null = null; - - constructor(editor: ICodeEditor, @IThemeService private readonly themeService: IThemeService) { - this.editor = editor; - } - - public dispose(): void { - // Dispose widget - if (this.widget) { - this.widget.destroy(); - this.widget = null; - } - } - - public run(opts: IQuickOpenControllerOpts): void { - if (this.widget) { - this.widget.destroy(); - this.widget = null; - } - - // Create goto line widget - let onClose = (canceled: boolean) => { - // Clear Highlight Decorations if present - this.clearDecorations(); - - // Restore selection if canceled - if (canceled && this.lastKnownEditorSelection) { - this.editor.setSelection(this.lastKnownEditorSelection); - this.editor.revealRangeInCenterIfOutsideViewport(this.lastKnownEditorSelection, ScrollType.Smooth); - } - - this.lastKnownEditorSelection = null; - - // Return focus to the editor if - // - focus is back on the element because no other focusable element was clicked - // - a command was picked from the picker which indicates the editor should get focused - if (document.activeElement === document.body || !canceled) { - this.editor.focus(); - } - }; - - this.widget = new QuickOpenEditorWidget( - this.editor, - () => onClose(false), - () => onClose(true), - (value: string) => { - this.widget!.setInput(opts.getModel(value), opts.getAutoFocus(value)); - }, - { - inputAriaLabel: opts.inputAriaLabel - }, - this.themeService - ); - - // Remember selection to be able to restore on cancel - if (!this.lastKnownEditorSelection) { - this.lastKnownEditorSelection = this.editor.getSelection(); - } - - // Show - this.widget.show(''); - } - - private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ - className: 'rangeHighlight', - isWholeLine: true - }); - - public decorateLine(range: Range, editor: ICodeEditor): void { - const oldDecorations: string[] = []; - if (this.rangeHighlightDecorationId) { - oldDecorations.push(this.rangeHighlightDecorationId); - this.rangeHighlightDecorationId = null; - } - - const newDecorations: IModelDeltaDecoration[] = [ - { - range: range, - options: QuickOpenController._RANGE_HIGHLIGHT_DECORATION - } - ]; - - const decorations = editor.deltaDecorations(oldDecorations, newDecorations); - this.rangeHighlightDecorationId = decorations[0]; - } - - public clearDecorations(): void { - if (this.rangeHighlightDecorationId) { - this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); - this.rangeHighlightDecorationId = null; - } - } -} - -export interface IQuickOpenOpts { - /** - * provide the quick open model for the given search value. - */ - getModel(value: string): QuickOpenModel; - - /** - * provide the quick open auto focus mode for the given search value. - */ - getAutoFocus(searchValue: string): IAutoFocus; -} - -/** - * Base class for providing quick open in the editor. - */ -export abstract class BaseEditorQuickOpenAction extends EditorAction { - - private readonly _inputAriaLabel: string; - - constructor(inputAriaLabel: string, opts: IActionOptions) { - super(opts); - this._inputAriaLabel = inputAriaLabel; - } - - protected getController(editor: ICodeEditor): QuickOpenController { - return QuickOpenController.get(editor); - } - - protected _show(controller: QuickOpenController, opts: IQuickOpenOpts): void { - controller.run({ - inputAriaLabel: this._inputAriaLabel, - getModel: (value: string): QuickOpenModel => opts.getModel(value), - getAutoFocus: (searchValue: string): IAutoFocus => opts.getAutoFocus(searchValue) - }); - } -} - -export interface IDecorator { - decorateLine(range: Range, editor: IEditor): void; - clearDecorations(): void; -} - -registerEditorContribution(QuickOpenController.ID, QuickOpenController); diff --git a/src/vs/editor/standalone/browser/quickOpen/gotoLine.css b/src/vs/editor/standalone/browser/quickOpen/gotoLine.css deleted file mode 100644 index e71a5e1dd76..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/gotoLine.css +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-quick-open-widget { - font-size: 13px; -} \ No newline at end of file diff --git a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts deleted file mode 100644 index 5274a3ae2c3..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts +++ /dev/null @@ -1,177 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./gotoLine'; -import * as strings from 'vs/base/common/strings'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ITextModel } from 'vs/editor/common/model'; -import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { GoToLineNLS } from 'vs/editor/common/standaloneStrings'; - -interface ParseResult { - position: Position; - isValid: boolean; - label: string; -} - -export class GotoLineEntry extends QuickOpenEntry { - private readonly parseResult: ParseResult; - private readonly decorator: IDecorator; - private readonly editor: IEditor; - - constructor(line: string, editor: IEditor, decorator: IDecorator) { - super(); - - this.editor = editor; - this.decorator = decorator; - this.parseResult = this.parseInput(line); - } - - private parseInput(line: string): ParseResult { - const numbers = line.split(',').map(part => parseInt(part, 10)).filter(part => !isNaN(part)); - let position: Position; - - if (numbers.length === 0) { - position = new Position(-1, -1); - } else if (numbers.length === 1) { - position = new Position(numbers[0], 1); - } else { - position = new Position(numbers[0], numbers[1]); - } - - let model: ITextModel | null; - if (isCodeEditor(this.editor)) { - model = this.editor.getModel(); - } else { - const diffModel = (this.editor).getModel(); - model = diffModel ? diffModel.modified : null; - } - - const isValid = model ? model.validatePosition(position).equals(position) : false; - let label: string; - - if (isValid) { - if (position.column && position.column > 1) { - label = strings.format(GoToLineNLS.gotoLineLabelValidLineAndColumn, position.lineNumber, position.column); - } else { - label = strings.format(GoToLineNLS.gotoLineLabelValidLine, position.lineNumber); - } - } else if (position.lineNumber < 1 || position.lineNumber > (model ? model.getLineCount() : 0)) { - label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineLimit, model ? model.getLineCount() : 0); - } else { - label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineAndColumnLimit, model ? model.getLineMaxColumn(position.lineNumber) : 0); - } - - return { - position: position, - isValid: isValid, - label: label - }; - } - - getLabel(): string { - return this.parseResult.label; - } - - getAriaLabel(): string { - const position = this.editor.getPosition(); - const currentLine = position ? position.lineNumber : 0; - return strings.format(GoToLineNLS.gotoLineAriaLabel, currentLine, this.parseResult.label); - } - - run(mode: Mode, _context: IEntryRunContext): boolean { - if (mode === Mode.OPEN) { - return this.runOpen(); - } - - return this.runPreview(); - } - - runOpen(): boolean { - - // No-op if range is not valid - if (!this.parseResult.isValid) { - return false; - } - - // Apply selection and focus - const range = this.toSelection(); - (this.editor).setSelection(range); - (this.editor).revealRangeInCenter(range, ScrollType.Smooth); - this.editor.focus(); - - return true; - } - - runPreview(): boolean { - - // No-op if range is not valid - if (!this.parseResult.isValid) { - this.decorator.clearDecorations(); - return false; - } - - // Select Line Position - const range = this.toSelection(); - this.editor.revealRangeInCenter(range, ScrollType.Smooth); - - // Decorate if possible - this.decorator.decorateLine(range, this.editor); - - return false; - } - - private toSelection(): Range { - return new Range( - this.parseResult.position.lineNumber, - this.parseResult.position.column, - this.parseResult.position.lineNumber, - this.parseResult.position.column - ); - } -} - -export class GotoLineAction extends BaseEditorQuickOpenAction { - - constructor() { - super(GoToLineNLS.gotoLineActionInput, { - id: 'editor.action.gotoLine', - label: GoToLineNLS.gotoLineActionLabel, - alias: 'Go to Line...', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_G, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }, - weight: KeybindingWeight.EditorContrib - } - }); - } - - run(accessor: ServicesAccessor, editor: ICodeEditor): void { - this._show(this.getController(editor), { - getModel: (value: string): QuickOpenModel => { - return new QuickOpenModel([new GotoLineEntry(value, editor, this.getController(editor))]); - }, - - getAutoFocus: (searchValue: string): IAutoFocus => { - return { - autoFocusFirstEntry: searchValue.length > 0 - }; - } - }); - } -} - -registerEditorAction(GotoLineAction); diff --git a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts b/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts deleted file mode 100644 index 2e49917211b..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts +++ /dev/null @@ -1,144 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as strings from 'vs/base/common/strings'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { matchesFuzzy } from 'vs/base/common/filters'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IHighlight, QuickOpenEntryGroup, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; -import { IEditor, IEditorAction } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { BaseEditorQuickOpenAction } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { QuickCommandNLS } from 'vs/editor/common/standaloneStrings'; - -export class EditorActionCommandEntry extends QuickOpenEntryGroup { - private readonly key: string; - private readonly action: IEditorAction; - private readonly editor: IEditor; - private readonly keyAriaLabel: string; - - constructor(key: string, keyAriaLabel: string, highlights: IHighlight[], action: IEditorAction, editor: IEditor) { - super(); - - this.key = key; - this.keyAriaLabel = keyAriaLabel; - this.setHighlights(highlights); - this.action = action; - this.editor = editor; - } - - public getLabel(): string { - return this.action.label; - } - - public getAriaLabel(): string { - if (this.keyAriaLabel) { - return strings.format(QuickCommandNLS.ariaLabelEntryWithKey, this.getLabel(), this.keyAriaLabel); - } - - return strings.format(QuickCommandNLS.ariaLabelEntry, this.getLabel()); - } - - public getGroupLabel(): string { - return this.key; - } - - public run(mode: Mode, context: IEntryRunContext): boolean { - if (mode === Mode.OPEN) { - - // Use a timeout to give the quick open widget a chance to close itself first - setTimeout(() => { - - // Some actions are enabled only when editor has focus - this.editor.focus(); - - try { - let promise = this.action.run() || Promise.resolve(); - promise.then(undefined, onUnexpectedError); - } catch (error) { - onUnexpectedError(error); - } - }, 50); - - return true; - } - - return false; - } -} - -export class QuickCommandAction extends BaseEditorQuickOpenAction { - - constructor() { - super(QuickCommandNLS.quickCommandActionInput, { - id: 'editor.action.quickCommand', - label: QuickCommandNLS.quickCommandActionLabel, - alias: 'Command Palette', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.focus, - primary: KeyCode.F1, - weight: KeybindingWeight.EditorContrib - }, - contextMenuOpts: { - group: 'z_commands', - order: 1 - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const keybindingService = accessor.get(IKeybindingService); - - this._show(this.getController(editor), { - getModel: (value: string): QuickOpenModel => { - return new QuickOpenModel(this._editorActionsToEntries(keybindingService, editor, value)); - }, - - getAutoFocus: (searchValue: string): IAutoFocus => { - return { - autoFocusFirstEntry: true, - autoFocusPrefixMatch: searchValue - }; - } - }); - } - - private _sort(elementA: QuickOpenEntryGroup, elementB: QuickOpenEntryGroup): number { - let elementAName = (elementA.getLabel() || '').toLowerCase(); - let elementBName = (elementB.getLabel() || '').toLowerCase(); - - return elementAName.localeCompare(elementBName); - } - - private _editorActionsToEntries(keybindingService: IKeybindingService, editor: ICodeEditor, searchValue: string): EditorActionCommandEntry[] { - let actions: IEditorAction[] = editor.getSupportedActions(); - let entries: EditorActionCommandEntry[] = []; - - for (const action of actions) { - - let keybinding = keybindingService.lookupKeybinding(action.id); - - if (action.label) { - let highlights = matchesFuzzy(searchValue, action.label); - if (highlights) { - entries.push(new EditorActionCommandEntry(keybinding ? keybinding.getLabel() || '' : '', keybinding ? keybinding.getAriaLabel() || '' : '', highlights, action, editor)); - } - } - } - - // Sort by name - entries = entries.sort(this._sort); - - return entries; - } -} - -registerEditorAction(QuickCommandAction); diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts b/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts deleted file mode 100644 index 17fcaf121bf..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget.ts +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dimension } from 'vs/base/browser/dom'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { QuickOpenWidget } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; -import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; - -export interface IQuickOpenEditorWidgetOptions { - inputAriaLabel: string; -} - -export class QuickOpenEditorWidget implements IOverlayWidget { - - private static readonly ID = 'editor.contrib.quickOpenEditorWidget'; - - private readonly codeEditor: ICodeEditor; - private readonly themeService: IThemeService; - private visible: boolean | undefined; - private quickOpenWidget: QuickOpenWidget; - private domNode: HTMLElement; - private styler: IDisposable; - - constructor(codeEditor: ICodeEditor, onOk: () => void, onCancel: () => void, onType: (value: string) => void, configuration: IQuickOpenEditorWidgetOptions, themeService: IThemeService) { - this.codeEditor = codeEditor; - this.themeService = themeService; - this.visible = false; - - this.domNode = document.createElement('div'); - - this.quickOpenWidget = new QuickOpenWidget( - this.domNode, - { - onOk: onOk, - onCancel: onCancel, - onType: onType - }, { - inputPlaceHolder: undefined, - inputAriaLabel: configuration.inputAriaLabel, - keyboardSupport: true - } - ); - this.styler = attachQuickOpenStyler(this.quickOpenWidget, this.themeService, { - pickerGroupForeground: foreground - }); - - this.quickOpenWidget.create(); - this.codeEditor.addOverlayWidget(this); - } - - setInput(model: QuickOpenModel, focus: IAutoFocus): void { - this.quickOpenWidget.setInput(model, focus); - } - - getId(): string { - return QuickOpenEditorWidget.ID; - } - - getDomNode(): HTMLElement { - return this.domNode; - } - - destroy(): void { - this.codeEditor.removeOverlayWidget(this); - this.quickOpenWidget.dispose(); - this.styler.dispose(); - } - - isVisible(): boolean { - return !!this.visible; - } - - show(value: string): void { - this.visible = true; - - const editorLayout = this.codeEditor.getLayoutInfo(); - if (editorLayout) { - this.quickOpenWidget.layout(new Dimension(editorLayout.width, editorLayout.height)); - } - - this.quickOpenWidget.show(value); - this.codeEditor.layoutOverlayWidget(this); - } - - hide(): void { - this.visible = false; - this.quickOpenWidget.hide(); - this.codeEditor.layoutOverlayWidget(this); - } - - getPosition(): IOverlayWidgetPosition | null { - if (this.visible) { - return { - preference: OverlayWidgetPositionPreference.TOP_CENTER - }; - } - - return null; - } -} diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOutline.css b/src/vs/editor/standalone/browser/quickOpen/quickOutline.css deleted file mode 100644 index 75309ce3318..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/quickOutline.css +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-quick-open-widget { - font-size: 13px; -} diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts deleted file mode 100644 index 772073939ef..00000000000 --- a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts +++ /dev/null @@ -1,325 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./quickOutline'; -import 'vs/base/browser/ui/codiconLabel/codiconLabel'; // The codicon symbol styles are defined here and must be loaded -import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded -import { CancellationToken } from 'vs/base/common/cancellation'; -import { matchesFuzzy } from 'vs/base/common/filters'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import * as strings from 'vs/base/common/strings'; -import { IHighlight, QuickOpenEntryGroup, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { ScrollType } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { DocumentSymbol, DocumentSymbolProviderRegistry, SymbolKinds } from 'vs/editor/common/modes'; -import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { QuickOutlineNLS } from 'vs/editor/common/standaloneStrings'; - -let SCOPE_PREFIX = ':'; - -export class SymbolEntry extends QuickOpenEntryGroup { - private readonly name: string; - private readonly type: string; - private readonly description: string | undefined; - private readonly range: Range; - private readonly editor: ICodeEditor; - private readonly decorator: IDecorator; - - constructor(name: string, type: string, description: string | undefined, range: Range, highlights: IHighlight[], editor: ICodeEditor, decorator: IDecorator) { - super(); - - this.name = name; - this.type = type; - this.description = description; - this.range = range; - this.setHighlights(highlights); - this.editor = editor; - this.decorator = decorator; - } - - public getLabel(): string { - return this.name; - } - - public getAriaLabel(): string { - return strings.format(QuickOutlineNLS.entryAriaLabel, this.name); - } - - public getIcon(): string { - return this.type; - } - - public getDescription(): string | undefined { - return this.description; - } - - public getType(): string { - return this.type; - } - - public getRange(): Range { - return this.range; - } - - public run(mode: Mode, context: IEntryRunContext): boolean { - if (mode === Mode.OPEN) { - return this.runOpen(context); - } - - return this.runPreview(); - } - - private runOpen(_context: IEntryRunContext): boolean { - - // Apply selection and focus - let range = this.toSelection(); - this.editor.setSelection(range); - this.editor.revealRangeInCenter(range, ScrollType.Smooth); - this.editor.focus(); - - return true; - } - - private runPreview(): boolean { - - // Select Outline Position - let range = this.toSelection(); - this.editor.revealRangeInCenter(range, ScrollType.Smooth); - - // Decorate if possible - this.decorator.decorateLine(this.range, this.editor); - - return false; - } - - private toSelection(): Range { - return new Range( - this.range.startLineNumber, - this.range.startColumn || 1, - this.range.startLineNumber, - this.range.startColumn || 1 - ); - } -} - -export class QuickOutlineAction extends BaseEditorQuickOpenAction { - - constructor() { - super(QuickOutlineNLS.quickOutlineActionInput, { - id: 'editor.action.quickOutline', - label: QuickOutlineNLS.quickOutlineActionLabel, - alias: 'Go to Symbol...', - precondition: EditorContextKeys.hasDocumentSymbolProvider, - kbOpts: { - kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O, - weight: KeybindingWeight.EditorContrib - }, - contextMenuOpts: { - group: 'navigation', - order: 3 - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor) { - if (!editor.hasModel()) { - return undefined; - } - - const model = editor.getModel(); - - if (!DocumentSymbolProviderRegistry.has(model)) { - return undefined; - } - - // Resolve outline - return getDocumentSymbols(model, true, CancellationToken.None).then((result: DocumentSymbol[]) => { - if (result.length === 0) { - return; - } - - this._run(editor, result); - }); - } - - private _run(editor: ICodeEditor, result: DocumentSymbol[]): void { - this._show(this.getController(editor), { - getModel: (value: string): QuickOpenModel => { - return new QuickOpenModel(this.toQuickOpenEntries(editor, result, value)); - }, - - getAutoFocus: (searchValue: string): IAutoFocus => { - // Remove any type pattern (:) from search value as needed - if (searchValue.indexOf(SCOPE_PREFIX) === 0) { - searchValue = searchValue.substr(SCOPE_PREFIX.length); - } - - return { - autoFocusPrefixMatch: searchValue, - autoFocusFirstEntry: !!searchValue - }; - } - }); - } - - private symbolEntry(name: string, type: string, description: string | undefined, range: IRange, highlights: IHighlight[], editor: ICodeEditor, decorator: IDecorator): SymbolEntry { - return new SymbolEntry(name, type, description, Range.lift(range), highlights, editor, decorator); - } - - private toQuickOpenEntries(editor: ICodeEditor, flattened: DocumentSymbol[], searchValue: string): SymbolEntry[] { - const controller = this.getController(editor); - - let results: SymbolEntry[] = []; - - // Convert to Entries - let normalizedSearchValue = searchValue; - if (searchValue.indexOf(SCOPE_PREFIX) === 0) { - normalizedSearchValue = normalizedSearchValue.substr(SCOPE_PREFIX.length); - } - - for (const element of flattened) { - let label = strings.trim(element.name); - - // Check for meatch - let highlights = matchesFuzzy(normalizedSearchValue, label); - if (highlights) { - - // Show parent scope as description - let description: string | undefined = undefined; - if (element.containerName) { - description = element.containerName; - } - - // Add - results.push(this.symbolEntry(label, SymbolKinds.toCssClassName(element.kind), description, element.range, highlights, editor, controller)); - } - } - - // Sort properly if actually searching - if (searchValue) { - if (searchValue.indexOf(SCOPE_PREFIX) === 0) { - results = results.sort(this.sortScoped.bind(this, searchValue.toLowerCase())); - } else { - results = results.sort(this.sortNormal.bind(this, searchValue.toLowerCase())); - } - } - - // Mark all type groups - if (results.length > 0 && searchValue.indexOf(SCOPE_PREFIX) === 0) { - let currentType: string | null = null; - let currentResult: SymbolEntry | null = null; - let typeCounter = 0; - - for (let i = 0; i < results.length; i++) { - let result = results[i]; - - // Found new type - if (currentType !== result.getType()) { - - // Update previous result with count - if (currentResult) { - currentResult.setGroupLabel(this.typeToLabel(currentType || '', typeCounter)); - } - - currentType = result.getType(); - currentResult = result; - typeCounter = 1; - - result.setShowBorder(i > 0); - } - - // Existing type, keep counting - else { - typeCounter++; - } - } - - // Update previous result with count - if (currentResult) { - currentResult.setGroupLabel(this.typeToLabel(currentType || '', typeCounter)); - } - } - - // Mark first entry as outline - else if (results.length > 0) { - results[0].setGroupLabel(strings.format(QuickOutlineNLS._symbols_, results.length)); - } - - return results; - } - - private typeToLabel(type: string, count: number): string { - switch (type) { - case 'module': return strings.format(QuickOutlineNLS._modules_, count); - case 'class': return strings.format(QuickOutlineNLS._class_, count); - case 'interface': return strings.format(QuickOutlineNLS._interface_, count); - case 'method': return strings.format(QuickOutlineNLS._method_, count); - case 'function': return strings.format(QuickOutlineNLS._function_, count); - case 'property': return strings.format(QuickOutlineNLS._property_, count); - case 'variable': return strings.format(QuickOutlineNLS._variable_, count); - case 'var': return strings.format(QuickOutlineNLS._variable2_, count); - case 'constructor': return strings.format(QuickOutlineNLS._constructor_, count); - case 'call': return strings.format(QuickOutlineNLS._call_, count); - } - - return type; - } - - private sortNormal(searchValue: string, elementA: SymbolEntry, elementB: SymbolEntry): number { - let elementAName = elementA.getLabel().toLowerCase(); - let elementBName = elementB.getLabel().toLowerCase(); - - // Compare by name - let r = elementAName.localeCompare(elementBName); - if (r !== 0) { - return r; - } - - // If name identical sort by range instead - let elementARange = elementA.getRange(); - let elementBRange = elementB.getRange(); - return elementARange.startLineNumber - elementBRange.startLineNumber; - } - - private sortScoped(searchValue: string, elementA: SymbolEntry, elementB: SymbolEntry): number { - - // Remove scope char - searchValue = searchValue.substr(SCOPE_PREFIX.length); - - // Sort by type first if scoped search - let elementAType = elementA.getType(); - let elementBType = elementB.getType(); - let r = elementAType.localeCompare(elementBType); - if (r !== 0) { - return r; - } - - // Special sort when searching in scoped mode - if (searchValue) { - let elementAName = elementA.getLabel().toLowerCase(); - let elementBName = elementB.getLabel().toLowerCase(); - - // Compare by name - let r = elementAName.localeCompare(elementBName); - if (r !== 0) { - return r; - } - } - - // Default to sort by range - let elementARange = elementA.getRange(); - let elementBRange = elementB.getRange(); - return elementARange.startLineNumber - elementBRange.startLineNumber; - } -} - -registerEditorAction(QuickOutlineAction); diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 406410b2abe..2fb24d8a345 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -138,6 +138,8 @@ class StandaloneTheme implements IStandaloneTheme { public get tokenColorMap(): string[] { return []; } + + public readonly semanticHighlighting = false; } function isBuiltinTheme(themeName: string): themeName is BuiltinTheme { diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 16b3d8c120c..fa53430b10e 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -60,6 +60,8 @@ suite('TokenizationSupport2Adapter', () => { return undefined; }, + semanticHighlighting: false, + tokenColorMap: [] }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8b4ab293ccc..08918576d0e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -74,6 +74,7 @@ declare namespace monaco { * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation * and encoding. * + * ```txt * foo://example.com:8042/over/there?name=ferret#nose * \_/ \______________/\_________/ \_________/ \__/ * | | | | | @@ -81,6 +82,7 @@ declare namespace monaco { * | _____________________|__ * / \ / \ * urn:example:animal:ferret:nose + * ``` */ export class Uri implements UriComponents { static isUri(thing: any): thing is Uri; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 6b561aaef76..4e3e1dec7a2 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 } 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'; @@ -216,9 +216,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; } } @@ -237,9 +238,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(() => { @@ -249,20 +252,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 ade8ecf75f3..f5a1a2e00bc 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; @@ -275,9 +278,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 || {}; @@ -373,7 +387,7 @@ export interface IAction2Options extends ICommandAction { /** * One or many menu items. */ - menu?: OneOrN<{ id: MenuId } & Omit & { command?: Partial> }>; + menu?: OneOrN<{ id: MenuId } & Omit>; /** * One keybinding. @@ -396,7 +410,7 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { const disposables = new DisposableStore(); const action = new ctor(); - const { f1, menu: menus, keybinding, description, ...command } = action.desc; + const { f1, menu, keybinding, description, ...command } = action.desc; // command disposables.add(CommandsRegistry.registerCommand({ @@ -406,14 +420,12 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { })); // menu - if (Array.isArray(menus)) { - for (let item of menus) { - const { command: commandOverrides, ...menu } = item; - disposables.add(MenuRegistry.appendMenuItem(item.id, { command: { ...command, ...commandOverrides }, ...menu })); + if (Array.isArray(menu)) { + for (let item of menu) { + disposables.add(MenuRegistry.appendMenuItem(item.id, { command: { ...command }, ...item })); } - } else if (menus) { - const { command: commandOverrides, ...menu } = menus; - disposables.add(MenuRegistry.appendMenuItem(menu.id, { command: { ...command, ...commandOverrides }, ...menu })); + } else if (menu) { + disposables.add(MenuRegistry.appendMenuItem(menu.id, { command: { ...command }, ...menu })); } if (f1) { disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: command })); diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 2bfa84d15eb..96987a4c911 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; + Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); } } this._onDidChange.fire(this); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 123c84684da..5df93a4f3f2 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; @@ -74,6 +73,7 @@ export interface ParsedArgs { 'disable-user-env-probe'?: boolean; 'force'?: boolean; 'force-user-env'?: boolean; + 'sync'?: 'on' | 'off'; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; @@ -167,5 +167,5 @@ export interface IEnvironmentService extends IUserHomeProvider { driverHandle?: string; driverVerbose: boolean; - galleryMachineIdResource?: URI; + serviceMachineIdResource?: URI; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index e68e0647c32..f6f818485d9 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -52,6 +52,7 @@ export const OPTIONS: OptionDescriptions> = { 'telemetry': { type: 'boolean', cat: 'o', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, 'folder-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('folderUri', "Opens a window with given folder uri(s)") }, 'file-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('fileUri', "Opens a window with given file uri(s)") }, + 'sync': { type: 'string', cat: 'o', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'builtin-extensions-dir': { type: 'string' }, @@ -80,7 +81,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/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 58b8f6ffcd1..4738addf8c7 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -255,7 +255,7 @@ export class EnvironmentService implements IEnvironmentService { get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; } @memoize - get galleryMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); } + get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); } get disableUpdates(): boolean { return !!this._args['disable-updates']; } get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index f3fb5ecca84..a0b43ec818c 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -13,7 +13,7 @@ import { IRequestService, asJson, asText } from 'vs/platform/request/common/requ import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { generateUuid, isUUID } from 'vs/base/common/uuid'; +import { generateUuid } from 'vs/base/common/uuid'; import { values } from 'vs/base/common/map'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -21,10 +21,9 @@ import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; -import { VSBuffer } from 'vs/base/common/buffer'; import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; interface IRawGalleryExtensionFile { assetType: string; @@ -341,7 +340,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, - @optional(IStorageService) private readonly storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, ) { const config = productService.extensionsGallery; this.extensionsGalleryUrl = config && config.serviceUrl; @@ -760,43 +759,15 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } } -export async function resolveMarketplaceHeaders(version: string, environmentService: IEnvironmentService, fileService: IFileService, storageService?: IStorageService): Promise<{ [key: string]: string; }> { +export async function resolveMarketplaceHeaders(version: string, environmentService: IEnvironmentService, fileService: IFileService, storageService: { + get: (key: string, scope: StorageScope) => string | undefined, + store: (key: string, value: string, scope: StorageScope) => void +}): Promise<{ [key: string]: string; }> { const headers: IHeaders = { 'X-Market-Client-Id': `VSCode ${version}`, 'User-Agent': `VSCode ${version}` }; - let uuid: string | null = null; - if (environmentService.galleryMachineIdResource) { - try { - const contents = await fileService.readFile(environmentService.galleryMachineIdResource); - const value = contents.value.toString(); - uuid = isUUID(value) ? value : null; - } catch (e) { - uuid = null; - } - - if (!uuid) { - uuid = generateUuid(); - try { - await fileService.writeFile(environmentService.galleryMachineIdResource, VSBuffer.fromString(uuid)); - } catch (error) { - //noop - } - } - } - - if (storageService) { - uuid = storageService.get('marketplace.userid', StorageScope.GLOBAL) || null; - if (!uuid) { - uuid = generateUuid(); - storageService.store('marketplace.userid', uuid, StorageScope.GLOBAL); - } - } - - if (uuid) { - headers['X-Market-User-Id'] = uuid; - } - + const uuid: string = await getServiceMachineId(environmentService, fileService, storageService); + headers['X-Market-User-Id'] = uuid; return headers; - } diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts index 07bb0a2e00f..377a92abbe9 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts @@ -19,6 +19,8 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import product from 'vs/platform/product/common/product'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IStorageService } from 'vs/platform/storage/common/storage'; suite('Extension Gallery Service', () => { const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice'); @@ -52,11 +54,12 @@ suite('Extension Gallery Service', () => { test('marketplace machine id', () => { const args = ['--user-data-dir', marketplaceHome]; const environmentService = new EnvironmentService(parseArgs(args, OPTIONS), process.execPath); + const storageService: IStorageService = new TestStorageService(); - return resolveMarketplaceHeaders(product.version, environmentService, fileService).then(headers => { + return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => { assert.ok(isUUID(headers['X-Market-User-Id'])); - return resolveMarketplaceHeaders(product.version, environmentService, fileService).then(headers2 => { + return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => { assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); }); }); diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index b0af12007e7..a5ed2443744 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -132,10 +132,12 @@ suite('Disk File Service', function () { const disposables = new DisposableStore(); // Given issues such as https://github.com/microsoft/vscode/issues/78602 - // we see random test failures when accessing the native file system. To - // diagnose further, we retry node.js file access tests up to 3 times to - // rule out any random disk issue. + // and https://github.com/microsoft/vscode/issues/92334 we see random test + // failures when accessing the native file system. To diagnose further, we + // retry node.js file access tests up to 3 times to rule out any random disk + // issue and increase the timeout. this.retries(3); + this.timeout(1000 * 10); setup(async () => { const logService = new NullLogService(); diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index daf424fa848..7b6863cd048 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { $ } from 'vs/base/browser/dom'; +import { $, EventHelper, EventLike } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -46,9 +46,12 @@ export class Link extends Disposable { .map(e => new StandardKeyboardEvent(e)) .filter(e => e.keyCode === KeyCode.Enter) .event; - const onOpen = Event.any(onClick, onEnterPress); + const onOpen = Event.any(onClick, onEnterPress); - this._register(onOpen(_ => openerService.open(link.href))); + this._register(onOpen(e => { + EventHelper.stop(e, true); + openerService.open(link.href); + })); this.applyStyles(); } diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts new file mode 100644 index 00000000000..d3e1a048da2 --- /dev/null +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { PickerQuickAccessProvider, IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +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 commandsHistory = this._register(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; + } + + /** + * Subclasses to provide the actual command entries. + */ + protected abstract getCommandPicks(disposables: DisposableStore, token: CancellationToken): Promise>; +} + +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/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts index d4a66d3556b..22e0879e475 100644 --- a/src/vs/platform/quickinput/browser/helpQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -6,7 +6,6 @@ import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -16,11 +15,13 @@ interface IHelpQuickAccessPickItem extends IQuickPickItem { export class HelpQuickAccessProvider implements IQuickAccessProvider { + static PREFIX = '?'; + private readonly registry = Registry.as(Extensions.Quickaccess); constructor(@IQuickInputService private readonly quickInputService: IQuickInputService) { } - provide(picker: IQuickPick, token: CancellationToken): IDisposable { + provide(picker: IQuickPick): IDisposable { const disposables = new DisposableStore(); // Open a picker with the selected value if picked @@ -31,6 +32,15 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { } })); + // Also open a picker when we detect the user typed the exact + // name of a provider (e.g. `?term` for terminals) + disposables.add(picker.onDidChangeValue(value => { + const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length)); + if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) { + this.quickInputService.quickAccess.show(providerDescriptor.prefix); + } + })); + // Fill in all providers separated by editor/global scope const { editorProviders, globalProviders } = this.getQuickAccessProviders(); picker.items = editorProviders.length === 0 || globalProviders.length === 0 ? @@ -55,7 +65,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { const globalProviders: IHelpQuickAccessPickItem[] = []; const editorProviders: IHelpQuickAccessPickItem[] = []; - for (const provider of this.registry.getQuickAccessProviders().sort((p1, p2) => p1.prefix.localeCompare(p2.prefix))) { + for (const provider of this.registry.getQuickAccessProviders().sort((providerA, providerB) => providerA.prefix.localeCompare(providerB.prefix))) { for (const helpEntry of provider.helpEntries) { const prefix = helpEntry.prefix || provider.prefix; const label = prefix || '\u2026' /* ... */; @@ -63,8 +73,8 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { (helpEntry.needsEditor ? editorProviders : globalProviders).push({ prefix, label, - description: helpEntry.description, - ariaLabel: localize('entryAriaLabel', "{0}, picker help", label) + ariaLabel: localize('entryAriaLabel', "{0}, quick access help picker", label), + description: helpEntry.description }); } } diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts new file mode 100644 index 00000000000..fca14db0d98 --- /dev/null +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; + +export enum TriggerAction { + + /** + * Do nothing after the button was clicked. + */ + NO_ACTION, + + /** + * Close the picker. + */ + CLOSE_PICKER, + + /** + * Update the results of the picker. + */ + REFRESH_PICKER +} + +export interface IPickerQuickAccessItem extends IQuickPickItem { + + /** + * A method that will be executed when the pick item is accepted from + * the picker. The picker will close automatically before running this. + * + * @param keyMods the state of modifier keys when the item was accepted. + * @param event the underlying event that caused the accept to trigger. + */ + accept?(keyMods: IKeyMods, event: IQuickPickAcceptEvent): void; + + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + +export abstract class PickerQuickAccessProvider extends Disposable implements IQuickAccessProvider { + + constructor(private prefix: string) { + super(); + } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Allow subclasses to configure picker + this.configure(picker); + + // Disable filtering & sorting, we control the results + picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + + // Set initial picks and update on type + let picksCts: CancellationTokenSource | undefined = undefined; + const updatePickerItems = async () => { + + // Cancel any previous ask for picks and busy + picksCts?.dispose(true); + picker.busy = false; + + // Create new cancellation source for this run + picksCts = new CancellationTokenSource(token); + + // Collect picks and support both long running and short + const res = this.getPicks(picker.value.substr(this.prefix.length).trim(), disposables.add(new DisposableStore()), picksCts.token); + if (Array.isArray(res)) { + picker.items = res; + } else { + picker.busy = true; + try { + const items = await res; + if (token.isCancellationRequested) { + return; + } + + picker.items = items; + } finally { + if (!token.isCancellationRequested) { + picker.busy = false; + } + } + } + }; + disposables.add(picker.onDidChangeValue(() => updatePickerItems())); + updatePickerItems(); + + // Accept the pick on accept and hide picker + disposables.add(picker.onDidAccept(event => { + const [item] = picker.selectedItems; + if (typeof item?.accept === 'function') { + if (!event.inBackground) { + picker.hide(); // hide picker unless we accept in background + } + item.accept(picker.keyMods, event); + } + })); + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(async ({ button, item }) => { + if (typeof item.trigger === 'function') { + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; + + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + } + } + } + })); + + return disposables; + } + + /** + * Subclasses can override this method to configure the picker before showing it. + * + * @param picker the picker instance used for the quick access before it opens. + */ + protected configure(picker: IQuickPick): void { } + + /** + * Returns an array of picks and separators as needed. If the picks are resolved + * long running, the provided cancellation token should be used to cancel the + * operation when the token signals this. + * + * The implementor is responsible for filtering and sorting the picks given the + * provided `filter`. + * + * @param filter a filter to apply to the picks. + * @param disposables can be used to register disposables that should be cleaned + * up when the picker closes. + * @param token for long running tasks, implementors need to check on cancellation + * through this token. + */ + protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise>; +} diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 377db89fbd3..ca96d93fdbe 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -37,11 +37,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Create a picker for the provider to use with the initial value // and adjust the filtering to exclude the prefix from filtering const picker = disposables.add(this.quickInputService.createQuickPick()); - picker.placeholder = descriptor.placeholder; + picker.placeholder = descriptor?.placeholder; picker.value = value; picker.valueSelection = [value.length, value.length]; - picker.contextKey = descriptor.contextKey; - picker.filterValue = (value: string) => value.substring(descriptor.prefix.length); + picker.contextKey = descriptor?.contextKey; + picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); // Remember as last active picker and clean up once picker get's disposed this.lastActivePicker = picker; @@ -72,8 +72,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } })); - // Ask provider to fill the picker as needed - disposables.add(provider.provide(picker, cts.token)); + // Ask provider to fill the picker as needed if we have one + if (provider) { + disposables.add(provider.provide(picker, cts.token)); + } // Finally, show the picker. This is important because a provider // may not call this and then our disposables would leak that rely @@ -81,8 +83,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon picker.show(); } - private getOrInstantiateProvider(value: string): [IQuickAccessProvider, IQuickAccessProviderDescriptor] { - const providerDescriptor = this.registry.getQuickAccessProvider(value) || this.registry.defaultProvider; + private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { + const providerDescriptor = this.registry.getQuickAccessProvider(value); + if (!providerDescriptor) { + return [undefined, undefined]; + } let provider = this.mapProviderToDescriptor.get(providerDescriptor); if (!provider) { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index be8fba3f311..9da4698d02b 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -6,9 +6,8 @@ import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { first } from 'vs/base/common/arrays'; +import { first, coalesce } from 'vs/base/common/arrays'; import { startsWith } from 'vs/base/common/strings'; -import { assertIsDefined } from 'vs/base/common/types'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IQuickAccessController { @@ -92,11 +91,6 @@ export const Extensions = { export interface IQuickAccessRegistry { - /** - * The default provider to use when no other provider matches. - */ - defaultProvider: IQuickAccessProviderDescriptor; - /** * Registers a quick access provider to the platform. */ @@ -113,29 +107,53 @@ export interface IQuickAccessRegistry { getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined; } -class QuickAccessRegistry implements IQuickAccessRegistry { +export class QuickAccessRegistry implements IQuickAccessRegistry { private providers: IQuickAccessProviderDescriptor[] = []; - - private _defaultProvider: IQuickAccessProviderDescriptor | undefined = undefined; - get defaultProvider(): IQuickAccessProviderDescriptor { return assertIsDefined(this._defaultProvider); } - set defaultProvider(provider: IQuickAccessProviderDescriptor) { this._defaultProvider = provider; } + private defaultProvider: IQuickAccessProviderDescriptor | undefined = undefined; registerQuickAccessProvider(provider: IQuickAccessProviderDescriptor): IDisposable { - this.providers.push(provider); + + // Extract the default provider when no prefix is present + if (provider.prefix.length === 0) { + this.defaultProvider = provider; + } else { + this.providers.push(provider); + } // sort the providers by decreasing prefix length, such that longer // prefixes take priority: 'ext' vs 'ext install' - the latter should win this.providers.sort((providerA, providerB) => providerB.prefix.length - providerA.prefix.length); - return toDisposable(() => this.providers.splice(this.providers.indexOf(provider), 1)); + return toDisposable(() => { + this.providers.splice(this.providers.indexOf(provider), 1); + + if (this.defaultProvider === provider) { + this.defaultProvider = undefined; + } + }); } getQuickAccessProviders(): IQuickAccessProviderDescriptor[] { - return [this.defaultProvider, ...this.providers]; + return coalesce([this.defaultProvider, ...this.providers]); } getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined { - return prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined; + const result = prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined; + + return result || this.defaultProvider; + } + + clear(): Function { + const providers = [...this.providers]; + const defaultProvider = this.defaultProvider; + + this.providers = []; + this.defaultProvider = undefined; + + return () => { + this.providers = providers; + this.defaultProvider = defaultProvider; + }; } } diff --git a/src/vs/platform/resource/common/resourceIdentityService.ts b/src/vs/platform/resource/common/resourceIdentityService.ts new file mode 100644 index 00000000000..d81e3dc8e2f --- /dev/null +++ b/src/vs/platform/resource/common/resourceIdentityService.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { 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 00000000000..71cb292b6ec --- /dev/null +++ b/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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/platform/serviceMachineId/common/serviceMachineId.ts b/src/vs/platform/serviceMachineId/common/serviceMachineId.ts new file mode 100644 index 00000000000..04957244a2a --- /dev/null +++ b/src/vs/platform/serviceMachineId/common/serviceMachineId.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IFileService } from 'vs/platform/files/common/files'; +import { StorageScope } from 'vs/platform/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { isUUID, generateUuid } from 'vs/base/common/uuid'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export async function getServiceMachineId(environmentService: IEnvironmentService, fileService: IFileService, storageService: { + get: (key: string, scope: StorageScope, fallbackValue?: string | undefined) => string | undefined, + store: (key: string, value: string, scope: StorageScope) => void +}): Promise { + let uuid: string | null = storageService.get('storage.serviceMachineId', StorageScope.GLOBAL) || null; + if (uuid) { + return uuid; + } + if (environmentService.serviceMachineIdResource) { + try { + const contents = await fileService.readFile(environmentService.serviceMachineIdResource); + const value = contents.value.toString(); + uuid = isUUID(value) ? value : null; + } catch (e) { + uuid = null; + } + + if (!uuid) { + uuid = generateUuid(); + try { + await fileService.writeFile(environmentService.serviceMachineIdResource, VSBuffer.fromString(uuid)); + } catch (error) { + //noop + } + } + } else { + uuid = generateUuid(); + } + storageService.store('storage.serviceMachineId', uuid, StorageScope.GLOBAL); + return uuid; +} diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 35599b40d05..54575132334 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -365,6 +365,7 @@ export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget. export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hc: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hc: contrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hc: '#a9a9a9' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); +export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', { dark: '#8C8C8C', light: '#8E8E90', hc: '#A7A8A9' }, nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized. ")); /** * Menu colors diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 6a1fb3a6966..bca0e9bc03f 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -112,6 +112,11 @@ export interface IColorTheme { * List of all colors used with tokens. getTokenStyleMetadata references the colors by index into this list. */ readonly tokenColorMap: string[]; + + /** + * Defines whether semantic highlighting should be enabled for the theme. + */ + readonly semanticHighlighting: boolean; } export interface IFileIconTheme { diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 2b3bebbb553..8911630ed91 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -433,6 +433,7 @@ function registerDefaultClassifications(): void { registerTokenStyleDefault('variable.readonly', [['variable.other.constant']]); + registerTokenStyleDefault('property.readonly', [['variable.other.constant.property']]); } export function getTokenClassificationRegistry(): ITokenClassificationRegistry { diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index c50b168605c..acada67c302 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -28,6 +28,8 @@ export class TestColorTheme implements IColorTheme { return undefined; } + readonly semanticHighlighting = false; + get tokenColorMap(): string[] { return []; } diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 8f79415fe07..2b6180c9ab2 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, ResourceKey, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname } from 'vs/base/common/resources'; import { CancelablePromise } from 'vs/base/common/async'; @@ -19,6 +19,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isString } from 'vs/base/common/types'; +import { uppercaseFirstLetter } from 'vs/base/common/strings'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -54,10 +55,10 @@ export abstract class AbstractSynchroniser extends Disposable { readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; protected readonly lastSyncResource: URI; + protected readonly syncResourceLogLabel: string; constructor( - readonly source: SyncSource, - readonly resourceKey: ResourceKey, + readonly resource: SyncResource, @IFileService protected readonly fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, @@ -68,8 +69,9 @@ export abstract class AbstractSynchroniser extends Disposable { @IConfigurationService protected readonly configurationService: IConfigurationService, ) { super(); - this.syncFolder = joinPath(environmentService.userDataSyncHome, source); - this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resourceKey}.json`); + this.syncResourceLogLabel = uppercaseFirstLetter(this.resource); + this.syncFolder = joinPath(environmentService.userDataSyncHome, resource); + this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`); } protected setStatus(status: SyncStatus): void { @@ -79,32 +81,32 @@ export abstract class AbstractSynchroniser extends Disposable { this._onDidChangStatus.fire(status); if (status === SyncStatus.HasConflicts) { // Log to telemetry when there is a sync conflict - this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.source }); + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource }); } if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) { // Log to telemetry when conflicts are resolved - this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.source }); + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource }); } } } - protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); } + protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); } async sync(ref?: string): Promise { if (!this.isEnabled()) { - this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is disabled.`); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`); return; } if (this.status === SyncStatus.HasConflicts) { - this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as there are conflicts.`); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`); return; } if (this.status === SyncStatus.Syncing) { - this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is running already.`); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`); return; } - this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`); + this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); @@ -114,9 +116,9 @@ export abstract class AbstractSynchroniser extends Disposable { try { status = await this.doSync(remoteUserData, lastSyncUserData); if (status === SyncStatus.HasConflicts) { - this.logService.info(`${this.source}: Detected conflicts while synchronizing ${this.source.toLowerCase()}.`); + this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`); } else if (status === SyncStatus.Idle) { - this.logService.trace(`${this.source}: Finished synchronizing ${this.source.toLowerCase()}.`); + this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`); } } finally { this.setStatus(status); @@ -126,8 +128,8 @@ export abstract class AbstractSynchroniser extends Disposable { protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version - this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.source }); - throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.source, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.source); + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource }); + throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource); } try { const status = await this.performSync(remoteUserData, lastSyncUserData); @@ -137,7 +139,7 @@ export abstract class AbstractSynchroniser extends Disposable { switch (e.code) { case UserDataSyncErrorCode.RemotePreconditionFailed: // Rejected as there is a new remote version. Syncing again, - this.logService.info(`${this.source}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); + this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); // Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624 remoteUserData = await this.getRemoteUserData(null); return this.doSync(remoteUserData, lastSyncUserData); @@ -163,7 +165,7 @@ export abstract class AbstractSynchroniser extends Disposable { } async getLocalBackupContent(ref?: string): Promise { - return this.userDataSyncBackupStoreService.resolveContent(this.resourceKey, ref); + return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref); } async resetLocal(): Promise { @@ -225,23 +227,23 @@ export abstract class AbstractSynchroniser extends Disposable { private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { if (isString(refOrLastSyncData)) { - const content = await this.userDataSyncStoreService.resolveContent(this.resourceKey, refOrLastSyncData); + const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData); return { ref: refOrLastSyncData, content }; } else { const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null; - return this.userDataSyncStoreService.read(this.resourceKey, lastSyncUserData, this.source); + return this.userDataSyncStoreService.read(this.resource, lastSyncUserData); } } protected async updateRemoteUserData(content: string, ref: string | null): Promise { const syncData: ISyncData = { version: this.version, content }; - ref = await this.userDataSyncStoreService.write(this.resourceKey, JSON.stringify(syncData), ref, this.source); + ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref); return { ref, syncData }; } protected async backupLocal(content: string): Promise { const syncData: ISyncData = { version: this.version, content }; - return this.userDataSyncBackupStoreService.backup(this.resourceKey, JSON.stringify(syncData)); + return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData)); } protected abstract readonly version: number; @@ -264,8 +266,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { constructor( protected readonly file: URI, - source: SyncSource, - resourceKey: ResourceKey, + resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @@ -275,14 +276,14 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { @IUserDataSyncLogService logService: IUserDataSyncLogService, @IConfigurationService configurationService: IConfigurationService, ) { - super(source, resourceKey, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(file))); this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } async stop(): Promise { this.cancel(); - this.logService.trace(`${this.source}: Stopped synchronizing ${this.source.toLowerCase()}.`); + this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); try { await this.fileService.del(this.conflictsPreviewResource); } catch (e) { /* ignore */ } @@ -362,8 +363,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni constructor( file: URI, - source: SyncSource, - resourceKey: ResourceKey, + resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @@ -374,7 +374,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService configurationService: IConfigurationService, ) { - super(file, source, resourceKey, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(file, resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); } protected hasErrors(content: string): boolean { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index cdc3d6faab5..ad91ac82436 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -50,7 +50,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(SyncSource.Extensions, 'extensions', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncResource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register( Event.debounce( Event.any( @@ -62,14 +62,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async pull(): Promise { if (!this.isEnabled()) { - this.logService.info('Extensions: Skipped pulling extensions as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling extensions as it is disabled.`); return; } this.stop(); try { - this.logService.info('Extensions: Started pulling extensions...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pulling extensions...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); @@ -84,10 +84,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // No remote exists to pull else { - this.logService.info('Extensions: Remote extensions does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Remote extensions does not exist.`); } - this.logService.info('Extensions: Finished pulling extensions.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling extensions.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -95,14 +95,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async push(): Promise { if (!this.isEnabled()) { - this.logService.info('Extensions: Skipped pushing extensions as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing extensions as it is disabled.`); return; } this.stop(); try { - this.logService.info('Extensions: Started pushing extensions...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`); this.setStatus(SyncStatus.Syncing); const localExtensions = await this.getLocalExtensions(); @@ -111,7 +111,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const remoteUserData = await this.getRemoteUserData(lastSyncUserData); await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData }, true); - this.logService.info('Extensions: Finished pushing extensions.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -148,7 +148,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } accept(content: string): Promise { - throw new Error('Extensions: Conflicts should not occur'); + throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } async hasLocalData(): Promise { @@ -177,9 +177,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const localExtensions = await this.getLocalExtensions(); if (remoteExtensions) { - this.logService.trace('Extensions: Merging remote extensions with local extensions...'); + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); } else { - this.logService.trace('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); + this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`); } const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions()); @@ -196,7 +196,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const hasChanges = added.length || removed.length || updated.length || remote; if (!hasChanges) { - this.logService.info('Extensions: No changes found during synchronizing extensions.'); + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`); } if (added.length || removed.length || updated.length) { @@ -208,17 +208,17 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (remote) { // update remote - this.logService.trace('Extensions: Updating remote extensions...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`); const content = JSON.stringify(remote); remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); - this.logService.info('Extensions: Updated remote extensions'); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`); } if (lastSyncUserData?.ref !== remoteUserData.ref) { // update last sync - this.logService.trace('Extensions: Updating last synchronized extensions...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`); await this.updateLastSyncUserData(remoteUserData, { skippedExtensions }); - this.logService.info('Extensions: Updated last synchronized extensions'); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`); } } @@ -230,9 +230,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r))); await Promise.all(extensionsToRemove.map(async extensionToRemove => { - this.logService.trace('Extensions: Uninstalling local extension...', extensionToRemove.identifier.id); + this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...', extensionToRemove.identifier.i`); await this.extensionManagementService.uninstall(extensionToRemove); - this.logService.info('Extensions: Uninstalled local extension.', extensionToRemove.identifier.id); + this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.', extensionToRemove.identifier.i`); removeFromSkipped.push(extensionToRemove.identifier); })); } @@ -245,13 +245,13 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // Builtin Extension: Sync only enablement state if (installedExtension && installedExtension.type === ExtensionType.System) { if (e.disabled) { - this.logService.trace('Extensions: Disabling extension...', e.identifier.id); + this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.i`); await this.extensionEnablementService.disableExtension(e.identifier); - this.logService.info('Extensions: Disabled extension', e.identifier.id); + this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.i`); } else { - this.logService.trace('Extensions: Enabling extension...', e.identifier.id); + this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.i`); await this.extensionEnablementService.enableExtension(e.identifier); - this.logService.info('Extensions: Enabled extension', e.identifier.id); + this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.i`); } removeFromSkipped.push(e.identifier); return; @@ -261,19 +261,19 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (extension) { try { if (e.disabled) { - this.logService.trace('Extensions: Disabling extension...', e.identifier.id, extension.version); + this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.id, extension.versio`); await this.extensionEnablementService.disableExtension(extension.identifier); - this.logService.info('Extensions: Disabled extension', e.identifier.id, extension.version); + this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.id, extension.versio`); } else { - this.logService.trace('Extensions: Enabling extension...', e.identifier.id, extension.version); + this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.id, extension.versio`); await this.extensionEnablementService.enableExtension(extension.identifier); - this.logService.info('Extensions: Enabled extension', e.identifier.id, extension.version); + this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.id, extension.versio`); } // Install only if the extension does not exist if (!installedExtension || installedExtension.manifest.version !== extension.version) { - this.logService.trace('Extensions: Installing extension...', e.identifier.id, extension.version); + this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...', e.identifier.id, extension.versio`); await this.extensionManagementService.installFromGallery(extension); - this.logService.info('Extensions: Installed extension.', e.identifier.id, extension.version); + this.logService.info(`${this.syncResourceLogLabel}: Installed extension.', e.identifier.id, extension.versio`); removeFromSkipped.push(extension.identifier); } } catch (error) { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index a095a09b482..0be9b802764 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -41,21 +41,21 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, ) { - super(SyncSource.GlobalState, 'globalState', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); } async pull(): Promise { if (!this.isEnabled()) { - this.logService.info('UI State: Skipped pulling ui state as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ui state as it is disabled.`); return; } this.stop(); try { - this.logService.info('UI State: Started pulling ui state...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); @@ -69,10 +69,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs // No remote exists to pull else { - this.logService.info('UI State: Remote UI state does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Remote UI state does not exist.`); } - this.logService.info('UI State: Finished pulling UI state.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling UI state.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -80,14 +80,14 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async push(): Promise { if (!this.isEnabled()) { - this.logService.info('UI State: Skipped pushing UI State as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing UI State as it is disabled.`); return; } this.stop(); try { - this.logService.info('UI State: Started pushing UI State...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pushing UI State...`); this.setStatus(SyncStatus.Syncing); const localUserData = await this.getLocalGlobalState(); @@ -95,7 +95,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const remoteUserData = await this.getRemoteUserData(lastSyncUserData); await this.apply({ local: undefined, remote: localUserData, remoteUserData, localUserData, lastSyncUserData }, true); - this.logService.info('UI State: Finished pushing UI State.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -132,7 +132,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } accept(content: string): Promise { - throw new Error('UI State: Conflicts should not occur'); + throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } async hasLocalData(): Promise { @@ -160,9 +160,9 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const localGloablState = await this.getLocalGlobalState(); if (remoteGlobalState) { - this.logService.trace('UI State: Merging remote ui state with local ui state...'); + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`); } else { - this.logService.trace('UI State: Remote ui state does not exist. Synchronizing ui state for the first time.'); + this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`); } const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState); @@ -175,30 +175,30 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const hasChanges = local || remote; if (!hasChanges) { - this.logService.info('UI State: No changes found during synchronizing ui state.'); + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); } if (local) { // update local - this.logService.trace('UI State: Updating local ui state...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`); await this.backupLocal(JSON.stringify(localUserData)); await this.writeLocalGlobalState(local); - this.logService.info('UI State: Updated local ui state'); + this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`); } if (remote) { // update remote - this.logService.trace('UI State: Updating remote ui state...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); const content = JSON.stringify(remote); remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); - this.logService.info('UI State: Updated remote ui state'); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`); } if (lastSyncUserData?.ref !== remoteUserData.ref) { // update last sync - this.logService.trace('UI State: Updating last synchronized ui state...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`); await this.updateLastSyncUserData(remoteUserData); - this.logService.info('UI State: Updated last synchronized ui state'); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`); } } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index c3f02bac0da..c5248fe5a8b 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; @@ -43,19 +43,19 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(environmentService.keybindingsResource, SyncSource.Keybindings, 'keybindings', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } async pull(): Promise { if (!this.isEnabled()) { - this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling keybindings as it is disabled.`); return; } this.stop(); try { - this.logService.info('Keybindings: Started pulling keybindings...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pulling keybindings...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); @@ -78,10 +78,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // No remote exists to pull else { - this.logService.info('Keybindings: Remote keybindings does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Remote keybindings does not exist.`); } - this.logService.info('Keybindings: Finished pulling keybindings.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling keybindings.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -90,14 +90,14 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem async push(): Promise { if (!this.isEnabled()) { - this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing keybindings as it is disabled.`); return; } this.stop(); try { - this.logService.info('Keybindings: Started pushing keybindings...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pushing keybindings...`); this.setStatus(SyncStatus.Syncing); const fileContent = await this.getLocalFileContent(); @@ -119,10 +119,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // No local exists to push else { - this.logService.info('Keybindings: Local keybindings does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Local keybindings does not exist.`); } - this.logService.info('Keybindings: Finished pushing keybindings.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing keybindings.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -202,7 +202,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem switch (e.code) { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. - this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...'); + this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...`); return this.performSync(remoteUserData, lastSyncUserData); } } @@ -219,21 +219,21 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (content !== null) { if (this.hasErrors(content)) { - throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source); + throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } if (hasLocalChanged) { - this.logService.trace('Keybindings: Updating local keybindings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); await this.backupLocal(this.toSyncContent(content, null)); await this.updateLocalFileContent(content, fileContent); - this.logService.info('Keybindings: Updated local keybindings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`); } if (hasRemoteChanged) { - this.logService.trace('Keybindings: Updating remote keybindings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`); const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref); - this.logService.info('Keybindings: Updated remote keybindings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`); } // Delete the preview @@ -241,14 +241,14 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem await this.fileService.del(this.conflictsPreviewResource); } catch (e) { /* ignore */ } } else { - this.logService.info('Keybindings: No changes found during synchronizing keybindings.'); + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); } if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { - this.logService.trace('Keybindings: Updating last synchronized keybindings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null); await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); - this.logService.info('Keybindings: Updated last synchronized keybindings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } this.syncPreviewResultPromise = null; @@ -276,14 +276,14 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (remoteContent) { const localContent: string = fileContent ? fileContent.value.toString() : '[]'; if (this.hasErrors(localContent)) { - throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source); + throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } if (!lastSyncContent // First time sync || lastSyncContent !== localContent // Local has forwarded || lastSyncContent !== remoteContent // Remote has forwarded ) { - this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote keybindings with local keybindings...`); const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService); // Sync only if there are changes if (result.hasChanges) { @@ -297,7 +297,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem // First time syncing to remote else if (fileContent) { - this.logService.trace('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.'); + this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`); content = fileContent.value.toString(); hasRemoteChanged = true; } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 219f2c2a698..6704fa622c3 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -33,7 +33,7 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent { && Object.keys(thing).length === 1; } -export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements ISettingsSyncService { +export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { _serviceBrand: any; @@ -57,7 +57,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @ITelemetryService telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, ) { - super(environmentService.settingsResource, SyncSource.Settings, 'settings', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } protected setStatus(status: SyncStatus): void { @@ -78,14 +78,14 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement async pull(): Promise { if (!this.isEnabled()) { - this.logService.info('Settings: Skipped pulling settings as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling settings as it is disabled.`); return; } this.stop(); try { - this.logService.info('Settings: Started pulling settings...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pulling settings...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); @@ -113,10 +113,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement // No remote exists to pull else { - this.logService.info('Settings: Remote settings does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Remote settings does not exist.`); } - this.logService.info('Settings: Finished pulling settings.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling settings.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -124,14 +124,14 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement async push(): Promise { if (!this.isEnabled()) { - this.logService.info('Settings: Skipped pushing settings as it is disabled.'); + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing settings as it is disabled.`); return; } this.stop(); try { - this.logService.info('Settings: Started pushing settings...'); + this.logService.info(`${this.syncResourceLogLabel}: Started pushing settings...`); this.setStatus(SyncStatus.Syncing); const fileContent = await this.getLocalFileContent(); @@ -159,10 +159,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement // No local exists to push else { - this.logService.info('Settings: Local settings does not exist.'); + this.logService.info(`${this.syncResourceLogLabel}: Local settings does not exist.`); } - this.logService.info('Settings: Finished pushing settings.'); + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing settings.`); } finally { this.setStatus(SyncStatus.Idle); } @@ -268,7 +268,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement switch (e.code) { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. - this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...'); + this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize settings as there is a new local version available. Synchronizing again...`); return this.performSync(remoteUserData, lastSyncUserData, resolvedConflicts); } } @@ -288,10 +288,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement this.validateContent(content); if (hasLocalChanged) { - this.logService.trace('Settings: Updating local settings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`); await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(content))); await this.updateLocalFileContent(content, fileContent); - this.logService.info('Settings: Updated local settings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`); } if (hasRemoteChanged) { const formatUtils = await this.getFormattingOptions(); @@ -299,9 +299,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); const ignoredSettings = await this.getIgnoredSettings(content); content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils); - this.logService.trace('Settings: Updating remote settings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`); remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref); - this.logService.info('Settings: Updated remote settings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`); } // Delete the preview @@ -309,13 +309,13 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement await this.fileService.del(this.conflictsPreviewResource); } catch (e) { /* ignore */ } } else { - this.logService.info('Settings: No changes found during synchronizing settings.'); + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`); } if (lastSyncUserData?.ref !== remoteUserData.ref) { - this.logService.trace('Settings: Updating last synchronized settings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`); await this.updateLastSyncUserData(remoteUserData); - this.logService.info('Settings: Updated last synchronized settings'); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized settings`); } this.syncPreviewResultPromise = null; @@ -343,7 +343,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (remoteSettingsSyncContent) { const localContent: string = fileContent ? fileContent.value.toString() : '{}'; this.validateContent(localContent); - this.logService.trace('Settings: Merging remote settings with local settings...'); + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`); const ignoredSettings = await this.getIgnoredSettings(); const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, resolvedConflicts, formattingOptions); content = result.localContent || result.remoteContent; @@ -355,7 +355,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement // First time syncing to remote else if (fileContent) { - this.logService.trace('Settings: Remote settings does not exist. Synchronizing settings for the first time.'); + this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`); content = fileContent.value.toString(); hasRemoteChanged = true; } @@ -406,7 +406,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement private validateContent(content: string): void { if (this.hasErrors(content)) { - throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source); + throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 0744514d2f5..260ca1554d8 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -135,11 +135,16 @@ export function getUserDataSyncStore(productService: IProductService, configurat return undefined; } -export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState']; -export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState'; +export const enum SyncResource { + Settings = 'settings', + Keybindings = 'keybindings', + Extensions = 'extensions', + GlobalState = 'globalState' +} +export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Extensions, SyncResource.GlobalState]; export interface IUserDataManifest { - latest?: Record + latest?: Record session: string; } @@ -152,21 +157,21 @@ export const IUserDataSyncStoreService = createDecorator; - write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise; + read(resource: SyncResource, oldValue: IUserData | null): Promise; + write(resource: SyncResource, content: string, ref: string | null): Promise; manifest(): Promise; clear(): Promise; - getAllRefs(key: ResourceKey): Promise; - resolveContent(key: ResourceKey, ref: string): Promise; - delete(key: ResourceKey): Promise; + getAllRefs(resource: SyncResource): Promise; + resolveContent(resource: SyncResource, ref: string): Promise; + delete(resource: SyncResource): Promise; } export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); export interface IUserDataSyncBackupStoreService { _serviceBrand: undefined; - backup(resourceKey: ResourceKey, content: string): Promise; - getAllRefs(key: ResourceKey): Promise; - resolveContent(key: ResourceKey, ref?: string): Promise; + backup(resource: SyncResource, content: string): Promise; + getAllRefs(resource: SyncResource): Promise; + resolveContent(resource: SyncResource, ref?: string): Promise; } //#endregion @@ -195,9 +200,9 @@ export enum UserDataSyncErrorCode { export class UserDataSyncError extends Error { - constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) { + constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly resource?: SyncResource) { super(message); - this.name = `${this.code} (UserDataSyncError) ${this.source}`; + this.name = `${this.code} (UserDataSyncError) ${this.resource}`; } static toUserDataSyncError(error: Error): UserDataSyncError { @@ -206,7 +211,7 @@ export class UserDataSyncError extends Error { } const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name); if (match && match[1]) { - return new UserDataSyncError(error.message, match[1], match[2]); + return new UserDataSyncError(error.message, match[1], match[2]); } return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown); } @@ -230,13 +235,6 @@ export interface IGlobalState { storage: IStringDictionary; } -export const enum SyncSource { - Settings = 'Settings', - Keybindings = 'Keybindings', - Extensions = 'Extensions', - GlobalState = 'GlobalState' -} - export const enum SyncStatus { Uninitialized = 'uninitialized', Idle = 'idle', @@ -246,8 +244,7 @@ export const enum SyncStatus { export interface IUserDataSynchroniser { - readonly resourceKey: ResourceKey; - readonly source: SyncSource; + readonly resource: SyncResource; readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; @@ -276,13 +273,13 @@ export interface IUserDataSyncEnablementService { _serviceBrand: any; readonly onDidChangeEnablement: Event; - readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]>; + readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>; isEnabled(): boolean; setEnablement(enabled: boolean): void; - isResourceEnabled(key: ResourceKey): boolean; - setResourceEnablement(key: ResourceKey, enabled: boolean): void; + isResourceEnabled(resource: SyncResource): boolean; + setResourceEnablement(resource: SyncResource, enabled: boolean): void; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); @@ -292,11 +289,11 @@ export interface IUserDataSyncService { readonly status: SyncStatus; readonly onDidChangeStatus: Event; - readonly conflictsSources: SyncSource[]; - readonly onDidChangeConflicts: Event; + readonly conflictsSources: SyncResource[]; + readonly onDidChangeConflicts: Event; - readonly onDidChangeLocal: Event; - readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>; + readonly onDidChangeLocal: Event; + readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; @@ -309,7 +306,7 @@ export interface IUserDataSyncService { isFirstTimeSyncWithMerge(): Promise; resolveContent(resource: URI): Promise; - accept(source: SyncSource, content: string): Promise; + accept(source: SyncResource, content: string): Promise; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); @@ -336,14 +333,6 @@ export interface IConflictSetting { remoteValue: any | undefined; } -export const ISettingsSyncService = createDecorator('ISettingsSyncService'); -export interface ISettingsSyncService extends IUserDataSynchroniser { - _serviceBrand: any; - readonly onDidChangeConflicts: Event; - readonly conflicts: IConflictSetting[]; - resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise; -} - //#endregion export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); @@ -351,50 +340,31 @@ export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; export const PREVIEW_QUERY = 'preview=true'; -export function toRemoteSyncResourceFromSource(source: SyncSource, ref?: string): URI { - return toRemoteSyncResource(getResourceKeyFromSyncSource(source), ref); +export function toRemoteSyncResource(resource: SyncResource, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resource}/${ref ? ref : 'latest'}` }); } -export function toRemoteSyncResource(resourceKey: ResourceKey, ref?: string): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); -} -export function toLocalBackupSyncResource(resourceKey: ResourceKey, ref?: string): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); +export function toLocalBackupSyncResource(resource: SyncResource, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resource}/${ref ? ref : 'latest'}` }); } -export function resolveSyncResource(resource: URI): { remote: boolean, resourceKey: ResourceKey, ref?: string } | null { - const remote = resource.authority === 'remote'; - const resourceKey: ResourceKey = basename(dirname(resource)) as ResourceKey; - const ref = basename(resource); - if (resourceKey && ref) { - return { remote, resourceKey, ref: ref !== 'latest' ? ref : undefined }; +export function resolveSyncResource(resource: URI): { remote: boolean, resource: SyncResource, ref?: string } | null { + if (resource.scheme === USER_DATA_SYNC_SCHEME) { + const remote = resource.authority === 'remote'; + const resourceKey: SyncResource = basename(dirname(resource)) as SyncResource; + const ref = basename(resource); + if (resourceKey && ref) { + return { remote, resource: resourceKey, ref: ref !== 'latest' ? ref : undefined }; + } } return null; } -export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncSource | undefined { +export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncResource | undefined { if (isEqual(uri, environmentService.settingsSyncPreviewResource)) { - return SyncSource.Settings; + return SyncResource.Settings; } if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) { - return SyncSource.Keybindings; + return SyncResource.Keybindings; } return undefined; } - -export function getResourceKeyFromSyncSource(source: SyncSource): ResourceKey { - switch (source) { - case SyncSource.Settings: return 'settings'; - case SyncSource.Keybindings: return 'keybindings'; - case SyncSource.Extensions: return 'extensions'; - case SyncSource.GlobalState: return 'globalState'; - } -} - -export function getSyncSourceFromResourceKey(resourceKey: ResourceKey): SyncSource { - switch (resourceKey) { - case 'settings': return SyncSource.Settings; - case 'keybindings': return SyncSource.Keybindings; - case 'extensions': return SyncSource.Extensions; - case 'globalState': return SyncSource.GlobalState; - } -} diff --git a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts index b439de0bc02..8f9333c9600 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, ResourceKey, ALL_RESOURCE_KEYS, IUserDataSyncBackupStoreService, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, ALL_SYNC_RESOURCES, IUserDataSyncBackupStoreService, IResourceRefHandle, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { joinPath } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService, IFileStat } from 'vs/platform/files/common/files'; @@ -23,11 +23,11 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, ) { super(); - ALL_RESOURCE_KEYS.forEach(resourceKey => this.cleanUpBackup(resourceKey)); + ALL_SYNC_RESOURCES.forEach(resourceKey => this.cleanUpBackup(resourceKey)); } - async getAllRefs(resourceKey: ResourceKey): Promise { - const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + async getAllRefs(resource: SyncResource): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resource); const stat = await this.fileService.resolve(folder); if (stat.children) { const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort().reverse(); @@ -39,22 +39,22 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD return []; } - async resolveContent(resourceKey: ResourceKey, ref?: string): Promise { + async resolveContent(resource: SyncResource, ref?: string): Promise { if (!ref) { - const refs = await this.getAllRefs(resourceKey); + const refs = await this.getAllRefs(resource); if (refs.length) { ref = refs[refs.length - 1].ref; } } if (ref) { - const file = joinPath(this.environmentService.userDataSyncHome, resourceKey, ref); + const file = joinPath(this.environmentService.userDataSyncHome, resource, ref); const content = await this.fileService.readFile(file); return content.value.toString(); } return null; } - async backup(resourceKey: ResourceKey, content: string): Promise { + async backup(resourceKey: SyncResource, content: string): Promise { const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); const resource = joinPath(folder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); try { @@ -67,8 +67,8 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD } catch (e) { /* Ignore */ } } - private async cleanUpBackup(resourceKey: ResourceKey): Promise { - const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + private async cleanUpBackup(resource: SyncResource): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resource); try { try { if (!(await this.fileService.exists(folder))) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts index 7adbf97471b..250dcbe66eb 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncEnablementService, ResourceKey, ALL_RESOURCE_KEYS } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; type SyncEnablementClassification = { enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; const enablementKey = 'sync.enable'; -function getEnablementKey(resourceKey: ResourceKey) { return `${enablementKey}.${resourceKey}`; } +function getEnablementKey(resource: SyncResource) { return `${enablementKey}.${resource}`; } export class UserDataSyncEnablementService extends Disposable implements IUserDataSyncEnablementService { @@ -23,14 +24,23 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa private _onDidChangeEnablement = new Emitter(); readonly onDidChangeEnablement: Event = this._onDidChangeEnablement.event; - private _onDidChangeResourceEnablement = new Emitter<[ResourceKey, boolean]>(); - readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]> = this._onDidChangeResourceEnablement.event; + private _onDidChangeResourceEnablement = new Emitter<[SyncResource, boolean]>(); + readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]> = this._onDidChangeResourceEnablement.event; constructor( @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, ) { super(); + switch (environmentService.args['sync']) { + case 'on': + this.setEnablement(true); + break; + case 'off': + this.setEnablement(false); + break; + } this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); } @@ -45,13 +55,13 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa } } - isResourceEnabled(resourceKey: ResourceKey): boolean { - return this.storageService.getBoolean(getEnablementKey(resourceKey), StorageScope.GLOBAL, true); + isResourceEnabled(resource: SyncResource): boolean { + return this.storageService.getBoolean(getEnablementKey(resource), StorageScope.GLOBAL, true); } - setResourceEnablement(resourceKey: ResourceKey, enabled: boolean): void { - if (this.isResourceEnabled(resourceKey) !== enabled) { - const resourceEnablementKey = getEnablementKey(resourceKey); + setResourceEnablement(resource: SyncResource, enabled: boolean): void { + if (this.isResourceEnabled(resource) !== enabled) { + const resourceEnablementKey = getEnablementKey(resource); this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(resourceEnablementKey, { enabled }); this.storageService.store(resourceEnablementKey, enabled, StorageScope.GLOBAL); } @@ -63,7 +73,7 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa this._onDidChangeEnablement.fire(this.isEnabled()); return; } - const resourceKey = ALL_RESOURCE_KEYS.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0]; + const resourceKey = ALL_SYNC_RESOURCES.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0]; if (resourceKey) { this._onDidChangeResourceEnablement.fire([resourceKey, this.isEnabled()]); return; diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index de2736a2fe3..6b8ef2b6710 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncStoreService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IUserDataSyncStoreService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -41,40 +41,6 @@ export class UserDataSyncChannel implements IServerChannel { } } -export class SettingsSyncChannel implements IServerChannel { - - constructor(private readonly service: ISettingsSyncService) { } - - listen(_: unknown, event: string): Event { - switch (event) { - case 'onDidChangeStatus': return this.service.onDidChangeStatus; - case 'onDidChangeLocal': return this.service.onDidChangeLocal; - case 'onDidChangeConflicts': return this.service.onDidChangeConflicts; - } - throw new Error(`Event not found: ${event}`); - } - - call(context: any, command: string, args?: any): Promise { - switch (command) { - case 'sync': return this.service.sync(); - case 'accept': return this.service.accept(args[0]); - case 'pull': return this.service.pull(); - case 'push': return this.service.push(); - case '_getInitialStatus': return Promise.resolve(this.service.status); - case '_getInitialConflicts': return Promise.resolve(this.service.conflicts); - case 'stop': this.service.stop(); return Promise.resolve(); - case 'resetLocal': return this.service.resetLocal(); - case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); - case 'hasLocalData': return this.service.hasLocalData(); - case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]); - case 'getRemoteContentFromPreview': return this.service.getRemoteContentFromPreview(); - case 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]); - case 'getLocalBackupContent': return this.service.getLocalBackupContent(args[0], args[1]); - } - throw new Error('Invalid call'); - } -} - export class UserDataAutoSyncChannel implements IServerChannel { constructor(private readonly service: IUserDataAutoSyncService) { } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 4be50d8387a..fd8d3a93cea 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveSyncResource, PREVIEW_QUERY } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveSyncResource, PREVIEW_QUERY } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -16,6 +16,7 @@ import { equals } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { URI } from 'vs/base/common/uri'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -35,22 +36,23 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - readonly onDidChangeLocal: Event; + readonly onDidChangeLocal: Event; - private _conflictsSources: SyncSource[] = []; - get conflictsSources(): SyncSource[] { return this._conflictsSources; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _conflictsSources: SyncResource[] = []; + get conflictsSources(): SyncResource[] { return this._conflictsSources; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; - private _syncErrors: [SyncSource, UserDataSyncError][] = []; - private _onSyncErrors: Emitter<[SyncSource, UserDataSyncError][]> = this._register(new Emitter<[SyncSource, UserDataSyncError][]>()); - readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]> = this._onSyncErrors.event; + private _syncErrors: [SyncResource, UserDataSyncError][] = []; + private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>()); + readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event; private _lastSyncTime: number | undefined = undefined; get lastSyncTime(): number | undefined { return this._lastSyncTime; } private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); readonly onDidChangeLastSyncTime: Event = this._onDidChangeLastSyncTime.event; + private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser; @@ -58,12 +60,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, ) { super(); + this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); @@ -75,7 +77,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined); - this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.source))); + this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource))); } async pull(): Promise { @@ -84,7 +86,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { await synchroniser.pull(); } catch (e) { - this.handleSyncError(e, synchroniser.source); + this.handleSyncError(e, synchroniser.resource); } } this.updateLastSyncTime(); @@ -96,7 +98,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { await synchroniser.push(); } catch (e) { - this.handleSyncError(e, synchroniser.source); + this.handleSyncError(e, synchroniser.resource); } } this.updateLastSyncTime(); @@ -129,10 +131,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ for (const synchroniser of this.synchronisers) { try { - await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined); + await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resource] : undefined); } catch (e) { - this.handleSyncError(e, synchroniser.source); - this._syncErrors.push([synchroniser.source, UserDataSyncError.toUserDataSyncError(e)]); + this.handleSyncError(e, synchroniser.resource); + this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]); } } @@ -171,7 +173,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - async accept(source: SyncSource, content: string): Promise { + async accept(source: SyncResource, content: string): Promise { await this.checkEnablement(); const synchroniser = this.getSynchroniser(source); await synchroniser.accept(content); @@ -180,7 +182,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ async resolveContent(resource: URI): Promise { const result = resolveSyncResource(resource); if (result) { - const synchronizer = this.synchronisers.filter(s => s.resourceKey === result.resourceKey)[0]; + const synchronizer = this.synchronisers.filter(s => s.resource === result.resource)[0]; if (synchronizer) { if (PREVIEW_QUERY === resource.query) { return result.remote ? synchronizer.getRemoteContentFromPreview() : null; @@ -216,7 +218,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { synchroniser.resetLocal(); } catch (e) { - this.logService.error(`${synchroniser.source}: ${toErrorMessage(e)}`); + this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`); this.logService.error(e); } } @@ -291,7 +293,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - private handleSyncError(e: Error, source: SyncSource): void { + private handleSyncError(e: Error, source: SyncResource): void { if (e instanceof UserDataSyncStoreError) { switch (e.code) { case UserDataSyncErrorCode.TooLarge: @@ -303,12 +305,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.error(`${source}: ${toErrorMessage(e)}`); } - private computeConflictsSources(): SyncSource[] { - return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.source); + private computeConflictsSources(): SyncResource[] { + return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.resource); } - getSynchroniser(source: SyncSource): IUserDataSynchroniser { - return this.synchronisers.filter(s => s.source === source)[0]; + getSynchroniser(source: SyncResource): IUserDataSynchroniser { + return this.synchronisers.filter(s => s.resource === source)[0]; } private async checkEnablement(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 091170c7abc..344fca00b30 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, ResourceKey, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -13,12 +13,19 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { IProductService } from 'vs/platform/product/common/productService'; import { URI } from 'vs/base/common/uri'; +import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { assign } from 'vs/base/common/objects'; + export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { _serviceBrand: any; readonly userDataSyncStore: IUserDataSyncStore | undefined; + private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; constructor( @IProductService productService: IProductService, @@ -26,17 +33,25 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn @IRequestService private readonly requestService: IRequestService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); + this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) + .then(uuid => ({ + 'X-Sync-Client-Id': productService.version, + 'X-Sync-Machine-Id': uuid + })); } - async getAllRefs(key: ResourceKey): Promise { + async getAllRefs(resource: SyncResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } - const uri = joinPath(this.userDataSyncStore.url, 'resource', key); + const uri = joinPath(this.userDataSyncStore.url, 'resource', resource); const headers: IHeaders = {}; const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None); @@ -46,15 +61,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } const result = await asJson<{ url: string, created: number }[]>(context) || []; - return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url))!, created: created })); + return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url))!, created: created * 1000 /* Server returns in seconds */ })); } - async resolveContent(key: ResourceKey, ref: string): Promise { + async resolveContent(resource: SyncResource, ref: string): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', key, ref).toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString(); const headers: IHeaders = {}; const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); @@ -67,12 +82,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn return content; } - async delete(key: ResourceKey): Promise { + async delete(resource: SyncResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); const headers: IHeaders = {}; const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); @@ -82,12 +97,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } } - async read(key: ResourceKey, oldValue: IUserData | null, source?: SyncSource): Promise { + async read(resource: SyncResource, oldValue: IUserData | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', key, 'latest').toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString(); const headers: IHeaders = {}; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -95,7 +110,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-None-Match'] = oldValue.ref; } - const context = await this.request({ type: 'GET', url, headers }, source, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None); if (context.res.statusCode === 304) { // There is no new value. Hence return the old value. @@ -103,37 +118,37 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, source); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); } const ref = context.res.headers['etag']; if (!ref) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, source); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); } const content = await asText(context); return { ref, content }; } - async write(key: ResourceKey, data: string, ref: string | null, source?: SyncSource): Promise { + async write(resource: SyncResource, data: string, ref: string | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; if (ref) { headers['If-Match'] = ref; } - const context = await this.request({ type: 'POST', url, data, headers }, source, CancellationToken.None); + const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, source); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); } const newRef = context.res.headers['etag']; if (!newRef) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, source); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); } return newRef; } @@ -169,13 +184,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } } - private async request(options: IRequestOptions, source: SyncSource | undefined, token: CancellationToken): Promise { + private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise { const authToken = await this.authTokenService.getToken(); if (!authToken) { throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source); } - options.headers = options.headers || {}; - options.headers['authorization'] = `Bearer ${authToken}`; + + const commonHeaders = await this.commonHeadersPromise; + options.headers = assign(options.headers || {}, commonHeaders, { + 'authorization': `Bearer ${authToken}`, + }); this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } }); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 7c40ae8d70d..c0f87712cf1 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, IUserDataSyncService, SyncSource, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { SettingsSynchroniser, ISettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; @@ -44,7 +44,7 @@ suite('SettingsSync', () => { setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); await client.setUp(); - testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncSource.Settings) as SettingsSynchroniser; + testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser; disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); }); @@ -77,7 +77,7 @@ suite('SettingsSync', () => { await updateSettings(expected); await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, expected); @@ -101,7 +101,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -132,7 +132,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -163,7 +163,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -187,7 +187,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -205,7 +205,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -239,7 +239,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ @@ -287,7 +287,7 @@ suite('SettingsSync', () => { await testObject.sync(); - const { content } = await client.read(testObject.resourceKey); + const { content } = await client.read(testObject.resource); assert.ok(content !== null); const actual = parseSettings(content!); assert.deepEqual(actual, `{ diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index b2fa135d413..d8abc061edf 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ResourceKey, IUserDataSyncStoreService, SyncSource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; @@ -17,7 +17,7 @@ class TestSynchroniser extends AbstractSynchroniser { syncResult: { status?: SyncStatus, error?: boolean } = {}; onDoSyncCall: Emitter = this._register(new Emitter()); - readonly resourceKey: ResourceKey = 'settings'; + readonly resource: SyncResource = SyncResource.Settings; protected readonly version: number = 1; private cancelled: boolean = false; @@ -40,7 +40,7 @@ class TestSynchroniser extends AbstractSynchroniser { } async apply(ref: string): Promise { - ref = await this.userDataSyncStoreService.write(this.resourceKey, '', ref); + ref = await this.userDataSyncStoreService.write(this.resource, '', ref); await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); } @@ -68,7 +68,7 @@ suite('TestSynchronizer', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -85,7 +85,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -97,7 +97,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); @@ -110,7 +110,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); testObject.syncResult = { error: true }; testObject.syncBarrier.open(); @@ -127,7 +127,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(); @@ -144,8 +144,8 @@ suite('TestSynchronizer', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); - client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resourceKey, false); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); + client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resource, false); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -157,7 +157,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); await testObject.sync(); @@ -171,7 +171,7 @@ suite('TestSynchronizer', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings, 'settings'); // Sync once testObject.syncBarrier.open(); await testObject.sync(); @@ -186,13 +186,13 @@ suite('TestSynchronizer', () => { }); // Start sycing - const { ref } = await userDataSyncStoreService.read(testObject.resourceKey, null); + const { ref } = await userDataSyncStoreService.read(testObject.resource, null); await testObject.sync(ref); assert.deepEqual(server.requests, [ - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resourceKey}`, headers: { 'If-Match': ref } }, - { type: 'GET', url: `${server.url}/v1/resource/${testObject.resourceKey}/latest`, headers: {} }, - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resourceKey}`, headers: { 'If-Match': `${parseInt(ref) + 1}` } }, + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': `${parseInt(ref) + 1}` } }, ]); }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 181df5ae44f..7f6e210866e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -31,7 +31,6 @@ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagemen import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { Disposable } from 'vs/base/common/lifecycle'; -import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter } from 'vs/base/common/event'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import product from 'vs/platform/product/common/product'; @@ -57,6 +56,7 @@ export class UserDataSyncClient extends Disposable { keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'), argvResource: joinPath(userDataDirectory, 'argv.json'), + args: {} }); const logService = new NullLogService(); @@ -105,7 +105,6 @@ export class UserDataSyncClient extends Disposable { async getCompatibleExtension() { return null; } }); - this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser)); this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); if (!empty) { @@ -120,8 +119,8 @@ export class UserDataSyncClient extends Disposable { return this.instantiationService.get(IUserDataSyncService).sync(); } - read(key: ResourceKey): Promise { - return this.instantiationService.get(IUserDataSyncStoreService).read(key, null); + read(resource: SyncResource): Promise { + return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null); } } @@ -132,7 +131,7 @@ export class UserDataSyncTestServer implements IRequestService { readonly url: string = 'http://host:3000'; private session: string | null = null; - private readonly data: Map = new Map(); + private readonly data: Map = new Map(); private _requests: { url: string, type: string, headers?: IHeaders }[] = []; get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; } @@ -180,7 +179,7 @@ export class UserDataSyncTestServer implements IRequestService { private async getManifest(headers?: IHeaders): Promise { if (this.session) { - const latest: Record = Object.create({}); + const latest: Record = Object.create({}); const manifest: IUserDataManifest = { session: this.session, latest }; this.data.forEach((value, key) => latest[key] = value.ref); return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest)); @@ -189,7 +188,7 @@ export class UserDataSyncTestServer implements IRequestService { } private async getLatestData(resource: string, headers: IHeaders = {}): Promise { - const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); if (!data) { @@ -210,7 +209,7 @@ export class UserDataSyncTestServer implements IRequestService { if (!this.session) { this.session = generateUuid(); } - const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); if (headers['If-Match'] !== (data ? data.ref : '0')) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 86ca60f8a0a..6c5e02511da 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; @@ -480,7 +480,7 @@ suite('UserDataSyncService', () => { await testObject.sync(); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); - assert.deepEqual(testObject.conflictsSources, [SyncSource.Settings]); + assert.deepEqual(testObject.conflictsSources, [SyncResource.Settings]); }); test('test sync will sync other non conflicted areas', async () => { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 53826e67839..c5b6fe3f2dd 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2116,6 +2116,9 @@ declare module 'vscode' { /** * A [command](#Command) this code action executes. + * + * If this command throws an exception, VS Code displays the exception message to users in the editor at the + * current cursor position. */ command?: Command; @@ -2145,8 +2148,8 @@ declare module 'vscode' { * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user a - * message with `reason` in the editor. + * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user an + * error message with `reason` in the editor. */ disabled?: { /** @@ -7929,7 +7932,7 @@ declare module 'vscode' { /** * The location at which progress should show. */ - location: ProgressLocation; + location: ProgressLocation | { viewId: string }; /** * A human-readable string which will be used to describe the diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 311be8bb15b..9d69b64b455 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -76,9 +76,9 @@ declare module 'vscode' { export const onDidChangeAuthenticationProviders: Event; /** - * Returns whether a provider with providerId is currently registered. + * An array of the ids of authentication providers that are currently registered. */ - export function hasProvider(providerId: string): boolean; + export const providerIds: string[]; /** * Get existing authentication sessions. Rejects if a provider with providerId is not @@ -1086,6 +1086,22 @@ declare module 'vscode' { //#endregion + //#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606 + + export namespace window { + export function registerTerminalLinkHandler(handler: TerminalLinkHandler): Disposable; + } + + export interface TerminalLinkHandler { + /** + * @return true when the link was handled (and should not be considered by + * other providers including the default), false when the link was not handled. + */ + handleLink(terminal: Terminal, link: string): ProviderResult; + } + + //#endregion + //#region Joh -> exclusive document filters export interface DocumentFilter { @@ -1222,81 +1238,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 +1317,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 +1419,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 +1437,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; } /** @@ -1422,6 +1473,21 @@ declare module 'vscode' { * @return Thenable indicating that the webview editor has been resolved. */ resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel): Thenable; + + /** + * TODO: discuss this at api sync. + * + * Handle when the underlying resource for a custom editor is renamed. + * + * This allows the webview for the editor be preserved throughout the rename. If this method is not implemented, + * VS Code will destory the previous custom editor and create a replacement one. + * + * @param newDocument New text document to use for the custom editor. + * @param existingWebviewPanel Webview panel for the custom editor. + * + * @return Thenable indicating that the webview editor has been moved. + */ + moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel): Thenable; } namespace window { @@ -1438,7 +1504,7 @@ declare module 'vscode' { export function registerCustomEditorProvider( viewType: string, provider: CustomEditorProvider | CustomTextEditorProvider, - webviewOptions?: WebviewPanelOptions, + webviewOptions?: WebviewPanelOptions, // TODO: move this onto provider? ): Disposable; } @@ -1921,9 +1987,9 @@ declare module 'vscode' { * A code that identifies this error. * * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), - * or `undefined` for an unspecified error. + * or `Unknown` for an unspecified error. */ - readonly code?: string; + readonly code: string; } //#endregion @@ -1943,4 +2009,5 @@ declare module 'vscode' { } //#endregion + } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 4e48c4bf864..9686820a36a 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -75,7 +75,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise { - return Promise.resolve(this._proxy.$runInTerminal(args)); + return this._proxy.$runInTerminal(args); } // RPC methods (MainThreadDebugServiceShape) diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 372db8a6af2..7018e5b4837 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { ITerminalInstanceService, ITerminalService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; @@ -23,6 +23,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private readonly _terminalProcesses = new Map>(); private readonly _terminalProcessesReady = new Map void>(); private _dataEventTracker: TerminalDataEventTracker | undefined; + private _linkHandler: IDisposable | undefined; constructor( extHostContext: IExtHostContext, @@ -146,6 +147,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } + public $startHandlingLinks(): void { + this._linkHandler?.dispose(); + this._linkHandler = this._terminalService.addLinkHandler(this._remoteAuthority || '', e => this._handleLink(e)); + } + + public $stopHandlingLinks(): void { + this._linkHandler?.dispose(); + } + + private async _handleLink(e: ITerminalBeforeHandleLinkEvent): Promise { + if (!e.terminal) { + return false; + } + return this._proxy.$handleLink(e.terminal.id, e.link); + } + private _onActiveTerminalChanged(terminalId: number | null): void { this._proxy.$acceptActiveTerminalChanged(terminalId); } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 9fd5f3d3b17..8549130f30e 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'; @@ -278,12 +279,12 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._revivers.delete(viewType); } - public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void { - return this.registerEditorProvider(ModelType.Text, extensionData, viewType, options); + public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { + this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities); } public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void { - return this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options); + this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {}); } private registerEditorProvider( @@ -291,41 +292,45 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, - ): void { + capabilities: extHostProtocol.CustomTextEditorCapabilities, + ): DisposableStore { if (this._editorProviders.has(viewType)) { throw new Error(`Provider for ${viewType} already registered`); } const extension = reviveWebviewExtension(extensionData); - this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({ + const disposables = new DisposableStore(); + disposables.add(this._webviewWorkbenchService.registerResolver({ canResolve: (webviewInput) => { return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType; }, resolveWebview: async (webviewInput: CustomEditorInput) => { const handle = webviewInput.id; this._webviewInputs.add(handle, webviewInput); - this.hookupWebviewEventDelegate(handle, webviewInput); + this.hookupWebviewEventDelegate(handle, webviewInput); webviewInput.webview.options = options; webviewInput.webview.extension = extension; const resource = webviewInput.resource; + let modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType); - const modelRef = await this.getOrCreateCustomEditorModel(modelType, webviewInput, resource, viewType); webviewInput.webview.onDispose(() => { modelRef.dispose(); }); + if (capabilities.supportsMove) { + webviewInput.onMove(async (newResource: URI) => { + const oldModel = modelRef; + modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType); + this._proxy.$onMoveCustomEditor(handle, newResource, viewType); + oldModel.dispose(); + }); + } + try { - await this._proxy.$resolveWebviewEditor( - resource, - handle, - viewType, - webviewInput.getTitle(), - editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), - webviewInput.webview.options - ); + await this._proxy.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); } catch (error) { onUnexpectedError(error); webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType); @@ -333,6 +338,10 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } } })); + + this._editorProviders.set(viewType, disposables); + + return disposables; } public $unregisterEditorProvider(viewType: string): void { @@ -349,11 +358,10 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private async getOrCreateCustomEditorModel( modelType: ModelType, - webviewInput: WebviewInput, resource: URI, viewType: string, ): Promise> { - const existingModel = this._customEditorService.models.tryRetain(webviewInput.resource, webviewInput.viewType); + const existingModel = this._customEditorService.models.tryRetain(resource, viewType); if (existingModel) { return existingModel; } @@ -365,12 +373,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) { @@ -380,14 +390,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma disposables.add(input.webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); disposables.add(input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); - input.onDispose(() => { + disposables.add(input.webview.onDispose(() => { disposables.dispose(); - }); - input.webview.onDispose(() => { + this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { this._webviewInputs.delete(handle); }); - }); + })); } private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { @@ -533,10 +542,14 @@ namespace HotExitState { export type State = typeof Allowed | typeof NotAllowed | Pending; } +const customDocumentFileScheme = 'custom'; + 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, @@ -551,27 +564,42 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod constructor( private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, private readonly _viewType: string, - private readonly _resource: URI, + private readonly _realResource: URI, private readonly _editable: boolean, @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() { - this._proxy.$disposeWebviewCustomEditorDocument(this.resource, this._viewType); + if (this._editable) { + this._undoService.removeElements(this._realResource); + } + this._proxy.$disposeWebviewCustomEditorDocument(this._realResource, this._viewType); super.dispose(); } //#region IWorkingCopy - public get resource() { return this._resource; } + public get resource() { + // Make sure each custom editor has a unique resource for backup and edits + return URI.from({ + scheme: customDocumentFileScheme, + authority: this._viewType, + path: this._realResource.path, + query: JSON.stringify(this._realResource.toJSON()) + }); + } public get name() { - return basename(this._labelService.getUriLabel(this._resource)); + return basename(this._labelService.getUriLabel(this._realResource)); } public get capabilities(): WorkingCopyCapabilities { @@ -579,7 +607,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()); @@ -590,50 +618,133 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod //#endregion + public isReadonly() { + return this._editable; + } + public get viewType() { 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._realResource, + 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._realResource, 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._realResource, 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._realResource, 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._realResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }); + this.change(() => { + this._currentEditIndex = this._savePoint; + this.spliceEdits(); + }); } public async save(_options?: ISaveOptions): Promise { if (!this._editable) { return false; } - await createCancelablePromise(token => this._proxy.$onSave(this.resource, this.viewType, token)); - this.setDirty(false); + await createCancelablePromise(token => this._proxy.$onSave(this._realResource, this.viewType, token)); + 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); + await this._proxy.$onSaveAs(this._realResource, this.viewType, targetResource); + this.change(() => { + this._savePoint = this._currentEditIndex; + }); return true; } else { // Since the editor is readonly, just copy the file over @@ -659,7 +770,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod const pendingState = new HotExitState.Pending( createCancelablePromise(token => - this._proxy.$backup(this.resource.toJSON(), this.viewType, token))); + this._proxy.$backup(this._realResource.toJSON(), this.viewType, token))); this._hotExitState = pendingState; try { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 22100a3c167..94119f6530e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -190,8 +190,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeAuthenticationProviders(): Event { return extHostAuthentication.onDidChangeAuthenticationProviders; }, - hasProvider(providerId: string): boolean { - return extHostAuthentication.hasProvider(providerId); + get providerIds(): string[] { + return extHostAuthentication.providerIds; }, getSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.getSessions(extension, providerId, scopes); @@ -542,6 +542,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } })); }, withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { + if (typeof options.location !== 'number') { + checkProposedApiEnabled(extension); + } return extHostProgress.withProgress(extension, options, task); }, createOutputChannel(name: string): vscode.OutputChannel { @@ -563,6 +566,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, + registerTerminalLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostTerminalService.registerLinkHandler(handler); + }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7eb4d7e86d..9f6a5ac157d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -68,6 +68,7 @@ export interface IEnvironment { userHome: URI; webviewResourceRoot: string; webviewCspSource: string; + useHostProxy?: boolean; } export interface IStaticWorkspaceData { @@ -432,6 +433,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $show(terminalId: number, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; + $startHandlingLinks(): void; + $stopHandlingLinks(): void; // Process $sendProcessTitle(terminalId: number, title: string): void; @@ -584,6 +587,10 @@ export enum WebviewEditorCapabilities { SupportsHotExit, } +export interface CustomTextEditorCapabilities { + readonly supportsMove?: boolean; +} + export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; $disposeWebview(handle: WebviewPanelHandle): void; @@ -599,11 +606,11 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; - $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; + $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $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 { @@ -626,13 +633,17 @@ 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; $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + + $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; } export enum CellKind { @@ -1362,6 +1373,7 @@ export interface ExtHostTerminalServiceShape { $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; $getAvailableShells(): Promise; $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; + $handleLink(id: number, link: string): Promise; } export interface ExtHostSCMShape { diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index baa486b5c5c..e57c6e13961 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -24,8 +24,13 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } - hasProvider(providerId: string): boolean { - return !!this._authenticationProviders.get(providerId); + get providerIds(): string[] { + const ids: string[] = []; + this._authenticationProviders.forEach(provider => { + ids.push(provider.id); + }); + + return ids; } async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { diff --git a/src/vs/workbench/api/common/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts index 27a4e145c42..5db3edf0862 100644 --- a/src/vs/workbench/api/common/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -26,6 +26,7 @@ export class ExtHostProgress implements ExtHostProgressShape { const handle = this._handles++; const { title, location, cancellable } = options; const source = localize('extensionSource', "{0} (Extension)", extension.displayName || extension.name); + this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, source, cancellable }, extension); return this._withProgress(handle, task, !!cancellable); } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index bccacd0543f..caf46e8a4ce 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -14,6 +14,7 @@ import { timeout } from 'vs/base/common/async'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable as VSCodeDisposable } from './extHostTypes'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { @@ -34,6 +35,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; + registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable } export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); @@ -295,6 +297,9 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {}; protected _getTerminalPromises: { [id: number]: Promise } = {}; + private readonly _bufferer: TerminalDataBufferer; + private readonly _linkHandlers: Set = new Set(); + public get activeTerminal(): ExtHostTerminal | undefined { return this._activeTerminal; } public get terminals(): ExtHostTerminal[] { return this._terminals; } @@ -309,8 +314,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected readonly _onDidWriteTerminalData: Emitter; public get onDidWriteTerminalData(): Event { return this._onDidWriteTerminalData && this._onDidWriteTerminalData.event; } - private readonly _bufferer: TerminalDataBufferer; - constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService ) { @@ -535,6 +538,38 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ return id; } + public registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable { + this._linkHandlers.add(handler); + if (this._linkHandlers.size === 1) { + this._proxy.$startHandlingLinks(); + } + return new VSCodeDisposable(() => { + this._linkHandlers.delete(handler); + if (this._linkHandlers.size === 0) { + this._proxy.$stopHandlingLinks(); + } + }); + } + + public async $handleLink(id: number, link: string): Promise { + const terminal = this._getTerminalById(id); + if (!terminal) { + return false; + } + + // Call each handler synchronously so multiple handlers aren't triggered at once + const it = this._linkHandlers.values(); + let next = it.next(); + while (!next.done) { + const handled = await next.value.handleLink(terminal, link); + if (handled) { + return true; + } + next = it.next(); + } + return false; + } + private _onProcessExit(id: number, exitCode: number | undefined): void { this._bufferer.stopBuffering(id); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 38154ceca2e..0a0034e0b44 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1093,7 +1093,11 @@ export namespace EndOfLine { } export namespace ProgressLocation { - export function from(loc: vscode.ProgressLocation): MainProgressLocation { + export function from(loc: vscode.ProgressLocation | { viewId: string }): MainProgressLocation | string { + if (typeof loc === 'string') { + return loc; + } + switch (loc) { case types.ProgressLocation.SourceControl: return MainProgressLocation.Scm; case types.ProgressLocation.Window: return MainProgressLocation.Window; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0df038e3931..27e443c0440 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2333,12 +2333,12 @@ export class FileSystemError extends Error { return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.Unavailable, FileSystemError.Unavailable); } - readonly code?: string; + readonly code: string; constructor(uriOrMessage?: string | URI, code: FileSystemProviderErrorCode = FileSystemProviderErrorCode.Unknown, terminator?: Function) { super(URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); - this.code = terminator?.name; + this.code = terminator?.name ?? 'Unknown'; // mark the error as file system provider error so that // we can extract the error code on the receiving side diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 86588547cd2..81cba4cd4b6 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,26 @@ 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); - this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options); + disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); + this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options, { + supportsMove: !!provider.moveCustomTextEditor, + }); } 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 +599,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 +660,49 @@ 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 { - const document = this.getCustomDocument(viewType, resourceComponents); - document._redo(); + async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { + throw new Error(`Provider does not implement move '${viewType}'`); + } + + const webview = this.getWebviewPanel(handle); + if (!webview) { + throw new Error(`No webview found`); + } + + const resource = URI.revive(newResourceComponents); + const document = this._extHostDocuments.getDocument(resource); + await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview); } - async $revert(resourceComponents: UriComponents, viewType: string): Promise { + async $undo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { const document = this.getCustomDocument(viewType, resourceComponents); - document._revert(); + return document._undo(editId); + } + + async $redo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { + const document = this.getCustomDocument(viewType, resourceComponents); + 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/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index dab72ef96b4..23ec3e88954 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -113,7 +113,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } else if (args.kind === 'external') { - runInExternalTerminal(args, await this._configurationService.getConfigProvider()); + return runInExternalTerminal(args, await this._configurationService.getConfigProvider()); } return super.$runInTerminal(args); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 79189ba670b..3a02c5ce0b7 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -61,7 +61,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Do this when extension service exists, but extensions are not being activated yet. const configProvider = await this._extHostConfiguration.getConfigProvider(); - await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy); + await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData); // Use IPC messages to forward console-calls, note that the console is // already patched to use`process.send()` diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index cb77aab68f5..3f1403df47d 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/labels.ts b/src/vs/workbench/browser/labels.ts index a394c7d34a4..2f4829e9b51 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -24,11 +24,23 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { withNullAsUndefined } from 'vs/base/common/types'; export interface IResourceLabelProps { - resource?: URI; + resource?: URI | { master?: URI, detail?: URI }; name?: string | string[]; description?: string; } +function toResource(props: IResourceLabelProps | undefined): URI | undefined { + if (!props || !props.resource) { + return undefined; + } + + if (URI.isUri(props.resource)) { + return props.resource; + } + + return props.resource.master; +} + export interface IResourceLabelOptions extends IIconLabelValueOptions { fileKind?: FileKind; fileDecorations?: { colors: boolean, badges: boolean }; @@ -289,11 +301,16 @@ class ResourceLabelWidget extends IconLabel { } notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void { - if (!this.options || !this.label || !this.label.resource) { + if (!this.options) { return; } - if (this.options.fileDecorations && e.affectsResource(this.label.resource)) { + const resource = toResource(this.label); + if (!resource) { + return; + } + + if (this.options.fileDecorations && e.affectsResource(resource)) { this.render(false); } } @@ -311,13 +328,13 @@ class ResourceLabelWidget extends IconLabel { } notifyFormattersChange(scheme: string): void { - if (this.label?.resource?.scheme === scheme) { + if (toResource(this.label)?.scheme === scheme) { this.render(false); } } notifyUntitledLabelChange(resource: URI): void { - if (isEqual(resource, this.label?.resource)) { + if (isEqual(resource, toResource(this.label))) { this.render(false); } } @@ -347,7 +364,10 @@ class ResourceLabelWidget extends IconLabel { } setResource(label: IResourceLabelProps, options: IResourceLabelOptions = Object.create(null)): void { - if (label.resource?.scheme === Schemas.untitled) { + const resource = toResource(this.label); + const isMasterDetail = this.label?.resource && !URI.isUri(this.label.resource); + + if (!isMasterDetail && resource?.scheme === Schemas.untitled) { // Untitled labels are very dynamic because they may change // whenever the content changes (unless a path is associated). // As such we always ask the actual editor for it's name and @@ -355,7 +375,11 @@ class ResourceLabelWidget extends IconLabel { // provided. If they are not provided from the label we got // we assume that the client does not want to display them // and as such do not override. - const untitledModel = this.textFileService.untitled.get(label.resource); + // + // We do not touch the label if it represents a master-detail + // because in that case we expect it to carry a proper label + // and description. + const untitledModel = this.textFileService.untitled.get(resource); if (untitledModel && !untitledModel.hasAssociatedFilePath) { if (typeof label.name === 'string') { label.name = untitledModel.name; @@ -415,7 +439,7 @@ class ResourceLabelWidget extends IconLabel { } private hasPathLabelChanged(newLabel: IResourceLabelProps, newOptions?: IResourceLabelOptions): boolean { - const newResource = newLabel ? newLabel.resource : undefined; + const newResource = toResource(newLabel); return !!newResource && this.computedPathLabel !== this.labelService.getUriLabel(newResource); } @@ -444,7 +468,8 @@ class ResourceLabelWidget extends IconLabel { } if (this.label) { - const detectedModeId = this.label.resource ? withNullAsUndefined(detectModeId(this.modelService, this.modeService, this.label.resource)) : undefined; + const resource = toResource(this.label); + const detectedModeId = resource ? withNullAsUndefined(detectModeId(this.modelService, this.modeService, resource)) : undefined; if (this.lastKnownDetectedModeId !== detectedModeId) { clearIconCache = true; this.lastKnownDetectedModeId = detectedModeId; @@ -463,14 +488,15 @@ class ResourceLabelWidget extends IconLabel { const iconLabelOptions: IIconLabelValueOptions & { extraClasses: string[] } = { title: '', - italic: this.options && this.options.italic, - matches: this.options && this.options.matches, + italic: this.options?.italic, + strikethrough: this.options?.strikethrough, + matches: this.options?.matches, extraClasses: [], separator: this.options?.separator, domId: this.options?.domId }; - const resource = this.label.resource; + const resource = toResource(this.label); const label = this.label.name; if (this.options && typeof this.options.title === 'string') { diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 44b9bb08329..0194b0a3b3b 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -4,60 +4,39 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IQuickPick, IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource } from 'vs/platform/quickinput/common/quickInput'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; +import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorsOrder, IEditorIdentifier, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; -import { prepareQuery, IPreparedQuery, ScorerCache, scoreItem, compareItemsByScore } from 'vs/base/common/fuzzyScorer'; -import { URI } from 'vs/base/common/uri'; +import { prepareQuery, scoreItem, compareItemsByScore } from 'vs/base/common/fuzzyScorer'; -interface IEditorQuickPickItem extends IQuickPickItemWithResource, IEditorIdentifier { - resource: URI | undefined; -} +interface IEditorQuickPickItem extends IQuickPickItemWithResource, IEditorIdentifier, IPickerQuickAccessItem { } -export abstract class BaseEditorQuickAccessProvider implements IQuickAccessProvider { - - protected abstract readonly prefix: string; +export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessProvider { constructor( + prefix: string, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @IEditorService protected readonly editorService: IEditorService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService ) { + super(prefix); } - provide(picker: IQuickPick, token: CancellationToken): IDisposable { - const disposables = new DisposableStore(); + protected configure(picker: IQuickPick): void { - // Disable filtering & sorting, we control the results - picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + // Allow to open editors in background without closing picker + picker.canAcceptInBackground = true; + } - // Add all view items & filter on type + protected getPicks(filter: string): Array { + const query = prepareQuery(filter); const scorerCache = Object.create(null); - const updatePickerItems = () => picker.items = this.getEditorPickItems(prepareQuery(picker.value.trim().substr(this.prefix.length)), scorerCache); - disposables.add(picker.onDidChangeValue(() => updatePickerItems())); - updatePickerItems(); - - // Open the picked view on accept - disposables.add(picker.onDidAccept(() => { - const [item] = picker.selectedItems; - if (item) { - picker.hide(); - this.editorGroupService.getGroup(item.groupId)?.openEditor(item.editor); - } - })); - - return disposables; - } - - private getEditorPickItems(query: IPreparedQuery, scorerCache: ScorerCache): Array { const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => { if (!query.value) { return true; @@ -110,18 +89,32 @@ export abstract class BaseEditorQuickAccessProvider implements IQuickAccessProvi } private doGetEditorPickItems(): Array { - return this.doGetEditors().map(({ editor, groupId }) => { + return this.doGetEditors().map(({ editor, groupId }): IEditorQuickPickItem => { const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); + const isDirty = editor.isDirty() && !editor.isSaving(); return { editor, groupId, resource, - label: editor.isDirty() && !editor.isSaving() ? `$(circle-filled) ${editor.getName()}` : editor.getName(), - ariaLabel: localize('entryAriaLabel', "{0}, editor picker", editor.getName()), + label: editor.getName(), + ariaLabel: localize('entryAriaLabel', "{0}, editors picker", editor.getName()), description: editor.getDescription(), iconClasses: getIconClasses(this.modelService, this.modeService, resource), - italic: !this.editorGroupService.getGroup(groupId)?.isPinned(editor) + italic: !this.editorGroupService.getGroup(groupId)?.isPinned(editor), + buttonsAlwaysVisible: isDirty, + buttons: [ + { + iconClass: isDirty ? 'codicon-circle-filled' : 'codicon-close', + tooltip: localize('closeEditor', "Close Editor") + } + ], + trigger: async () => { + await this.editorGroupService.getGroup(groupId)?.closeEditor(editor, { preserveFocus: true }); + + return TriggerAction.REFRESH_PICKER; + }, + accept: (keyMods, event) => this.editorGroupService.getGroup(groupId)?.openEditor(editor, { preserveFocus: event.inBackground }), }; }); } @@ -135,7 +128,14 @@ export class ActiveGroupEditorsByMostRecentlyUsedQuickAccess extends BaseEditorQ static PREFIX = 'edt active '; - readonly prefix = ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX; + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService + ) { + super(ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX, editorGroupService, editorService, modelService, modeService); + } protected doGetEditors(): IEditorIdentifier[] { const group = this.editorGroupService.activeGroup; @@ -153,7 +153,14 @@ export class AllEditorsByAppearanceQuickAccess extends BaseEditorQuickAccessProv static PREFIX = 'edt '; - readonly prefix = AllEditorsByAppearanceQuickAccess.PREFIX; + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService + ) { + super(AllEditorsByAppearanceQuickAccess.PREFIX, editorGroupService, editorService, modelService, modeService); + } protected doGetEditors(): IEditorIdentifier[] { const entries: IEditorIdentifier[] = []; @@ -177,7 +184,14 @@ export class AllEditorsByMostRecentlyUsedQuickAccess extends BaseEditorQuickAcce static PREFIX = 'edt mru '; - readonly prefix = AllEditorsByMostRecentlyUsedQuickAccess.PREFIX; + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService + ) { + super(AllEditorsByMostRecentlyUsedQuickAccess.PREFIX, editorGroupService, editorService, modelService, modeService); + } protected doGetEditors(): IEditorIdentifier[] { const entries: IEditorIdentifier[] = []; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 211fdec8ce1..3af508ded81 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -260,9 +260,6 @@ export class NoTabsTitleControl extends TitleControl { this.updateEditorDirty(editor); // Editor Label - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - const name = editor.getName(); - const { labelFormat } = this.accessor.partOptions; let description: string; if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { @@ -278,7 +275,19 @@ export class NoTabsTitleControl extends TitleControl { title = ''; // dont repeat what is already shown } - editorLabel.setResource({ name, description, resource }, { title: typeof title === 'string' ? title : undefined, italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'] }); + editorLabel.setResource( + { + resource: toResource(editor, { supportSideBySide: SideBySideEditor.BOTH }), + name: editor.getName(), + description + }, + { + title, + italic: !isEditorPinned, + extraClasses: ['no-tabs', 'title-label'] + } + ); + if (isGroupActive) { editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; } else { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d5042465900..c85a70573e1 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -963,10 +963,13 @@ export class TabsTitleControl extends TitleControl { tabContainer.title = title; // Label - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - tabLabelWidget.setResource({ name, description, resource }, { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) }); + tabLabelWidget.setResource( + { name, description, resource: toResource(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, + { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) } + ); // Tests helper + const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource) { tabContainer.setAttribute('data-resource-name', basenameOrAuthority(resource)); } else { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 19cd1f14f2b..d6384439f03 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -151,7 +151,10 @@ class NotificationMessageRenderer { const anchor = $('a', { href: node.href, title: title, }, node.label); if (actionHandler) { - actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, () => actionHandler.callback(node.href))); + actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, e => { + EventHelper.stop(e, true); + actionHandler.callback(node.href); + })); } messageContainer.appendChild(anchor); diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 25b9dcd0625..c5953e53573 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 MIT License. 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 9ec114b965a..58bae1c2809 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -7,6 +7,10 @@ border-top: none !important; /* less clutter: do not show any border for first views in a pane */ } +.monaco-pane-view .pane > .pane-header { + position: relative; +} + .monaco-pane-view .pane > .pane-header > .actions.show { display: initial; } @@ -23,3 +27,15 @@ .monaco-pane-view .pane > .pane-header h3.title:first-child { margin-left: 7px; } + +.monaco-pane-view .pane .monaco-progress-container { + position: absolute; + left: 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/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index d2ef7ff154a..f706855932b 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -171,6 +171,10 @@ background-repeat: no-repeat; } +.customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon { + line-height: 22px; +} + .customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon::before { vertical-align: middle; } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index afc66e4979b..8cc4285aa63 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler } from 'vs/platform/theme/common/styler'; +import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -44,6 +44,9 @@ import { Button } from 'vs/base/browser/ui/button/button'; import { Link } from 'vs/platform/opener/browser/link'; import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -181,6 +184,8 @@ export abstract class ViewPane extends Pane implements IView { title: string; private readonly menuActions: ViewMenuActions; + private progressBar!: ProgressBar; + private progressIndicator!: IProgressIndicator; private toolbar?: ToolBar; private readonly showActionsAlways: boolean = false; @@ -205,7 +210,7 @@ export abstract class ViewPane extends Pane implements IView { @IThemeService protected themeService: IThemeService, @ITelemetryService protected telemetryService: ITelemetryService, ) { - super(options); + super({ ...options, ...{ orientation: viewDescriptorService.getViewLocation(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); this.id = options.id; this.title = options.title; @@ -220,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; @@ -320,6 +334,20 @@ export abstract class ViewPane extends Pane implements IView { // noop } + getProgressIndicator() { + if (this.progressBar === undefined) { + // Progress bar + this.progressBar = this._register(new ProgressBar(this.element)); + this._register(attachProgressBarStyler(this.progressBar, this.themeService)); + this.progressBar.hide(); + } + + if (this.progressIndicator === undefined) { + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isVisible()); + } + return this.progressIndicator; + } + protected getProgressLocation(): string { return this.viewDescriptorService.getViewContainer(this.id)!.id; } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 4d4cb205a2d..2cf36b3d452 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -34,6 +34,7 @@ import { Viewlet, ViewletDescriptor, ViewletRegistry, Extensions as ViewletExten import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { URI } from 'vs/base/common/uri'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; export interface IViewState { visibleGlobal: boolean | undefined; @@ -465,6 +466,8 @@ export class ViewsService extends Disposable implements IViewsService { private readonly visibleViewContextKeys: Map>; + private readonly viewPaneContainers: Map; + constructor( @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IPanelService private readonly panelService: IPanelService, @@ -476,6 +479,7 @@ export class ViewsService extends Disposable implements IViewsService { this.viewContainersRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); this.viewDisposable = new Map(); this.visibleViewContextKeys = new Map>(); + this.viewPaneContainers = new Map(); this._register(toDisposable(() => { this.viewDisposable.forEach(disposable => disposable.dispose()); @@ -484,12 +488,16 @@ export class ViewsService extends Disposable implements IViewsService { this.viewContainersRegistry.all.forEach(viewContainer => this.onDidRegisterViewContainer(viewContainer, this.viewContainersRegistry.getViewContainerLocation(viewContainer))); this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer, viewContainerLocation }) => this.onDidRegisterViewContainer(viewContainer, viewContainerLocation))); + + this._register(this.viewContainersRegistry.onDidDeregister(e => this.viewPaneContainers.delete(e.viewContainer.id))); } private registerViewPaneContainer(viewPaneContainer: ViewPaneContainer): void { this._register(viewPaneContainer.onDidAddViews(views => this.onViewsAdded(views))); this._register(viewPaneContainer.onDidChangeViewVisibility(view => this.onViewsVisibilityChanged(view, view.isBodyVisible()))); this._register(viewPaneContainer.onDidRemoveViews(views => this.onViewsRemoved(views))); + + this.viewPaneContainers.set(viewPaneContainer.getId(), viewPaneContainer); } private onViewsAdded(added: IView[]): void { @@ -699,6 +707,16 @@ export class ViewsService extends Disposable implements IViewsService { return null; } + getProgressIndicator(id: string): IProgressIndicator | undefined { + const viewContainer = this.viewDescriptorService.getViewContainer(id); + if (viewContainer === null) { + return undefined; + } + + const view = this.viewPaneContainers.get(viewContainer.id)?.getView(id); + return view?.getProgressIndicator(); + } + private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { switch (viewContainerLocation) { case ViewContainerLocation.Panel: diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 8973e3fc36c..fe12a0da800 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 263d17ccba4..743441a13ee 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -333,6 +333,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/common/editor.ts b/src/vs/workbench/common/editor.ts index f7ccc4da432..227a3e9cf5f 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1334,7 +1334,8 @@ export interface IEditorPartOptionsChangeEvent { export enum SideBySideEditor { MASTER = 1, - DETAILS = 2 + DETAILS = 2, + BOTH = 3 } export interface IResourceOptions { @@ -1342,12 +1343,22 @@ export interface IResourceOptions { filterByScheme?: string | string[]; } -export function toResource(editor: IEditorInput | undefined, options?: IResourceOptions): URI | undefined { +export function toResource(editor: IEditorInput | undefined): URI | undefined; +export function toResource(editor: IEditorInput | undefined, options: IResourceOptions & { supportSideBySide?: SideBySideEditor.MASTER | SideBySideEditor.DETAILS }): URI | undefined; +export function toResource(editor: IEditorInput | undefined, options: IResourceOptions & { supportSideBySide: SideBySideEditor.BOTH }): URI | { master?: URI, detail?: URI } | undefined; +export function toResource(editor: IEditorInput | undefined, options?: IResourceOptions): URI | { master?: URI, detail?: URI } | undefined { if (!editor) { return undefined; } if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { + if (options?.supportSideBySide === SideBySideEditor.BOTH) { + return { + master: toResource(editor.master, { filterByScheme: options.filterByScheme }), + detail: toResource(editor.details, { filterByScheme: options.filterByScheme }) + }; + } + editor = options.supportSideBySide === SideBySideEditor.MASTER ? editor.master : editor.details; } @@ -1356,12 +1367,14 @@ export function toResource(editor: IEditorInput | undefined, options?: IResource return resource; } - if (Array.isArray(options.filterByScheme) && options.filterByScheme.some(scheme => resource.scheme === scheme)) { - return resource; - } - - if (options.filterByScheme === resource.scheme) { - return resource; + if (Array.isArray(options.filterByScheme)) { + if (options.filterByScheme.some(scheme => resource.scheme === scheme)) { + return resource; + } + } else { + if (options.filterByScheme === resource.scheme) { + return resource; + } } return undefined; diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index 597dd5d96f9..2a7844da48f 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -113,8 +113,8 @@ export class ResourceGlobMatcher extends Disposable { private readonly _onExpressionChange = this._register(new Emitter()); readonly onExpressionChange = this._onExpressionChange.event; - private readonly mapRootToParsedExpression: Map = new Map(); - private readonly mapRootToExpressionConfig: Map = new Map(); + private readonly mapRootToParsedExpression = new Map(); + private readonly mapRootToExpressionConfig = new Map(); constructor( private globFn: (root?: URI) => IExpression, diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 14564a51d3b..c303cd3c1ab 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -20,6 +20,8 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { flatten, mergeSort } from 'vs/base/common/arrays'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SetMap } from 'vs/base/common/collections'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; +import Severity from 'vs/base/common/severity'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -402,6 +404,7 @@ export interface IView { setExpanded(expanded: boolean): boolean; + getProgressIndicator(): IProgressIndicator | undefined; } export interface IViewsViewlet extends IViewlet { @@ -426,6 +429,7 @@ export interface IViewsService { closeView(id: string): void; + getProgressIndicator(id: string): IProgressIndicator | undefined; } /** @@ -580,7 +584,7 @@ export interface ITreeViewDataProvider { } export interface IEditableData { - validationMessage: (value: string) => string | null; + validationMessage: (value: string) => { content: string, severity: Severity } | null; placeholder?: string | null; startingValue?: string | null; onFinish: (value: string, success: boolean) => void; diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index b3b65134298..ea3b5f8d8b6 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 1e7343260ea..ea8c85ea656 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('BackupTracker', () => { 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 85225bed6c1..f76fc93385e 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/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index ddcd8ff16b4..b9992fde979 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -10,7 +10,9 @@ import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; import './quickaccess/gotoLineQuickAccess'; +import './quickaccess/gotoSymbolQuickAccess'; import './saveParticipants'; +import './semanticTokensHelp'; import './toggleColumnSelection'; import './toggleMinimap'; import './toggleMultiCursorModifier'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index e50b43138e3..6bb934a241d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -26,7 +26,7 @@ import { findMatchingThemeRule } from 'vs/workbench/services/textMate/common/TMH import { ITextMateService, IGrammar, IToken, StackElement } from 'vs/workbench/services/textMate/common/textMateService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ColorThemeData, TokenStyleDefinitions, TokenStyleDefinition } from 'vs/workbench/services/themes/common/colorThemeData'; +import { ColorThemeData, TokenStyleDefinitions, TokenStyleDefinition, TextMateThemingRuleDefinitions } from 'vs/workbench/services/themes/common/colorThemeData'; import { TokenStylingRule, TokenStyleData, TokenStyle } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -260,6 +260,9 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } private _isSemanticColoringEnabled() { + if (!this._themeService.getColorTheme().semanticHighlighting) { + return false; + } const options = this._configurationService.getValue('editor.semanticHighlighting', { overrideIdentifier: this._model.getLanguageIdentifier().language, resource: this._model.uri }); return options && options.enabled; } @@ -303,7 +306,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const allDefValues = []; // remember the order // first collect to detect when the same rule is used fro multiple properties for (let property of properties) { - if (semanticTokenInfo.metadata[property]) { + if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); let properties = propertiesByDefValue[defValue]; @@ -532,11 +535,11 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const isTokenStylingRule = (d: any): d is TokenStylingRule => !!d.value; if (Array.isArray(definition)) { - for (const d of definition) { - const matchingRule = findMatchingThemeRule(theme, d, false); - if (matchingRule) { - return `${escape(d.join(' '))}
${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; - } + const scopesDefinition: TextMateThemingRuleDefinitions = {}; + theme.resolveScopes(definition, scopesDefinition); + const matchingRule = scopesDefinition[property]; + if (matchingRule && scopesDefinition.scope) { + return `${escape(scopesDefinition.scope.join(' '))}
${matchingRule.scope}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; } return ''; } else if (isTokenStylingRule(definition)) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 6404881f956..0e8c8f29aa5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -8,39 +8,55 @@ 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, GOTO_LINE_PREFIX } 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { - readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; - constructor(@IEditorService private readonly editorService: IEditorService) { + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { super(); } - get activeTextEditorControl() { + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + }; + } + + protected get activeTextEditorControl() { return this.editorService.activeTextEditorControl; } - protected gotoLine(editor: IEditor, range: IRange, keyMods: IKeyMods): void { + protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { // Check for sideBySide use - if (keyMods.ctrlCmd && this.editorService.activeEditor) { - this.editorService.openEditor(this.editorService.activeEditor, { selection: range, pinned: keyMods.alt }, SIDE_GROUP); + if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + this.editorService.openEditor(this.editorService.activeEditor, { + selection: range, + pinned: keyMods.alt || this.configuration.openEditorPinned + }, SIDE_GROUP); } // Otherwise let parent handle it else { - super.gotoLine(editor, range, keyMods); + super.gotoLocation(editor, range, keyMods); } } } Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ ctor: GotoLineQuickAccessProvider, - prefix: GOTO_LINE_PREFIX, + prefix: AbstractGotoLineQuickAccessProvider.PREFIX, placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."), helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line"), needsEditor: true }] }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts new file mode 100644 index 00000000000..e3a213f9084 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { 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 { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; + +export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { + + protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super({ + openSideBySideDirection: () => this.configuration.openSideBySideDirection + }); + } + + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection + }; + } + + protected get activeTextEditorControl() { + return this.editorService.activeTextEditorControl; + } + + protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + + // Check for sideBySide use + if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + this.editorService.openEditor(this.editorService.activeEditor, { + selection: range, + pinned: keyMods.alt || this.configuration.openEditorPinned + }, SIDE_GROUP); + } + + // Otherwise let parent handle it + else { + super.gotoLocation(editor, range, keyMods); + } + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: GotoSymbolQuickAccessProvider, + prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, + placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), + helpEntries: [ + { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, + { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true } + ] +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts new file mode 100644 index 00000000000..4d260368d25 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as path from 'vs/base/common/path'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; + +/** + * Shows a message when semantic tokens are shown the first time. + */ +export class SemanticTokensHelp extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.semanticHighlightHelp'; + + constructor( + _editor: ICodeEditor, + @INotificationService _notificationService: INotificationService, + @IOpenerService _openerService: IOpenerService, + @IWorkbenchThemeService _themeService: IWorkbenchThemeService + ) { + super(); + + const toDispose = this._register(new DisposableStore()); + const localToDispose = toDispose.add(new DisposableStore()); + const installChangeTokenListener = (model: ITextModel) => { + localToDispose.add(model.onDidChangeTokens((e) => { + if (!e.semanticTokensApplied) { + return; + } + + toDispose.dispose(); // uninstall all listeners, makes sure the notification is only shown once per window + + const message = nls.localize( + { + key: 'semanticTokensHelp', + comment: [ + 'Variable 0 will be a file name.', + 'Variable 1 will be a theme name.' + ] + }, + "Semantic highlighting has been applied to '{0}' as the theme '{1}' has semantic highlighting enabled.", + path.basename(model.uri.path), _themeService.getColorTheme().label + ); + + _notificationService.prompt(Severity.Info, message, [ + { + label: nls.localize('learnMoreButton', "Learn More"), + run: () => { + const url = 'https://go.microsoft.com/fwlink/?linkid=2122588'; + + _openerService.open(URI.parse(url)); + } + } + ], { neverShowAgain: { id: 'editor.contrib.semanticTokensHelp' } }); + })); + }; + + + const model = _editor.getModel(); + if (model !== null) { + installChangeTokenListener(model); + } + + toDispose.add(_editor.onDidChangeModel((e) => { + localToDispose.clear(); + + const model = _editor.getModel(); + if (!model) { + return; + } + installChangeTokenListener(model); + })); + } +} + +registerEditorContribution(SemanticTokensHelp.ID, SemanticTokensHelp); diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 6d2b997a4c0..94b8ea8294a 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, @@ -186,7 +186,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { } } - const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup); + const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup.id); editorService.replaceEditors([{ editor: activeEditor, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 60e3d5a4ee3..39730562c4c 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -10,11 +10,11 @@ import { basename } from 'vs/base/common/path'; import { isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; 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 +44,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; @@ -96,7 +97,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } public isReadonly(): boolean { - return false; // TODO + return this._modelRef ? this._modelRef.object.isReadonly() : false; } public isDirty(): boolean { @@ -137,7 +138,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return undefined; } - return this.handleMove(groupId, target) || this.editorService.createEditorInput({ resource: target, forceFile: true }); + return this.tryMoveWebview(groupId, target) || this.editorService.createEditorInput({ resource: target, forceFile: true }); } public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { @@ -159,26 +160,60 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return null; } - public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { + move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + if (!this._moveHandler) { + return { + editor: this.customEditorService.createInput(newResource, this.viewType, group) + }; + } + this._moveHandler(newResource); + const newEditor = this.tryMoveWebview(group, newResource); + if (!newEditor) { + return; + } + return { editor: newEditor }; + } + + private tryMoveWebview(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(uri)) { - const webview = assertIsDefined(this.takeOwnershipOfWebview()); const newInput = this.instantiationService.createInstance(CustomEditorInput, uri, this.viewType, - generateUuid(), - new Lazy(() => webview)); + this.id, + new Lazy(() => undefined!)); // this webview is replaced in the transfer call + this.transfer(newInput); newInput.updateGroup(groupId); return newInput; } return undefined; } + 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); + } + + private _moveHandler?: (newResource: URI) => void; + + public onMove(handler: (newResource: URI) => void): void { + // TODO: Move this to the service + this._moveHandler = handler; + } + + protected transfer(other: CustomEditorInput): CustomEditorInput | undefined { + if (!super.transfer(other)) { + return; + } + + other._moveHandler = this._moveHandler; + this._moveHandler = undefined; + return other; } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 30c7de0b5b8..f5399eb8fe5 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -21,7 +21,7 @@ import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, EditorOptions, IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorInput, IEditorPane, GroupIdentifier } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; @@ -238,14 +238,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.promptOpenWith(resource, options, group); } - const input = this.createInput(resource, viewType, group); + const input = this.createInput(resource, viewType, group?.id); return this.openEditorForResource(resource, input, options, group); } public createInput( resource: URI, viewType: string, - group: IEditorGroup | undefined, + group: GroupIdentifier | undefined, options?: { readonly customClasses: string; }, ): IEditorInput { if (viewType === defaultEditorId) { @@ -257,8 +257,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}); }); const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview); - if (group) { - input.updateGroup(group.id); + if (typeof group !== 'undefined') { + input.updateGroup(group); } return input; } @@ -317,12 +317,18 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ continue; } + if (!isEqual(editor.resource, oldResource)) { + continue; + } + const editorInfo = this._editorInfoStore.get(editor.viewType); if (!editorInfo?.matches(newResource)) { continue; } - const replacement = this.createInput(newResource, editor.viewType, group); + const moveResult = editor.move(group.id, newResource); + const replacement = moveResult ? moveResult.editor : this.createInput(newResource, editor.viewType, group.id); + this.editorService.replaceEditors([{ editor: editor, replacement: replacement, @@ -486,7 +492,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return undefined; } - const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group, { customClasses }); + const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group.id, { customClasses }); if (input instanceof EditorInput) { return input; } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 72f3c0d0b6c..78369fec628 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -6,14 +6,14 @@ import { distinct, mergeSort } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; +import { IDisposable, IReference } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorPane, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IEditorInput, IEditorPane, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IDisposable, IReference } from 'vs/base/common/lifecycle'; export const ICustomEditorService = createDecorator('customEditorService'); @@ -29,7 +29,7 @@ export interface ICustomEditorService { getContributedCustomEditors(resource: URI): CustomEditorInfoCollection; getUserConfiguredCustomEditors(resource: URI): CustomEditorInfoCollection; - createInput(resource: URI, viewType: string, group: IEditorGroup | undefined, options?: { readonly customClasses: string }): IEditorInput; + createInput(resource: URI, viewType: string, group: GroupIdentifier | undefined, options?: { readonly customClasses: string }): IEditorInput; openWith(resource: URI, customEditorViewType: string, options?: ITextEditorOptions, group?: IEditorGroup): Promise; promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise; @@ -49,11 +49,11 @@ export interface ICustomEditorModel extends IDisposable { readonly viewType: string; readonly resource: URI; + isReadonly(): boolean; + 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 1f0c33e633f..a041fa249df 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -31,12 +31,12 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo private constructor( public readonly viewType: string, private readonly _resource: URI, - model: IReference, + private readonly _model: IReference, @ITextFileService private readonly textFileService: ITextFileService, ) { super(); - this._register(model); + this._register(_model); this._register(this.textFileService.files.onDidChangeDirty(e => { if (isEqual(this.resource, e.resource)) { @@ -50,6 +50,10 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return this._resource; } + public isReadonly(): boolean { + return this._model.object.isReadonly(); + } + public isDirty(): boolean { return this.textFileService.isDirty(this.resource); } @@ -64,14 +68,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/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 77086ba5fc7..b2ed59e3764 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -643,6 +643,11 @@ class ShowMoreRenderer implements ITreeRenderer { getHeight(element: CallStackItem): number { + if (element instanceof StackFrame) { + if (!element.source || !element.source.available || isDeemphasized(element)) { + return 12; + } + } return 22; } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 3cc92cc0715..6f93ec9f731 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -52,6 +52,8 @@ import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/ import { BreakpointEditorContribution } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { StartDebugQuickAccessProvider } from 'vs/workbench/contrib/debug/browser/debugQuickAccess'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -116,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); @@ -173,6 +175,14 @@ registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlin ) ); +// Register Quick Access +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: StartDebugQuickAccessProvider, + prefix: StartDebugQuickAccessProvider.PREFIX, + placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), + helpEntries: [{ description: nls.localize('startDebugHelp', "Start Debug Configurations"), needsEditor: false }] +}); + // register service registerSingleton(IDebugService, service.DebugService); diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index ca025ac0c76..df048219fac 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -393,20 +393,18 @@ export class CopyValueAction extends Action { async run(): Promise { const stackFrame = this.debugService.getViewModel().focusedStackFrame; const session = this.debugService.getViewModel().focusedSession; - - if (typeof this.value === 'string') { - return this.clipboardService.writeText(this.value); + if (!stackFrame || !session) { + return; } - if (stackFrame && session && this.value.evaluateName) { - try { - const evaluation = await session.evaluate(this.value.evaluateName, stackFrame.frameId, this.context); - this.clipboardService.writeText(evaluation.body.result); - } catch (e) { - this.clipboardService.writeText(this.value.value); - } - } else { - this.clipboardService.writeText(this.value.value); + const context = session.capabilities.supportsClipboardContext ? 'clipboard' : this.context; + const toEvaluate = typeof this.value === 'string' ? this.value : this.value.evaluateName || this.value.value; + + try { + const evaluation = await session.evaluate(toEvaluate, stackFrame.frameId, context); + this.clipboardService.writeText(evaluation.body.result); + } catch (e) { + this.clipboardService.writeText(typeof this.value === 'string' ? this.value : this.value.value); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 2ce26e4be5f..319ccdb451e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -697,7 +697,7 @@ class UserLaunch extends AbstractLaunch implements ILaunch { } async openConfigFile(_: boolean, preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { - const editor = await this.preferencesService.openGlobalSettings(false, { preserveFocus }); + const editor = await this.preferencesService.openGlobalSettings(true, { preserveFocus }); return ({ editor: withUndefinedAsNull(editor), created: false diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts new file mode 100644 index 00000000000..c2de911e0f6 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { localize } from 'vs/nls'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { StartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { withNullAsUndefined } from 'vs/base/common/types'; + +export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { + + static PREFIX = 'debug '; + + constructor( + @IDebugService private readonly debugService: IDebugService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @ICommandService private readonly commandService: ICommandService, + @INotificationService private readonly notificationService: INotificationService + ) { + super(StartDebugQuickAccessProvider.PREFIX); + } + + protected getPicks(filter: string): (IQuickPickSeparator | IPickerQuickAccessItem)[] { + const picks: Array = []; + + const configManager = this.debugService.getConfigurationManager(); + + // Entries: configs + let lastGroup: string | undefined; + for (let config of configManager.getAllConfigurations()) { + const highlights = matchesFuzzy(filter, config.name, true); + if (highlights) { + + // Separator + if (lastGroup !== config.presentation?.group) { + picks.push({ type: 'separator' }); + lastGroup = config.presentation?.group; + } + + // Launch entry + picks.push({ + label: config.name, + ariaLabel: localize('entryAriaLabel', "{0}, debug picker", config.name), + description: this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? config.launch.name : '', + highlights: { label: highlights }, + buttons: [{ + iconClass: 'codicon-gear', + tooltip: localize('customizeLaunchConfig', "Configure Launch Configuration") + }], + trigger: () => { + config.launch.openConfigFile(false, false); + + return TriggerAction.CLOSE_PICKER; + }, + accept: async () => { + if (StartAction.isEnabled(this.debugService)) { + this.debugService.getConfigurationManager().selectConfiguration(config.launch, config.name); + try { + await this.debugService.startDebugging(config.launch); + } catch (error) { + this.notificationService.error(error); + } + } + } + }); + } + } + + // Entries: launches + const visibleLaunches = configManager.getLaunches().filter(launch => !launch.hidden); + + // Separator + if (visibleLaunches.length > 0) { + picks.push({ type: 'separator' }); + } + + for (const launch of visibleLaunches) { + const label = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? + localize("addConfigTo", "Add Config ({0})...", launch.name) : + localize('addConfiguration', "Add Configuration..."); + + // Add Config entry + picks.push({ + label, + ariaLabel: localize('entryAriaLabel', "{0}, debug picker", label), + description: this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? launch.name : '', + highlights: { label: withNullAsUndefined(matchesFuzzy(filter, label, true)) }, + accept: () => this.commandService.executeCommand('debug.addConfiguration', launch.uri.toString()) + }); + } + + return picks; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 63833c44c8b..fae348805d3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -5,7 +5,6 @@ import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; -import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; import severity from 'vs/base/common/severity'; import { Event, Emitter } from 'vs/base/common/event'; @@ -34,6 +33,7 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cance import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { localize } from 'vs/nls'; export class DebugSession implements IDebugSession { @@ -232,7 +232,7 @@ export class DebugSession implements IDebugSession { */ async launchOrAttach(config: IConfig): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'launch or attach')); } // __sessionID only used for EH debugging (but we add it always for now...) @@ -250,7 +250,7 @@ export class DebugSession implements IDebugSession { */ async terminate(restart = false): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'terminate')); } this.cancelAllRequests(); @@ -266,7 +266,7 @@ export class DebugSession implements IDebugSession { */ async disconnect(restart = false): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'disconnect')); } this.cancelAllRequests(); @@ -278,7 +278,7 @@ export class DebugSession implements IDebugSession { */ async restart(): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restart')); } this.cancelAllRequests(); @@ -287,7 +287,7 @@ export class DebugSession implements IDebugSession { async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints')); } if (!this.raw.readyForBreakpoints) { @@ -321,7 +321,7 @@ export class DebugSession implements IDebugSession { async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'function breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -338,7 +338,7 @@ export class DebugSession implements IDebugSession { async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exception breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -348,10 +348,10 @@ export class DebugSession implements IDebugSession { async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }> { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints info')); } if (!this.raw.readyForBreakpoints) { - throw new Error(nls.localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); + throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); @@ -360,7 +360,7 @@ export class DebugSession implements IDebugSession { async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -377,7 +377,7 @@ export class DebugSession implements IDebugSession { async breakpointsLocations(uri: URI, lineNumber: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints locations')); } const source = this.getRawSource(uri); @@ -393,7 +393,7 @@ export class DebugSession implements IDebugSession { customRequest(request: string, args: any): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", request)); } return this.raw.custom(request, args); @@ -401,7 +401,7 @@ export class DebugSession implements IDebugSession { stackTrace(threadId: number, startFrame: number, levels: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stackTrace')); } const token = this.getNewCancellationToken(threadId); @@ -410,7 +410,7 @@ export class DebugSession implements IDebugSession { async exceptionInfo(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exceptionInfo')); } const response = await this.raw.exceptionInfo({ threadId }); @@ -428,7 +428,7 @@ export class DebugSession implements IDebugSession { scopes(frameId: number, threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'scopes')); } const token = this.getNewCancellationToken(threadId); @@ -437,7 +437,7 @@ export class DebugSession implements IDebugSession { variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'variables')); } const token = threadId ? this.getNewCancellationToken(threadId) : undefined; @@ -446,7 +446,7 @@ export class DebugSession implements IDebugSession { evaluate(expression: string, frameId: number, context?: string): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'evaluate')); } return this.raw.evaluate({ expression, frameId, context }); @@ -454,7 +454,7 @@ export class DebugSession implements IDebugSession { async restartFrame(frameId: number, threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restartFrame')); } await this.raw.restartFrame({ frameId }, threadId); @@ -462,7 +462,7 @@ export class DebugSession implements IDebugSession { async next(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'next')); } await this.raw.next({ threadId }); @@ -470,7 +470,7 @@ export class DebugSession implements IDebugSession { async stepIn(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepIn')); } await this.raw.stepIn({ threadId }); @@ -478,7 +478,7 @@ export class DebugSession implements IDebugSession { async stepOut(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepOut')); } await this.raw.stepOut({ threadId }); @@ -486,7 +486,7 @@ export class DebugSession implements IDebugSession { async stepBack(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepBack')); } await this.raw.stepBack({ threadId }); @@ -494,7 +494,7 @@ export class DebugSession implements IDebugSession { async continue(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'continue')); } await this.raw.continue({ threadId }); @@ -502,7 +502,7 @@ export class DebugSession implements IDebugSession { async reverseContinue(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'reverse continue')); } await this.raw.reverseContinue({ threadId }); @@ -510,7 +510,7 @@ export class DebugSession implements IDebugSession { async pause(threadId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'pause')); } await this.raw.pause({ threadId }); @@ -518,7 +518,7 @@ export class DebugSession implements IDebugSession { async terminateThreads(threadIds?: number[]): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'terminateThreads')); } await this.raw.terminateThreads({ threadIds }); @@ -526,7 +526,7 @@ export class DebugSession implements IDebugSession { setVariable(variablesReference: number, name: string, value: string): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'setVariable')); } return this.raw.setVariable({ variablesReference, name, value }); @@ -534,7 +534,7 @@ export class DebugSession implements IDebugSession { gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'gotoTargets')); } return this.raw.gotoTargets({ source, line, column }); @@ -542,7 +542,7 @@ export class DebugSession implements IDebugSession { goto(threadId: number, targetId: number): Promise { if (!this.raw) { - throw new Error('no debug adapter'); + throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'goto')); } return this.raw.goto({ threadId, targetId }); @@ -550,7 +550,7 @@ export class DebugSession implements IDebugSession { loadSource(resource: URI): Promise { if (!this.raw) { - return Promise.reject(new Error('no debug adapter')); + return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'loadSource'))); } const source = this.getSourceForUri(resource); @@ -568,7 +568,7 @@ export class DebugSession implements IDebugSession { async getLoadedSources(): Promise { if (!this.raw) { - return Promise.reject(new Error('no debug adapter')); + return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'getLoadedSources'))); } const response = await this.raw.loadedSources({}); @@ -581,7 +581,7 @@ export class DebugSession implements IDebugSession { async completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { if (!this.raw) { - return Promise.reject(new Error('no debug adapter')); + return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'completions'))); } return this.raw.completions({ @@ -700,7 +700,7 @@ export class DebugSession implements IDebugSession { } this.rawListeners.push(this.raw.onDidInitialize(async () => { - aria.status(nls.localize('debuggingStarted', "Debugging started.")); + aria.status(localize('debuggingStarted', "Debugging started.")); const sendConfigurationDone = async () => { if (this.raw && this.raw.capabilities.supportsConfigurationDoneRequest) { try { @@ -782,7 +782,7 @@ export class DebugSession implements IDebugSession { })); this.rawListeners.push(this.raw.onDidTerminateDebugee(async event => { - aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); + aria.status(localize('debuggingStopped', "Debugging stopped.")); if (event.body && event.body.restart) { await this.debugService.restartSession(this, event.body.restart); } else if (this.raw) { 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 2fb7b93bd76..fc93ad15e0b 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 518e7f07d7e..7a67a067a43 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -83,6 +83,7 @@ .debug-pane .disabled { opacity: 0.65; + cursor: initial; } /* Call stack */ @@ -163,6 +164,7 @@ .monaco-workbench .debug-pane .debug-call-stack .monaco-action-bar .action-item > .action-label { width: 16px; height: 100%; + line-height: 22px; margin-right: 8px; vertical-align: text-top; } @@ -316,6 +318,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/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 38e4f35cd63..6411c081038 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -230,7 +230,7 @@ export class RawDebugSession implements IDisposable { */ async start(): Promise { if (!this.debugAdapter) { - return Promise.reject(new Error('no debug adapter')); + return Promise.reject(new Error(nls.localize('noDebugAdapterStart', "No debug adapter, can not start debug session."))); } await this.debugAdapter.startSession(); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 6011bdbd99a..99b12d01db5 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 0dbd60fc37c..02cdac7f1f9 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 333c01000bd..7bd746a4b5b 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 5ee3d8f4117..d989002308b 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 eb63050bddf..2661d4e0f58 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 5807fa4a3b6..051e962d9dc 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/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 6be7695e38f..89e850f23aa 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -12,7 +12,7 @@ import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfigurat let externalTerminalService: IExternalTerminalService | undefined = undefined; -export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): void { +export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { if (!externalTerminalService) { if (env.isWindows) { externalTerminalService = new WindowsExternalTerminalService(undefined); @@ -20,12 +20,12 @@ export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestAr externalTerminalService = new MacExternalTerminalService(undefined); } else if (env.isLinux) { externalTerminalService = new LinuxExternalTerminalService(undefined); + } else { + throw new Error('external terminals not supported on this platform'); } } - if (externalTerminalService) { - const config = configProvider.getConfiguration('terminal'); - externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); - } + const config = configProvider.getConfiguration('terminal'); + return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {}); } function spawnAsPromised(command: string, args: string[]): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index cf17ac8d9c5..4d8f38e448b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -24,7 +24,7 @@ import { import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; -import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; +import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; @@ -50,6 +50,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { CONTEXT_SYNC_ENABLEMENT } from 'vs/platform/userDataSync/common/userDataSync'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { InstallExtensionQuickAccessProvider, ManageExtensionsQuickAccessProvider } from 'vs/workbench/contrib/extensions/browser/extensionsQuickAccess'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -59,7 +61,7 @@ Registry.as(OutputExtensions.OutputChannels) .registerChannel({ id: ExtensionsChannelId, label: ExtensionsLabel, log: false }); // Quickopen -Registry.as(Extensions.Quickopen).registerQuickOpenHandler( +Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpenHandler( QuickOpenHandlerDescriptor.create( ExtensionsHandler, ExtensionsHandler.ID, @@ -70,6 +72,14 @@ Registry.as(Extensions.Quickopen).registerQuickOpenHandler( ) ); +// Quick Access +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: ManageExtensionsQuickAccessProvider, + prefix: ManageExtensionsQuickAccessProvider.PREFIX, + placeholder: localize('manageExtensionsQuickAccessPlaceholder', "Press Enter to manage extensions."), + helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] +}); + // Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -476,7 +486,7 @@ class ExtensionsContributions implements IWorkbenchContribution { const canManageExtensions = extensionManagementServerService.localExtensionManagementServer || extensionManagementServerService.remoteExtensionManagementServer; if (canManageExtensions) { - Registry.as(Extensions.Quickopen).registerQuickOpenHandler( + Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpenHandler( QuickOpenHandlerDescriptor.create( GalleryExtensionsHandler, GalleryExtensionsHandler.ID, @@ -486,6 +496,13 @@ class ExtensionsContributions implements IWorkbenchContribution { true ) ); + + Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: InstallExtensionQuickAccessProvider, + prefix: InstallExtensionQuickAccessProvider.PREFIX, + placeholder: localize('installExtensionQuickAccessPlaceholder', "Type the name of an extension to install or search."), + helpEntries: [{ description: localize('installExtensionQuickAccessHelp', "Install or Search Extensions"), needsEditor: false }] + }); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index ca8928e5557..ddaaea85318 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 } from 'vs/platform/extensions/common/extensions'; @@ -1939,7 +1939,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 @@ -1951,7 +1951,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 @@ -1965,7 +1965,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 @@ -1979,7 +1979,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 @@ -1993,7 +1993,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 @@ -2007,7 +2007,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 new file mode 100644 index 00000000000..d45a0ebb62a --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; +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 { + + static PREFIX = 'ext install '; + + constructor( + @IViewletService private readonly viewletService: IViewletService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IExtensionManagementService private readonly extensionsService: IExtensionManagementService, + @INotificationService private readonly notificationService: INotificationService, + @ILogService private readonly logService: ILogService + ) { + super(InstallExtensionQuickAccessProvider.PREFIX); + } + + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> { + + // Nothing typed + if (!filter) { + return [{ + label: localize('type', "Type an extension name to install or search.") + }]; + } + + const genericSearchPickItem: IPickerQuickAccessItem = { + label: localize('searchFor', "Press Enter to search for extension '{0}'.", filter), + accept: () => this.searchExtension(filter) + }; + + // Extension ID typed: try to find it + if (/\./.test(filter)) { + return this.getPicksForExtensionId(filter, genericSearchPickItem, token); + } + + // Extension name typed: offer to search it + else { + return [genericSearchPickItem]; + } + } + + protected async getPicksForExtensionId(filter: string, fallback: IPickerQuickAccessItem, token: CancellationToken): Promise> { + try { + const galleryResult = await this.galleryService.query({ names: [filter], pageSize: 1 }, token); + if (token.isCancellationRequested) { + return []; // return early if canceled + } + + const galleryExtension = galleryResult.firstPage[0]; + if (!galleryExtension) { + return [fallback]; + } else { + return [{ + label: localize('install', "Press Enter to install extension '{0}'.", filter), + accept: () => this.installExtension(galleryExtension, filter) + }]; + } + } catch (error) { + if (token.isCancellationRequested) { + return []; // expected error + } + + this.logService.error(error); + + return [fallback]; + } + } + + private async installExtension(extension: IGalleryExtension, name: string): Promise { + try { + await openExtensionsViewlet(this.viewletService, `@id:${name}`); + await this.extensionsService.installFromGallery(extension); + } catch (error) { + this.notificationService.error(error); + } + } + + private async searchExtension(name: string): Promise { + openExtensionsViewlet(this.viewletService, name); + } +} + +export class ManageExtensionsQuickAccessProvider extends PickerQuickAccessProvider { + + static PREFIX = 'ext '; + + constructor(@IViewletService private readonly viewletService: IViewletService) { + super(ManageExtensionsQuickAccessProvider.PREFIX); + } + + protected getPicks(): Array { + return [{ + label: localize('manage', "Press Enter to manage your extensions."), + accept: () => openExtensionsViewlet(this.viewletService) + }]; + } +} + +async function openExtensionsViewlet(viewletService: IViewletService, search = ''): Promise { + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; + view?.search(search); + view?.focus(); +} diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 333072e183f..c2f3364c260 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/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 2bc6d031192..4e7e11e8ebe 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -733,18 +733,24 @@ export class ShowOpenedFileInNewWindow extends Action { } } -export function validateFileName(item: ExplorerItem, name: string): string | null { +export function validateFileName(item: ExplorerItem, name: string): { content: string, severity: Severity } | null { // Produce a well formed file name name = getWellFormedFileName(name); // Name not provided if (!name || name.length === 0 || /^\s+$/.test(name)) { - return nls.localize('emptyFileNameError', "A file or folder name must be provided."); + return { + content: nls.localize('emptyFileNameError', "A file or folder name must be provided."), + severity: Severity.Error + }; } // Relative paths only if (name[0] === '/' || name[0] === '\\') { - return nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."); + return { + content: nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."), + severity: Severity.Error + }; } const names = coalesce(name.split(/[\\/]/)); @@ -754,14 +760,27 @@ export function validateFileName(item: ExplorerItem, name: string): string | nul // Do not allow to overwrite existing file const child = parent?.getChild(name); if (child && child !== item) { - return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); + return { + content: nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name), + severity: Severity.Error + }; } } // Invalid File name const windowsBasenameValidity = item.resource.scheme === Schemas.file && isWindows; if (names.some((folderName) => !extpath.isValidBasename(folderName, windowsBasenameValidity))) { - return nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(name)); + return { + content: nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(name)), + severity: Severity.Error + }; + } + + if (names.some(name => /^\s|\s$/.test(name))) { + return { + content: nls.localize('fileNameWhitespaceWarning', "Leading or trailing whitespace detected in file or folder name."), + severity: Severity.Warning + }; } return null; @@ -783,7 +802,7 @@ export function getWellFormedFileName(filename: string): string { // Trim tabs filename = strings.trim(filename, '\t'); - // Remove trailing dots, slashes, and spaces + // Remove trailing dots and slashes filename = strings.rtrim(filename, '.'); filename = strings.rtrim(filename, '/'); filename = strings.rtrim(filename, '\\'); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index f92fbd96463..7f560e77ee5 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -13,7 +13,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; -import { AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; +import { AutoSaveConfiguration, HotExitConfiguration, FILES_EXCLUDE_CONFIG, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { VIEWLET_ID, SortOrder, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; @@ -202,9 +202,9 @@ configurationRegistry.registerConfiguration({ 'title': nls.localize('filesConfigurationTitle', "Files"), 'type': 'object', 'properties': { - 'files.exclude': { + [FILES_EXCLUDE_CONFIG]: { 'type': 'object', - 'markdownDescription': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), + 'markdownDescription': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting. Refer to the `#search.exclude#` setting to define search specific excludes. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), 'default': { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true }, 'scope': ConfigurationScope.RESOURCE, 'additionalProperties': { @@ -227,7 +227,7 @@ configurationRegistry.registerConfiguration({ ] } }, - 'files.associations': { + [FILES_ASSOCIATIONS_CONFIG]: { 'type': 'object', 'markdownDescription': nls.localize('associations', "Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."), }, @@ -306,7 +306,7 @@ configurationRegistry.registerConfiguration({ 'files.watcherExclude': { 'type': 'object', 'default': platform.isWindows /* https://github.com/Microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, - 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of cpu time on startup, you can exclude large folders to reduce the initial load."), + 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."), 'scope': ConfigurationScope.RESOURCE }, 'files.hotExit': hotExitConfiguration, diff --git a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts index 0f09459edf8..090962b14d7 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; -import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; +import { listInvalidItemForeground, listDeemphasizedForeground } from 'vs/platform/theme/common/colorRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { explorerRootErrorEmitter } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; @@ -34,6 +34,11 @@ export function provideDecorations(fileStat: ExplorerItem): IDecorationData | un letter: '?' }; } + if (fileStat.isExcluded) { + return { + color: listDeemphasizedForeground, + }; + } return undefined; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 107f5f1da8b..d3b8e0b336a 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -376,13 +376,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer { - const content = editableData.validationMessage(value); - if (!content) { + const message = editableData.validationMessage(value); + if (!message || message.severity !== Severity.Error) { return null; } return { - content, + content: message.content, formatContent: true, type: MessageType.ERROR }; @@ -392,10 +392,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer { - label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing! - }); - const lastDot = value.lastIndexOf('.'); inputBox.value = value; @@ -412,8 +408,27 @@ export class FilesRenderer implements ICompressibleTreeRenderer { + if (inputBox.isInputValid()) { + const message = editableData.validationMessage(inputBox.value); + if (message) { + inputBox.showMessage({ + content: message.content, + formatContent: true, + type: message.severity === Severity.Info ? MessageType.INFO : message.severity === Severity.Warning ? MessageType.WARNING : MessageType.ERROR + }); + } else { + inputBox.hideMessage(); + } + } + }; + showInputBoxNotification(); + const toDispose = [ inputBox, + inputBox.onDidChange(value => { + label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing! + }), DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Enter)) { if (inputBox.validate()) { @@ -423,6 +438,9 @@ export class FilesRenderer implements ICompressibleTreeRenderer { + showInputBoxNotification(); + }), DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { done(inputBox.isInputValid(), true); }), @@ -563,7 +581,9 @@ export class FilesFilter implements ITreeFilter { } private isVisible(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean { + stat.isExcluded = false; if (parentVisibility === TreeVisibility.Hidden) { + stat.isExcluded = true; return false; } if (this.explorerService.getEditableData(stat) || stat.isRoot) { @@ -573,6 +593,7 @@ export class FilesFilter implements ITreeFilter { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); if (cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) { + stat.isExcluded = true; const editors = this.editorService.visibleEditors; const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop(); if (editor) { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 1c22f7201cd..9d089f8261b 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -592,7 +592,7 @@ class OpenEditorRenderer implements IListRenderer { 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 81e71fd750f..9325e95d69b 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 ea14e0d998b..1bbf4ad8b0f 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 c7fbe8223f5..17d4417f0df 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 00000000000..47bf09f848b --- /dev/null +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { 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 { 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() + ]); + + protected get activeTextEditorControl(): IEditor | undefined { return this.editorService.activeTextEditorControl; } + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IMenuService private readonly menuService: IMenuService, + @IExtensionService private readonly extensionService: IExtensionService, + @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 8a03f8bd360..615cfa95982 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -7,33 +7,35 @@ import { localize } from 'vs/nls'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; -import { ViewQuickAccessProvider, VIEW_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/quickaccess/browser/viewQuickAccess'; -import { QUICK_ACCESS_COMMAND_ID } from 'vs/workbench/contrib/quickaccess/browser/quickAccessCommands'; +import { ViewQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/viewQuickAccess'; +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); -registry.defaultProvider = { - ctor: HelpQuickAccessProvider, - prefix: '', - placeholder: localize('helpQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here."), - helpEntries: [{ description: localize('gotoFileQuickAccess', "Go to File"), needsEditor: false }] -}; - registry.registerQuickAccessProvider({ ctor: HelpQuickAccessProvider, - prefix: '?', - placeholder: localize('helpQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here."), + prefix: HelpQuickAccessProvider.PREFIX, + placeholder: localize('helpQuickAccessPlaceholder', "Type '{0}' to get help on the actions you can take from here.", HelpQuickAccessProvider.PREFIX), helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers"), needsEditor: false }] }); registry.registerQuickAccessProvider({ ctor: ViewQuickAccessProvider, - prefix: VIEW_QUICK_ACCESS_PREFIX, + prefix: ViewQuickAccessProvider.PREFIX, placeholder: localize('viewQuickAccessPlaceholder', "Type the name of a view, output channel or terminal to open."), 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 +44,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 23b2521b043..8761cc73f2c 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/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index d19c973f300..502a6c9b9be 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -5,10 +5,8 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewDescriptorService, IViewsService, ViewContainer, IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry } from 'vs/workbench/common/views'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; @@ -20,14 +18,13 @@ import { matchesFuzzy } from 'vs/base/common/filters'; import { fuzzyContains } from 'vs/base/common/strings'; import { withNullAsUndefined } from 'vs/base/common/types'; -export const VIEW_QUICK_ACCESS_PREFIX = 'view '; - -interface IViewQuickPickItem extends IQuickPickItem { +interface IViewQuickPickItem extends IPickerQuickAccessItem { containerLabel: string; - run: () => Promise; } -export class ViewQuickAccessProvider implements IQuickAccessProvider { +export class ViewQuickAccessProvider extends PickerQuickAccessProvider { + + static PREFIX = 'view '; constructor( @IViewletService private readonly viewletService: IViewletService, @@ -36,34 +33,12 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { @IOutputService private readonly outputService: IOutputService, @ITerminalService private readonly terminalService: ITerminalService, @IPanelService private readonly panelService: IPanelService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { + super(ViewQuickAccessProvider.PREFIX); } - provide(picker: IQuickPick, token: CancellationToken): IDisposable { - const disposables = new DisposableStore(); - - // Disable filtering & sorting, we control the results - picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; - - // Add all view items & filter on type - const updatePickerItems = () => picker.items = this.getViewPickItems(picker.value.trim().substr(VIEW_QUICK_ACCESS_PREFIX.length)); - disposables.add(picker.onDidChangeValue(() => updatePickerItems())); - updatePickerItems(); - - // Open the picked view on accept - disposables.add(picker.onDidAccept(() => { - const [item] = picker.selectedItems; - if (item) { - picker.hide(); - item.run(); - } - })); - - return disposables; - } - - private getViewPickItems(filter: string): Array { + protected getPicks(filter: string): Array { const filteredViewEntries = this.doGetViewPickItems().filter(entry => { if (!filter) { return true; @@ -120,8 +95,9 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { if (this.contextKeyService.contextMatchesRules(view.when)) { result.push({ label: view.name, + ariaLabel: localize('viewPickAriaLabel', "{0}, view picker", view.name), containerLabel: viewlet.name, - run: () => this.viewsService.openView(view.id, true) + accept: () => this.viewsService.openView(view.id, true) }); } } @@ -135,8 +111,9 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { if (this.includeViewlet(viewlet)) { viewEntries.push({ label: viewlet.name, + ariaLabel: localize('viewPickAriaLabel', "{0}, view picker", viewlet.name), containerLabel: localize('views', "Side Bar"), - run: () => this.viewletService.openViewlet(viewlet.id, true) + accept: () => this.viewletService.openViewlet(viewlet.id, true) }); } } @@ -146,8 +123,9 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { for (const panel of panels) { viewEntries.push({ label: panel.name, + ariaLabel: localize('viewPickAriaLabel', "{0}, view picker", panel.name), containerLabel: localize('panels', "Panel"), - run: () => this.panelService.openPanel(panel.id, true) + accept: () => this.panelService.openPanel(panel.id, true) }); } @@ -162,13 +140,15 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { // Terminals this.terminalService.terminalTabs.forEach((tab, tabIndex) => { tab.terminalInstances.forEach((terminal, terminalIndex) => { + const label = localize('terminalTitle', "{0}: {1}", `${tabIndex + 1}.${terminalIndex + 1}`, terminal.title); viewEntries.push({ - label: localize('terminalTitle', "{0}: {1}", `${tabIndex + 1}.${terminalIndex + 1}`, terminal.title), + label, + ariaLabel: localize('viewPickAriaLabel', "{0}, view picker", label), containerLabel: localize('terminals', "Terminal"), - run: async () => { + accept: async () => { await this.terminalService.showPanel(true); - return this.terminalService.setActiveInstance(terminal); + this.terminalService.setActiveInstance(terminal); } }); }); @@ -177,10 +157,12 @@ export class ViewQuickAccessProvider implements IQuickAccessProvider { // Output Channels const channels = this.outputService.getChannelDescriptors(); for (const channel of channels) { + const label = channel.log ? localize('logChannel', "Log ({0})", channel.label) : channel.label; viewEntries.push({ - label: channel.log ? localize('logChannel', "Log ({0})", channel.label) : channel.label, + label, + ariaLabel: localize('viewPickAriaLabel', "{0}, view picker", label), containerLabel: localize('channels', "Output"), - run: () => this.outputService.showChannel(channel.id) + accept: () => this.outputService.showChannel(channel.id) }); } diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index a41fb8930e9..98c39f8386c 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/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 37bab5fdc5d..ae1bac984fd 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -28,7 +28,7 @@ import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/pl import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem, Tunnel } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { once } from 'vs/base/common/functional'; @@ -282,13 +282,13 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { - const content = editableData.validationMessage(value); - if (!content) { + const message = editableData.validationMessage(value); + if (!message || message.severity !== Severity.Error) { return null; } return { - content, + content: message.content, formatContent: true, type: MessageType.ERROR }; @@ -657,6 +657,17 @@ export class TunnelPanelDescriptor implements IViewDescriptor { } } +function validationMessage(validationString: string | null): { content: string, severity: Severity } | null { + if (!validationString) { + return null; + } + + return { + content: validationString, + severity: Severity.Error + }; +} + namespace LabelTunnelAction { export const ID = 'remote.tunnel.label'; export const LABEL = nls.localize('remote.tunnel.label', "Set Label"); @@ -733,7 +744,7 @@ namespace ForwardPortAction { } remoteExplorerService.setEditable(undefined, null); }, - validationMessage: validateInput, + validationMessage: (value) => validationMessage(validateInput(value)), placeholder: forwardPrompt }); } @@ -916,7 +927,7 @@ namespace ChangeLocalPortAction { } } }, - validationMessage: validateInput, + validationMessage: (value) => validationMessage(validateInput(value)), placeholder: nls.localize('remote.tunnelsView.changePort', "New local port") }); } diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 9c94a5c49eb..5ea3fd924b9 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 { + line-height: 22px; +} + .scm-viewlet .scm-editor { box-sizing: border-box; padding: 5px 12px 5px 16px; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 3d9deddf982..b57f2b35e6b 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -47,7 +47,7 @@ import { getWorkspaceSymbols } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { FileMatchOrMatch, ISearchWorkbenchService, RenderableMatch, SearchWorkbenchService, FileMatch, Match, FolderMatch } from 'vs/workbench/contrib/search/common/searchModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { VIEWLET_ID, VIEW_ID, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { VIEWLET_ID, VIEW_ID, SEARCH_EXCLUDE_CONFIG, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; @@ -55,6 +55,8 @@ import { assertType, assertIsDefined } from 'vs/base/common/types'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -651,6 +653,16 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen ) ); +// Register Quick Access Handler + +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: SymbolsQuickAccessProvider, + prefix: SymbolsQuickAccessProvider.PREFIX, + placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."), + contextKey: 'inWorkspaceSymbolsPicker', + helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), needsEditor: false }] +}); + // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ @@ -659,9 +671,9 @@ configurationRegistry.registerConfiguration({ title: nls.localize('searchConfigurationTitle', "Search"), type: 'object', properties: { - 'search.exclude': { + [SEARCH_EXCLUDE_CONFIG]: { type: 'object', - markdownDescription: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), + markdownDescription: nls.localize('exclude', "Configure glob patterns for excluding files and folders in fulltext searches and quick open. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), default: { '**/node_modules': true, '**/bower_components': true, '**/*.code-search': true }, additionalProperties: { anyOf: [ diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 706c368d91a..f133d15905d 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -27,7 +27,7 @@ import { ISearchConfigurationProperties } from 'vs/workbench/services/search/com import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; -import { appendKeyBindingLabel, isSearchViewFocused } from 'vs/workbench/contrib/search/browser/searchActions'; +import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { isMacintosh } from 'vs/base/common/platform'; @@ -52,19 +52,9 @@ export interface ISearchWidgetOptions { class ReplaceAllAction extends Action { - private static fgInstance: ReplaceAllAction | null = null; static readonly ID: string = 'search.action.replaceAll'; - static get INSTANCE(): ReplaceAllAction { - if (ReplaceAllAction.fgInstance === null) { - ReplaceAllAction.fgInstance = new ReplaceAllAction(); - } - return ReplaceAllAction.fgInstance; - } - - private _searchWidget: SearchWidget | null = null; - - constructor() { + constructor(private _searchWidget: SearchWidget) { super(ReplaceAllAction.ID, '', 'codicon-replace-all', false); } @@ -417,8 +407,7 @@ export class SearchWidget extends Widget { this._register(this.replaceInput.inputBox.onDidChange(() => this._onReplaceValueChanged.fire())); this._register(this.replaceInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire())); - this.replaceAllAction = ReplaceAllAction.INSTANCE; - this.replaceAllAction.searchWidget = this; + this.replaceAllAction = new ReplaceAllAction(this); this.replaceAllAction.label = SearchWidget.REPLACE_ALL_DISABLED_LABEL; this.replaceActionBar = this._register(new ActionBar(this.replaceContainer)); this.replaceActionBar.push([this.replaceAllAction], { icon: true, label: false }); @@ -667,8 +656,12 @@ export function registerContributions() { when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE), primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter, handler: accessor => { - if (isSearchViewFocused(accessor.get(IViewsService))) { - ReplaceAllAction.INSTANCE.run(); + const viewsService = accessor.get(IViewsService); + if (isSearchViewFocused(viewsService)) { + const searchView = getSearchView(viewsService); + if (searchView) { + new ReplaceAllAction(searchView.searchAndReplaceWidget).run(); + } } } }); diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts new file mode 100644 index 00000000000..19c7c4d083d --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { fuzzyScore, createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { stripWildcards } from 'vs/base/common/strings'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; +import { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { Schemas } from 'vs/base/common/network'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { Range } from 'vs/editor/common/core/range'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { IKeyMods, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search'; +import { ResourceMap } from 'vs/base/common/map'; + +interface ISymbolsQuickPickItem extends IPickerQuickAccessItem { + score: FuzzyScore; + symbol: IWorkspaceSymbol; +} + +export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider { + + static PREFIX = '#'; + + private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching + + private delayer = new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY); + + private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); + + constructor( + @ILabelService private readonly labelService: ILabelService, + @IOpenerService private readonly openerService: IOpenerService, + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(SymbolsQuickAccessProvider.PREFIX); + } + + protected configure(picker: IQuickPick): void { + + // Allow to open symbols in background without closing picker + picker.canAcceptInBackground = true; + } + + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection + }; + } + + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + return this.delayer.trigger(async () => { + if (token.isCancellationRequested) { + return []; + } + + return this.doGetSymbolPicks(filter, token); + }); + } + + private async doGetSymbolPicks(filter: string, token: CancellationToken): Promise> { + const workspaceSymbols = await getWorkspaceSymbols(filter, token); + if (token.isCancellationRequested) { + return []; + } + + const symbolPicks: Array = []; + + // Normalize filter + const [symbolFilter, containerFilter] = stripWildcards(filter).split(' ') as [string, string | undefined]; + const symbolFilterLow = symbolFilter.toLowerCase(); + const containerFilterLow = containerFilter?.toLowerCase(); + + // Convert to symbol picks and apply filtering + const openSideBySideDirection = this.configuration.openSideBySideDirection; + const symbolsExcludedByResource = new ResourceMap(); + for (const [provider, symbols] of workspaceSymbols) { + for (const symbol of symbols) { + + // Score by symbol label + const symbolLabel = symbol.name; + const symbolScore = fuzzyScore(symbolFilter, symbolFilterLow, 0, symbolLabel, symbolLabel.toLowerCase(), 0, true); + if (!symbolScore) { + continue; + } + + const symbolUri = symbol.location.uri; + let containerLabel: string | undefined = undefined; + if (symbolUri) { + const containerPath = this.labelService.getUriLabel(symbolUri, { relative: true }); + if (symbol.containerName) { + containerLabel = `${symbol.containerName} • ${containerPath}`; + } else { + containerLabel = containerPath; + } + } + + // Score by container if specified + let containerScore: FuzzyScore | undefined = undefined; + if (containerFilter && containerFilterLow) { + if (containerLabel) { + containerScore = fuzzyScore(containerFilter, containerFilterLow, 0, containerLabel, containerLabel.toLowerCase(), 0, true); + } + + if (!containerScore) { + continue; + } + } + + // Filter out symbols that match the global resource filter + if (symbolUri) { + let excludeSymbolByResource = symbolsExcludedByResource.get(symbolUri); + if (typeof excludeSymbolByResource === 'undefined') { + excludeSymbolByResource = this.resourceExcludeMatcher.matches(symbolUri); + symbolsExcludedByResource.set(symbolUri, excludeSymbolByResource); + } + + if (excludeSymbolByResource) { + continue; + } + } + + const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`; + const deprecated = symbol.tags ? symbol.tags.indexOf(SymbolTag.Deprecated) >= 0 : false; + + symbolPicks.push({ + symbol, + score: symbolScore, + label: symbolLabelWithIcon, + ariaLabel: localize('symbolAriaLabel', "{0}, symbols picker", symbolLabel), + highlights: deprecated ? undefined : { + label: createMatches(symbolScore, symbolLabelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */), + description: createMatches(containerScore) + }, + description: containerLabel, + strikethrough: deprecated, + buttons: [ + { + iconClass: openSideBySideDirection === 'right' ? 'codicon-split-horizontal' : 'codicon-split-vertical', + tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom") + } + ], + accept: async (keyMods, event) => this.openSymbol(provider, symbol, token, keyMods, { preserveFocus: event.inBackground }), + trigger: (buttonIndex, keyMods) => { + this.openSymbol(provider, symbol, token, keyMods, { forceOpenSideBySide: true }); + + return TriggerAction.CLOSE_PICKER; + } + }); + } + } + + // Sort picks + symbolPicks.sort((symbolA, symbolB) => this.compareSymbols(symbolA, symbolB)); + + return symbolPicks; + } + + private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, keyMods: IKeyMods, options: { forceOpenSideBySide?: boolean, preserveFocus?: boolean }): Promise { + + // Resolve actual symbol to open for providers that can resolve + let symbolToOpen = symbol; + if (typeof provider.resolveWorkspaceSymbol === 'function' && !symbol.location.range) { + symbolToOpen = await provider.resolveWorkspaceSymbol(symbol, token) || symbol; + + if (token.isCancellationRequested) { + return; + } + } + + // Open HTTP(s) links with opener service + if (symbolToOpen.location.uri.scheme === Schemas.http || symbolToOpen.location.uri.scheme === Schemas.https) { + await this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true }); + } + + // Otherwise open as editor + else { + await this.editorService.openEditor({ + resource: symbolToOpen.location.uri, + options: { + preserveFocus: options?.preserveFocus, + pinned: keyMods.alt || this.configuration.openEditorPinned, + selection: symbolToOpen.location.range ? Range.collapseToStart(symbolToOpen.location.range) : undefined + } + }, keyMods.ctrlCmd || options?.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP); + } + } + + private compareSymbols(symbolA: ISymbolsQuickPickItem, symbolB: ISymbolsQuickPickItem): number { + + // By score + if (symbolA.score && symbolB.score) { + if (symbolA.score[0] > symbolB.score[0]) { + return -1; + } else if (symbolA.score[0] < symbolB.score[0]) { + return 1; + } + } + + // By name + const symbolAName = symbolA.symbol.name.toLowerCase(); + const symbolBName = symbolB.symbol.name.toLowerCase(); + const res = symbolAName.localeCompare(symbolBName); + if (res !== 0) { + return res; + } + + // By kind + const symbolAKind = SymbolKinds.toCssClassName(symbolA.symbol.kind); + const symbolBKind = SymbolKinds.toCssClassName(symbolB.symbol.kind); + return symbolAKind.localeCompare(symbolBKind); + } +} diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index a2eb0ba8458..1b212bb21cb 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -18,6 +18,8 @@ import { IFileMatch } from 'vs/workbench/services/search/common/search'; import { ReplaceAction } from 'vs/workbench/contrib/search/browser/searchActions'; import { FileMatch, FileMatchOrMatch, Match } from 'vs/workbench/contrib/search/common/searchModel'; import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; suite('Search Actions', () => { @@ -153,6 +155,7 @@ suite('Search Actions', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index c8f8bbbccac..39499ce4639 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -15,6 +15,8 @@ import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { isWindows } from 'vs/base/common/platform'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; @@ -105,6 +107,7 @@ suite('Search - Viewlet', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index 4bd8e74e94e..545d7de9943 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -19,6 +19,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; import * as process from 'vs/base/common/process'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; const nullEvent = new class { id: number = -1; @@ -328,6 +330,7 @@ suite('SearchModel', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } diff --git a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts index 82e8ecc9fdd..f46f158ac29 100644 --- a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts @@ -16,6 +16,8 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; const lineOneRange = new OneLineRange(1, 0, 1); @@ -352,6 +354,7 @@ suite('SearchResult', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index e74e02bdbab..d9de089e8c4 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/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index ffeb8859239..58ccc008066 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -39,6 +39,8 @@ import { AbstractTaskService, ConfigureTaskAction } from 'vs/workbench/contrib/t import { tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { TasksQuickAccessProvider } from 'vs/workbench/contrib/tasks/browser/tasksQuickAccess'; let tasksCategory = nls.localize('tasksCategory', "Tasks"); @@ -274,6 +276,17 @@ quickOpenRegistry.registerQuickOpenHandler( ) ); +// Register Quick Access +const quickAccessRegistry = (Registry.as(QuickAccessExtensions.Quickaccess)); + +quickAccessRegistry.registerQuickAccessProvider({ + ctor: TasksQuickAccessProvider, + prefix: TasksQuickAccessProvider.PREFIX, + contextKey: tasksPickerContextKey, + placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a task to run."), + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task"), needsEditor: false }] +}); + const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionContributor); diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts new file mode 100644 index 00000000000..6d157f944fe --- /dev/null +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +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 { + + static PREFIX = 'task '; + + private activationPromise: Promise; + + constructor( + @IExtensionService extensionService: IExtensionService, + @ITaskService private taskService: ITaskService + ) { + super(TasksQuickAccessProvider.PREFIX); + + this.activationPromise = extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask'); + } + + protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + + // always await extensions + await this.activationPromise; + + if (token.isCancellationRequested) { + return []; + } + + // Resolve custom and contributed tasks + const tasks = (await this.taskService.tasks()) + .filter((task): task is CustomTask | ContributedTask => ContributedTask.is(task) || CustomTask.is(task)); + + if (token.isCancellationRequested) { + return []; + } + + this.taskService.migrateRecentTasks(tasks); + + // Split up tasks across recently used, configured and detected + const recentlyUsedTasks = this.taskService.getRecentlyUsedTasks(); + const recent: Array = []; + const configured: CustomTask[] = []; + const detected: ContributedTask[] = []; + const taskMap: IStringDictionary = Object.create(null); + for (const task of tasks) { + const key = task.getRecentlyUsedKey(); + if (key) { + taskMap[key] = task; + } + } + for (const key of recentlyUsedTasks.keys()) { + const task = taskMap[key]; + if (task) { + recent.push(task); + } + } + for (const task of tasks) { + const key = task.getRecentlyUsedKey(); + if (!key || !recentlyUsedTasks.has(key)) { + if (CustomTask.is(task)) { + configured.push(task); + } else { + detected.push(task); + } + } + } + + const taskPicks: Array = []; + const sorter = this.taskService.createSorter(); + + // Fill picks in sorted order + + this.fillPicks(taskPicks, filter, recent, localize('recentlyUsed', "recently used tasks")); + + configured.sort((a, b) => sorter.compare(a, b)); + this.fillPicks(taskPicks, filter, configured, localize('configured', "configured tasks")); + + detected.sort((a, b) => sorter.compare(a, b)); + this.fillPicks(taskPicks, filter, detected, localize('detected', "detected tasks")); + + return taskPicks; + } + + private fillPicks(taskPicks: Array, input: string, tasks: Array, groupLabel: string): void { + let first = true; + for (const task of tasks) { + const highlights = matchesFuzzy(input, task._label); + if (!highlights) { + continue; + } + if (first) { + first = false; + taskPicks.push({ type: 'separator', label: groupLabel }); + } + taskPicks.push({ + label: task._label, + ariaLabel: localize('entryAriaLabel', "{0}, tasks picker", task._label), + description: this.taskService.getTaskDescription(task), + highlights: { label: highlights }, + buttons: (() => { + const buttons = []; + + if (ContributedTask.is(task) || CustomTask.is(task)) { + buttons.push({ + iconClass: 'codicon-gear', + tooltip: localize('customizeTask', "Configure Task") + }); + } + + return buttons; + })(), + trigger: () => { + if (ContributedTask.is(task)) { + this.taskService.customize(task, undefined, true); + } else { + this.taskService.openConfig(task); + } + + return TriggerAction.CLOSE_PICKER; + }, + accept: () => { + this.taskService.run(task, { attachProblemMatcher: true }); + } + }); + } + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts new file mode 100644 index 00000000000..e73900ed0a0 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPickSeparator, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; + +export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { + + static PREFIX = 'term '; + + constructor( + @ITerminalService private readonly terminalService: ITerminalService, + @ICommandService private readonly commandService: ICommandService, + ) { + super(TerminalQuickAccessProvider.PREFIX); + } + + protected configure(picker: IQuickPick): void { + + // Allow to open terminals in background without closing picker + picker.canAcceptInBackground = true; + } + + protected getPicks(filter: string): Array { + const terminalPicks: Array = []; + + const terminalTabs = this.terminalService.terminalTabs; + for (let tabIndex = 0; tabIndex < terminalTabs.length; tabIndex++) { + const terminalTab = terminalTabs[tabIndex]; + for (let terminalIndex = 0; terminalIndex < terminalTab.terminalInstances.length; terminalIndex++) { + const terminal = terminalTab.terminalInstances[terminalIndex]; + const label = `${tabIndex + 1}.${terminalIndex + 1}: ${terminal.title}`; + + const highlights = matchesFuzzy(filter, label, true); + if (highlights) { + terminalPicks.push({ + label, + ariaLabel: localize('termEntryAriaLabel', "{0}, terminal picker", label), + highlights: { label: highlights }, + buttons: [ + { + iconClass: 'codicon-gear', + tooltip: localize('renameTerminal', "Rename Terminal") + }, + { + iconClass: 'codicon-trash', + tooltip: localize('killTerminal', "Kill Terminal Instance") + } + ], + trigger: buttonIndex => { + switch (buttonIndex) { + case 0: + this.commandService.executeCommand(TERMINAL_COMMAND_ID.RENAME, terminal); + return TriggerAction.NO_ACTION; + case 1: + terminal.dispose(true); + return TriggerAction.REFRESH_PICKER; + } + + return TriggerAction.NO_ACTION; + }, + accept: (keyMod, event) => { + this.terminalService.setActiveInstance(terminal); + this.terminalService.showPanel(!event.inBackground); + } + }); + } + } + } + + if (terminalPicks.length > 0) { + terminalPicks.push({ type: 'separator' }); + } + + const createTerminalLabel = localize("workbench.action.terminal.newplus", "Create New Integrated Terminal"); + terminalPicks.push({ + label: `$(plus) ${createTerminalLabel}`, + ariaLabel: localize('termEntryAriaLabel', "{0}, terminal picker", createTerminalLabel), + accept: () => this.commandService.executeCommand('workbench.action.terminal.new') + }); + + return terminalPicks; + + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index a355f69febd..8b354bf3d01 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -39,6 +39,8 @@ import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminaQuickAccess'; registerSingleton(ITerminalService, TerminalService, true); @@ -60,6 +62,16 @@ quickOpenRegistry.registerQuickOpenHandler( ) ); +const quickAccessRegistry = (Registry.as(QuickAccessExtensions.Quickaccess)); + +quickAccessRegistry.registerQuickAccessProvider({ + ctor: TerminalQuickAccessProvider, + prefix: TerminalQuickAccessProvider.PREFIX, + contextKey: inTerminalsPicker, + placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a terminal to open."), + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Opened Terminals"), needsEditor: false }] +}); + const quickOpenNavigateNextInTerminalPickerId = 'workbench.action.quickOpenNavigateNextInTerminalPicker'; CommandsRegistry.registerCommand( { id: quickOpenNavigateNextInTerminalPickerId, handler: getQuickNavigateHandler(quickOpenNavigateNextInTerminalPickerId, true) }); @@ -282,8 +294,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - 'terminal.integrated.allowMenubarMnemonics': { - markdownDescription: nls.localize('terminal.integrated.allowMenubarMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true."), + 'terminal.integrated.allowMnemonics': { + markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true."), type: 'boolean', default: false }, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index bac468cd33a..e28e9e597e5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -131,6 +131,14 @@ export interface ITerminalService { findNext(): void; findPrevious(): void; + /** + * Link handlers can be registered here to allow intercepting links clicked in the terminal. + * When a link is clicked, the link will be considered handled when the first interceptor + * resolves with true. It will be considered not handled when _all_ link handlers resolve with + * false, or 3 seconds have elapsed. + */ + addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable; + selectDefaultWindowsShell(): Promise; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; @@ -179,6 +187,18 @@ export enum WindowsShellType { } export type TerminalShellType = WindowsShellType | undefined; +export const LINK_INTERCEPT_THRESHOLD = 3000; + +export interface ITerminalBeforeHandleLinkEvent { + terminal?: ITerminalInstance; + /** The text of the link */ + link: string; + /** Call with whether the link was handled by the interceptor */ + resolve(wasHandled: boolean): void; +} + +export type TerminalLinkHandlerCallback = (e: ITerminalBeforeHandleLinkEvent) => Promise; + export interface ITerminalInstance { /** * The ID of the terminal instance, this is an arbitrary number only used to identify the @@ -240,6 +260,11 @@ export interface ITerminalInstance { */ onExit: Event; + /** + * Attach a listener to intercept and handle link clicks in the terminal. + */ + onBeforeHandleLink: Event; + readonly exitCode: number | undefined; processReady: Promise; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c79d87d2150..6b4d0453bc1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -30,7 +30,7 @@ import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGR import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ITerminalInstanceService, ITerminalInstance, TerminalShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; @@ -272,6 +272,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { public get onMaximumDimensionsChanged(): Event { return this._onMaximumDimensionsChanged.event; } private readonly _onFocus = new Emitter(); public get onFocus(): Event { return this._onFocus.event; } + private readonly _onBeforeHandleLink = new Emitter(); + public get onBeforeHandleLink(): Event { return this._onBeforeHandleLink.event; } public constructor( private readonly _terminalFocusContextKey: IContextKey, @@ -523,6 +525,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); } this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, xterm, this._processManager, this._configHelper); + this._linkHandler.onBeforeHandleLink(e => { + e.terminal = this; + this._onBeforeHandleLink.fire(e); + }); }); this._commandTrackerAddon = new CommandTrackerAddon(); @@ -615,7 +621,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Skip processing by xterm.js of keyboard events that match menu bar mnemonics - if (this._configHelper.config.allowMenubarMnemonics && event.altKey) { + if (this._configHelper.config.allowMnemonics && event.altKey) { return false; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index 18169644b84..c323e7923ae 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -16,9 +16,11 @@ import { IFileService } from 'vs/platform/files/common/files'; import { Terminal, ILinkMatcherOptions, IViewportRange } from 'xterm'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { posix, win32 } from 'vs/base/common/path'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal'; import { OperatingSystem, isMacintosh } from 'vs/base/common/platform'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; const pathPrefix = '(\\.\\.?|\\~)'; const pathSeparatorClause = '\\/'; @@ -58,7 +60,7 @@ const CUSTOM_LINK_PRIORITY = -1; /** Lowest */ const LOCAL_LINK_PRIORITY = -2; -export type XtermLinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; +export type XtermLinkMatcherHandler = (event: MouseEvent, link: string) => Promise; export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; interface IPath { @@ -66,14 +68,28 @@ interface IPath { normalize(path: string): string; } -export class TerminalLinkHandler { - private readonly _hoverDisposables = new DisposableStore(); +export class TerminalLinkHandler extends DisposableStore { private _widgetManager: TerminalWidgetManager | undefined; private _processCwd: string | undefined; private _gitDiffPreImagePattern: RegExp; private _gitDiffPostImagePattern: RegExp; private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void; private readonly _leaveCallback: () => void; + private _hasBeforeHandleLinkListeners = false; + + protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD; + public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD; + + private readonly _onBeforeHandleLink = this.add(new Emitter({ + onFirstListenerAdd: () => this._hasBeforeHandleLinkListeners = true, + onLastListenerRemove: () => this._hasBeforeHandleLinkListeners = false + })); + /** + * Allows intercepting links and handling them outside of the default link handler. When fired + * the listener has a set amount of time to handle the link or the default handler will fire. + * This was designed to only be handled by a single listener. + */ + public get onBeforeHandleLink(): Event { return this._onBeforeHandleLink.event; } constructor( private _xterm: Terminal, @@ -83,8 +99,11 @@ export class TerminalLinkHandler { @IEditorService private readonly _editorService: IEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @ILogService private readonly _logService: ILogService ) { + super(); + // Matches '--- a/src/file1', capturing 'src/file1' in group 1 this._gitDiffPreImagePattern = /^--- a\/(\S*)/; // Matches '+++ b/src/file1', capturing 'src/file1' in group 1 @@ -211,19 +230,40 @@ export class TerminalLinkHandler { this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options); } - public dispose(): void { - this._hoverDisposables.dispose(); - } - - private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler { - return (event: MouseEvent, uri: string) => { + protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler { + return async (event: MouseEvent, link: string) => { // Prevent default electron link handling so Alt+Click mode works normally event.preventDefault(); // Require correct modifier on click if (!this._isLinkActivationModifierDown(event)) { - return false; + return; } - return handler(uri); + + // Allow the link to be intercepted if there are listeners + if (this._hasBeforeHandleLinkListeners) { + const wasHandled = await new Promise(r => { + const timeoutId = setTimeout(() => { + canceled = true; + this._logService.error('An extension intecepted a terminal link but did not return'); + r(false); + }, TerminalLinkHandler.LINK_INTERCEPT_THRESHOLD); + let canceled = false; + const resolve = (handled: boolean) => { + if (!canceled) { + clearTimeout(timeoutId); + r(handled); + } + }; + this._onBeforeHandleLink.fire({ link, resolve }); + }); + if (!wasHandled) { + handler(link); + } + return; + } + + // Just call the handler if there is no before listener + handler(link); }; } @@ -244,18 +284,17 @@ export class TerminalLinkHandler { return this._gitDiffPostImagePattern; } - private _handleLocalLink(link: string): PromiseLike { - return this._resolvePath(link).then(resolvedLink => { - if (!resolvedLink) { - return Promise.resolve(null); - } - const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link); - const selection: ITextEditorSelection = { - startLineNumber: lineColumnInfo.lineNumber, - startColumn: lineColumnInfo.columnNumber - }; - return this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } }); - }); + private async _handleLocalLink(link: string): Promise { + const resolvedLink = await this._resolvePath(link); + if (!resolvedLink) { + return; + } + const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link); + const selection: ITextEditorSelection = { + startLineNumber: lineColumnInfo.lineNumber, + startColumn: lineColumnInfo.columnNumber + }; + await this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } }); } private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void { @@ -270,7 +309,7 @@ export class TerminalLinkHandler { this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) }); } - private _isLinkActivationModifierDown(event: MouseEvent): boolean { + protected _isLinkActivationModifierDown(event: MouseEvent): boolean { const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); if (editorConf.multiCursorModifier === 'ctrlCmd') { return !!event.altKey; @@ -346,19 +385,19 @@ export class TerminalLinkHandler { return link; } - private _resolvePath(link: string): PromiseLike { + private async _resolvePath(link: string): Promise { if (!this._processManager) { throw new Error('Process manager is required'); } const preprocessedLink = this._preprocessPath(link); if (!preprocessedLink) { - return Promise.resolve(null); + return undefined; } const linkUrl = this.extractLinkUrl(preprocessedLink); if (!linkUrl) { - return Promise.resolve(null); + return undefined; } try { @@ -373,18 +412,20 @@ export class TerminalLinkHandler { uri = URI.file(linkUrl); } - return this._fileService.resolve(uri).then(stat => { + try { + const stat = await this._fileService.resolve(uri); if (stat.isDirectory) { - return null; + return undefined; } return uri; - }).catch(() => { + } + catch (e) { // Does not exist - return null; - }); + return undefined; + } } catch { // Errors in parsing the path - return Promise.resolve(null); + return undefined; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 2dfb87a8d90..cbf22e4c7d2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -15,7 +15,7 @@ import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; -import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, TerminalLinkHandlerCallback, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; @@ -30,6 +30,7 @@ import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; import { find } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { IViewsService } from 'vs/workbench/common/views'; +import { IDisposable } from 'vs/base/common/lifecycle'; interface IExtHostReadyEntry { promise: Promise; @@ -50,6 +51,7 @@ export class TerminalService implements ITerminalService { private _findState: FindReplaceState; private _extHostsReady: { [authority: string]: IExtHostReadyEntry | undefined } = {}; private _activeTabIndex: number; + private _linkHandlers: { [key: string]: TerminalLinkHandlerCallback } = {}; public get activeTabIndex(): number { return this._activeTabIndex; } public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } @@ -411,6 +413,50 @@ export class TerminalService implements ITerminalService { instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance))); instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance))); instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); + instance.addDisposable(instance.onBeforeHandleLink(async e => { + // No link handlers have been registered + const keys = Object.keys(this._linkHandlers); + if (keys.length === 0) { + e.resolve(false); + return; + } + + // Fire each link interceptor and wait for either a true, all false or the cancel time + let resolved = false; + const promises: Promise[] = []; + const timeout = setTimeout(() => { + resolved = true; + e.resolve(false); + }, LINK_INTERCEPT_THRESHOLD); + for (let i = 0; i < keys.length; i++) { + const p = this._linkHandlers[keys[i]](e); + p.then(handled => { + if (!resolved && handled) { + resolved = true; + clearTimeout(timeout); + e.resolve(true); + } + }); + promises.push(p); + } + await Promise.all(promises); + if (!resolved) { + resolved = true; + clearTimeout(timeout); + e.resolve(false); + } + })); + } + + public addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable { + this._linkHandlers[key] = callback; + return { + dispose: () => { + if (this._linkHandlers[key] === callback) { + delete this._linkHandlers[key]; + } + } + }; } private _getTabForInstance(instance: ITerminalInstance): ITerminalTab | undefined { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 4b9abc01604..0d2d6cc069d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -107,7 +107,7 @@ export interface ITerminalConfiguration { scrollback: number; commandsToSkipShell: string[]; allowChords: boolean; - allowMenubarMnemonics: boolean; + allowMnemonics: boolean; cwd: string; confirmOnExit: boolean; enableBell: boolean; diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalLinkHandler.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalLinkHandler.test.ts index dcfcf378d02..f817d6bb400 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalLinkHandler.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalLinkHandler.test.ts @@ -5,11 +5,12 @@ import * as assert from 'assert'; import { OperatingSystem } from 'vs/base/common/platform'; -import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler'; +import { TerminalLinkHandler, LineColumnInfo, XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler'; import * as strings from 'vs/base/common/strings'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { Event } from 'vs/base/common/event'; import { ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; class TestTerminalLinkHandler extends TerminalLinkHandler { public get localLinkRegex(): RegExp { @@ -24,6 +25,13 @@ class TestTerminalLinkHandler extends TerminalLinkHandler { public preprocessPath(link: string): string | null { return this._preprocessPath(link); } + protected _isLinkActivationModifierDown(event: MouseEvent): boolean { + return true; + } + public wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler { + TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD = 0; + return this._wrapLinkHandler(handler); + } } class TestXterm { @@ -81,7 +89,7 @@ suite('Workbench - TerminalLinkHandler', () => { const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Windows, userHome: '' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) { assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl); assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl); @@ -157,7 +165,7 @@ suite('Workbench - TerminalLinkHandler', () => { const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Linux, userHome: '' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) { assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl); assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl); @@ -225,7 +233,7 @@ suite('Workbench - TerminalLinkHandler', () => { const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Windows, userHome: 'C:\\Users\\Me' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); linkHandler.processCwd = 'C:\\base'; assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1'); @@ -238,7 +246,7 @@ suite('Workbench - TerminalLinkHandler', () => { const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Windows, userHome: 'C:\\Users\\M e' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); linkHandler.processCwd = 'C:\\base dir'; assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1'); @@ -252,7 +260,7 @@ suite('Workbench - TerminalLinkHandler', () => { const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Linux, userHome: '/home/me' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); linkHandler.processCwd = '/base'; assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1'); @@ -265,7 +273,7 @@ suite('Workbench - TerminalLinkHandler', () => { const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Linux, userHome: '/home/me' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); assert.equal(linkHandler.preprocessPath('./src/file1'), null); assert.equal(linkHandler.preprocessPath('src/file2'), null); @@ -279,7 +287,7 @@ suite('Workbench - TerminalLinkHandler', () => { const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { os: OperatingSystem.Linux, userHome: '' - } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!); + } as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!); function assertAreGoodMatches(matches: RegExpMatchArray | null) { if (matches) { @@ -302,4 +310,35 @@ suite('Workbench - TerminalLinkHandler', () => { assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null'), false); assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null '), false); }); + + suite('wrapLinkHandler', () => { + const nullMouseEvent: any = Object.freeze({ preventDefault: () => { } }); + + test('should allow intercepting of links with onBeforeHandleLink', async () => { + const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, { + os: OperatingSystem.Linux, + userHome: '' + } as any, testConfigHelper, null!, null!, new TestConfigurationService(), new MockTerminalInstanceService(), null!, null!); + linkHandler.onBeforeHandleLink(e => { + if (e.link === 'https://www.microsoft.com') { + intercepted = true; + e.resolve(true); + } + e.resolve(false); + }); + const wrappedHandler = linkHandler.wrapLinkHandler(() => defaultHandled = true); + + let defaultHandled = false; + let intercepted = false; + await wrappedHandler(nullMouseEvent, 'https://www.visualstudio.com'); + assert.equal(intercepted, false); + assert.equal(defaultHandled, true); + + defaultHandled = false; + intercepted = false; + await wrappedHandler(nullMouseEvent, 'https://www.microsoft.com'); + assert.equal(intercepted, true); + assert.equal(defaultHandled, false); + }); + }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index 7862e19ba67..48c52ffb3a8 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -21,8 +21,8 @@ function getMockTheme(type: ThemeType): IColorTheme { getColor: (colorId: ColorIdentifier): Color | undefined => themingRegistry.resolveDefaultColor(colorId, theme), defines: () => true, getTokenStyleMetadata: () => undefined, - tokenColorMap: [] - + tokenColorMap: [], + semanticHighlighting: false }; return theme; } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index b96337ce6f5..587bda235a2 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -24,7 +24,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ITimelineService, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvidersChangeEvent, TimelineRequest, Timeline, TimelinePaneId } from 'vs/workbench/contrib/timeline/common/timeline'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IThemeService, LIGHT, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { basename } from 'vs/base/common/path'; @@ -34,7 +34,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, ActionRunner } from 'vs/base/common/actions'; import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction, IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuItemAction, IMenuService, MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { fromNow } from 'vs/base/common/date'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -403,7 +403,7 @@ export class TimelinePane extends ViewPane { private async handleRequest(request: TimelineRequest) { let timeline: Timeline | undefined; try { - timeline = await this.progressService.withProgress({ location: this.getProgressLocation() }, () => request.result); + timeline = await this.progressService.withProgress({ location: this.id }, () => request.result); } finally { this._pendingRequests.delete(request.source); @@ -932,38 +932,35 @@ class TimelinePaneCommands extends Disposable { } })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: 'timeline.toggleFollowActiveEditor', - title: { value: localize('timeline.toggleFollowActiveEditorCommand', "Toggle Active Editor Following"), original: 'Toggle Active Editor Following' }, - category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, - menu: [{ - id: MenuId.TimelineTitle, - command: { - // title: localize(`timeline.toggleFollowActiveEditorCommand.stop`, "Stop following the Active Editor"), - icon: { id: 'codicon/eye' } - }, - group: 'navigation', - order: 98, - when: TimelineFollowActiveEditorContext - }, - { - id: MenuId.TimelineTitle, - command: { - // title: localize(`ToggleFollowActiveEditorCommand.follow`, "Follow the Active Editor"), - icon: { id: 'codicon/eye-closed' } - }, - group: 'navigation', - order: 98, - when: TimelineFollowActiveEditorContext.toNegated() - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - pane.followActiveEditor = !pane.followActiveEditor; - } - })); + this._register(CommandsRegistry.registerCommand('timeline.toggleFollowActiveEditor', + (accessor: ServicesAccessor, ...args: any[]) => pane.followActiveEditor = !pane.followActiveEditor + )); + + this._register(MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({ + command: { + id: 'timeline.toggleFollowActiveEditor', + title: { value: localize('timeline.toggleFollowActiveEditorCommand', "Toggle Active Editor Following"), original: 'Toggle Active Editor Following' }, + // title: localize(`timeline.toggleFollowActiveEditorCommand.stop`, "Stop following the Active Editor"), + icon: { id: 'codicon/eye' }, + category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + }, + group: 'navigation', + order: 98, + when: TimelineFollowActiveEditorContext + }))); + + this._register(MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({ + command: { + id: 'timeline.toggleFollowActiveEditor', + title: { value: localize('timeline.toggleFollowActiveEditorCommand', "Toggle Active Editor Following"), original: 'Toggle Active Editor Following' }, + // title: localize(`timeline.toggleFollowActiveEditorCommand.stop`, "Stop following the Active Editor"), + icon: { id: 'codicon/eye-closed' }, + category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + }, + group: 'navigation', + order: 98, + when: TimelineFollowActiveEditorContext.toNegated() + }))); this._register(timelineService.onDidChangeProviders(() => this.updateTimelineSourceFilters())); this.updateTimelineSourceFilters(); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index d7f3ea4d80d..c4a99be9b0b 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT, toRemoteSyncResourceFromSource, PREVIEW_QUERY, resolveSyncResource, getSyncSourceFromResourceKey } from 'vs/platform/userDataSync/common/userDataSync'; +import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT, PREVIEW_QUERY, resolveSyncResource, toRemoteSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -60,14 +60,14 @@ const enum AuthStatus { const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthStatus.Initializing); const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); -type ConfigureSyncQuickPickItem = { id: ResourceKey, label: string, description?: string }; +type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string }; -function getSyncAreaLabel(source: SyncSource): string { +function getSyncAreaLabel(source: SyncResource): string { switch (source) { - case SyncSource.Settings: return localize('settings', "Settings"); - case SyncSource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); - case SyncSource.Extensions: return localize('extensions', "Extensions"); - case SyncSource.GlobalState: return localize('ui state label', "UI State"); + case SyncResource.Settings: return localize('settings', "Settings"); + case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); + case SyncResource.Extensions: return localize('extensions', "Extensions"); + case SyncResource.GlobalState: return localize('ui state label', "UI State"); } } @@ -283,8 +283,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateBadge(); } - private readonly conflictsDisposables = new Map(); - private onDidChangeConflicts(conflicts: SyncSource[]) { + private readonly conflictsDisposables = new Map(); + private onDidChangeConflicts(conflicts: SyncResource[]) { this.updateBadge(); if (conflicts.length) { this.conflictsSources.set(this.userDataSyncService.conflictsSources.join(',')); @@ -352,22 +352,22 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async acceptRemote(syncSource: SyncSource) { + private async acceptRemote(syncResource: SyncResource) { try { - const contents = await this.userDataSyncService.resolveContent(toRemoteSyncResourceFromSource(syncSource).with({ query: PREVIEW_QUERY })); + const contents = await this.userDataSyncService.resolveContent(toRemoteSyncResource(syncResource).with({ query: PREVIEW_QUERY })); if (contents) { - await this.userDataSyncService.accept(syncSource, contents); + await this.userDataSyncService.accept(syncResource, contents); } } catch (e) { this.notificationService.error(e); } } - private async acceptLocal(syncSource: SyncSource): Promise { + private async acceptLocal(syncSource: SyncResource): Promise { try { - const previewResource = syncSource === SyncSource.Settings + const previewResource = syncSource === SyncResource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource - : syncSource === SyncSource.Keybindings + : syncSource === SyncResource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource : null; if (previewResource) { @@ -415,15 +415,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); return; case UserDataSyncErrorCode.TooLarge: - if (error.source === SyncSource.Keybindings || error.source === SyncSource.Settings) { - this.disableSync(error.source); - const sourceArea = getSyncAreaLabel(error.source); + if (error.resource === SyncResource.Keybindings || error.resource === SyncResource.Settings) { + this.disableSync(error.resource); + const sourceArea = getSyncAreaLabel(error.resource); this.notificationService.notify({ severity: Severity.Error, message: localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), actions: { primary: [new Action('open sync file', localize('open file', "Open {0} File", sourceArea), undefined, true, - () => error.source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] + () => error.resource === SyncResource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] } }); } @@ -438,8 +438,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private readonly invalidContentErrorDisposables = new Map(); - private onSyncErrors(errors: [SyncSource, UserDataSyncError][]): void { + private readonly invalidContentErrorDisposables = new Map(); + private onSyncErrors(errors: [SyncResource, UserDataSyncError][]): void { if (errors.length) { for (const [source, error] of errors) { switch (error.code) { @@ -460,14 +460,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private handleInvalidContentError(source: SyncSource): void { + private handleInvalidContentError(source: SyncResource): void { if (this.invalidContentErrorDisposables.has(source)) { return; } - if (source !== SyncSource.Settings && source !== SyncSource.Keybindings) { + if (source !== SyncResource.Settings && source !== SyncResource.Keybindings) { return; } - const resource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsResource : this.workbenchEnvironmentService.keybindingsResource; + const resource = source === SyncResource.Settings ? this.workbenchEnvironmentService.settingsResource : this.workbenchEnvironmentService.keybindingsResource; if (isEqual(resource, toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }))) { // Do not show notification if the file in error is active return; @@ -478,7 +478,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: localize('errorInvalidConfiguration', "Unable to sync {0} because there are some errors/warnings in the file. Please open the file to correct errors/warnings in it.", errorArea.toLowerCase()), actions: { primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true, - () => source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] + () => source === SyncResource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] } }); this.invalidContentErrorDisposables.set(source, toDisposable(() => { @@ -602,17 +602,17 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { return [{ - id: 'settings', - label: getSyncAreaLabel(SyncSource.Settings) + id: SyncResource.Settings, + label: getSyncAreaLabel(SyncResource.Settings) }, { - id: 'keybindings', - label: getSyncAreaLabel(SyncSource.Keybindings) + id: SyncResource.Keybindings, + label: getSyncAreaLabel(SyncResource.Keybindings) }, { - id: 'extensions', - label: getSyncAreaLabel(SyncSource.Extensions) + id: SyncResource.Extensions, + label: getSyncAreaLabel(SyncResource.Extensions) }, { - id: 'globalState', - label: getSyncAreaLabel(SyncSource.GlobalState), + id: SyncResource.GlobalState, + label: getSyncAreaLabel(SyncResource.GlobalState), description: localize('ui state description', "only 'Display Language' for now") }]; } @@ -707,15 +707,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private disableSync(source?: SyncSource): void { + private disableSync(source?: SyncResource): void { if (source === undefined) { this.userDataSyncEnablementService.setEnablement(false); } else { switch (source) { - case SyncSource.Settings: return this.userDataSyncEnablementService.setResourceEnablement('settings', false); - case SyncSource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement('keybindings', false); - case SyncSource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement('extensions', false); - case SyncSource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement('globalState', false); + case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false); + case SyncResource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Keybindings, false); + case SyncResource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false); + case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, false); } } } @@ -729,9 +729,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private getConflictsEditorInput(source: SyncSource): IEditorInput | undefined { - const previewResource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource - : source === SyncSource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource + private getConflictsEditorInput(source: SyncResource): IEditorInput | undefined { + const previewResource = source === SyncResource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource + : source === SyncResource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource : null; return previewResource ? this.editorService.editors.filter(input => input instanceof DiffEditorInput && isEqual(previewResource, input.master.resource))[0] : undefined; } @@ -743,18 +743,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } - private async handleConflicts(source: SyncSource): Promise { + private async handleConflicts(resource: SyncResource): Promise { let previewResource: URI | undefined = undefined; let label: string = ''; - if (source === SyncSource.Settings) { + if (resource === SyncResource.Settings) { previewResource = this.workbenchEnvironmentService.settingsSyncPreviewResource; label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); - } else if (source === SyncSource.Keybindings) { + } else if (resource === SyncResource.Keybindings) { previewResource = this.workbenchEnvironmentService.keybindingsSyncPreviewResource; label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); } if (previewResource) { - const remoteContentResource = toRemoteSyncResourceFromSource(source).with({ query: PREVIEW_QUERY }); + const remoteContentResource = toRemoteSyncResource(resource).with({ query: PREVIEW_QUERY }); await this.editorService.openEditor({ leftResource: remoteContentResource, rightResource: previewResource, @@ -846,7 +846,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerShowSettingsConflictsAction(): void { const resolveSettingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i); - CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Settings)); + CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleConflicts(SyncResource.Settings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { @@ -873,7 +873,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerShowKeybindingsConflictsAction(): void { const resolveKeybindingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*keybindings.*/i); - CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Keybindings)); + CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleConflicts(SyncResource.Keybindings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { @@ -934,10 +934,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (that.userDataSyncService.conflictsSources.length) { for (const source of that.userDataSyncService.conflictsSources) { switch (source) { - case SyncSource.Settings: + case SyncResource.Settings: items.push({ id: resolveSettingsConflictsCommand.id, label: resolveSettingsConflictsCommand.title }); break; - case SyncSource.Keybindings: + case SyncResource.Keybindings: items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title }); break; } @@ -1130,7 +1130,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { - const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || getSyncSourceFromResourceKey(resolveSyncResource(model.uri)!.resourceKey))!; + const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || resolveSyncResource(model.uri)!.resource)!; this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); const syncAreaLabel = getSyncAreaLabel(conflictsSource); const result = await this.dialogService.confirm({ diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index b189b51863d..be02cbc7149 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_RESOURCE_KEYS, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteSyncResource, resolveSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, ResourceKey, toLocalBackupSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteSyncResource, resolveSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, toLocalBackupSyncResource, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; @@ -30,8 +30,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { @IUserDataSyncBackupStoreService private readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, ) { const container = this.registerSyncViewContainer(); - // Disable remote backup view until server is upgraded. - // this.registerBackupView(container, true); + this.registerBackupView(container, true); this.registerBackupView(container, false); } @@ -61,8 +60,8 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { if (visible && !treeView.dataProvider) { disposable.dispose(); treeView.dataProvider = this.instantiationService.createInstance(UserDataSyncHistoryViewDataProvider, id, - (resourceKey: ResourceKey) => remote ? this.userDataSyncStoreService.getAllRefs(resourceKey) : this.userDataSyncBackupStoreService.getAllRefs(resourceKey), - (resourceKey: ResourceKey, ref: string) => remote ? toRemoteSyncResource(resourceKey, ref) : toLocalBackupSyncResource(resourceKey, ref)); + (resource: SyncResource) => remote ? this.userDataSyncStoreService.getAllRefs(resource) : this.userDataSyncBackupStoreService.getAllRefs(resource), + (resource: SyncResource, ref: string) => remote ? toRemoteSyncResource(resource, ref) : toLocalBackupSyncResource(resource, ref)); } }); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -114,7 +113,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { let resource = URI.parse(handle.$treeItemHandle); const result = resolveSyncResource(resource); if (result) { - resource = resource.with({ fragment: result.resourceKey }); + resource = resource.with({ fragment: result.resource }); await editorService.openEditor({ resource }); } } @@ -152,8 +151,8 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { const resource = URI.parse(handle.$treeItemHandle); const result = resolveSyncResource(resource); if (result) { - const leftResource: URI = resource.with({ fragment: result.resourceKey }); - const rightResource: URI = result.resourceKey === 'settings' ? environmentService.settingsResource : environmentService.keybindingsResource; + const leftResource: URI = resource.with({ fragment: result.resource }); + const rightResource: URI = result.resource === 'settings' ? environmentService.settingsResource : environmentService.keybindingsResource; await editorService.openEditor({ leftResource, rightResource, @@ -174,8 +173,8 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { constructor( private readonly viewId: string, - private getAllRefs: (resourceKey: ResourceKey) => Promise, - private toResource: (resourceKey: ResourceKey, ref: string) => URI + private getAllRefs: (resource: SyncResource) => Promise, + private toResource: (resource: SyncResource, ref: string) => URI ) { } @@ -183,7 +182,7 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { if (element) { return this.getResources(element.handle); } - return ALL_RESOURCE_KEYS.map(resourceKey => ({ + return ALL_SYNC_RESOURCES.map(resourceKey => ({ handle: resourceKey, collapsibleState: TreeItemCollapsibleState.Collapsed, label: { label: resourceKey }, @@ -193,7 +192,7 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { } private async getResources(handle: string): Promise { - const resourceKey = ALL_RESOURCE_KEYS.filter(key => key === handle)[0]; + const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === handle)[0]; if (resourceKey) { const refHandles = await this.getAllRefs(resourceKey); return refHandles.map(({ ref, created }) => { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 22dc38e88e9..dd29d133c52 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -109,7 +109,8 @@ export class WebviewEditor extends BaseEditor { return; } - if (this.webview) { + const alreadyOwnsWebview = input instanceof WebviewInput && input.webview === this.webview; + if (this.webview && !alreadyOwnsWebview) { this.webview.release(this); } @@ -125,7 +126,9 @@ export class WebviewEditor extends BaseEditor { input.updateGroup(this.group.id); } - this.claimWebview(input); + if (!alreadyOwnsWebview) { + this.claimWebview(input); + } if (this._dimension) { this.layout(this._dimension); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 98ca51637c8..3da701349a1 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -18,8 +18,9 @@ export class WebviewInput extends EditorInput { private _iconPath?: WebviewIcons; private _group?: GroupIdentifier; - private readonly _webview: Lazy; - private _didSomeoneTakeMyWebview = false; + private _webview: Lazy; + + private _hasTransfered = false; get resource() { return URI.from({ @@ -42,8 +43,8 @@ export class WebviewInput extends EditorInput { dispose() { if (!this.isDisposed()) { - if (!this._didSomeoneTakeMyWebview) { - this._webview?.rawValue?.dispose(); + if (!this._hasTransfered) { + this._webview.rawValue?.dispose(); } } super.dispose(); @@ -107,11 +108,12 @@ export class WebviewInput extends EditorInput { return false; } - protected takeOwnershipOfWebview(): WebviewOverlay | undefined { - if (this._didSomeoneTakeMyWebview) { + protected transfer(other: WebviewInput): WebviewInput | undefined { + if (this._hasTransfered) { return undefined; } - this._didSomeoneTakeMyWebview = true; - return this.webview; + this._hasTransfered = true; + other._webview = this._webview; + return other; } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index 3ffe992a8e1..b32a09cb805 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -113,11 +113,25 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput { super(id, viewType, name, webview, webviewService); } + #resolved = false; + @memoize public async resolve() { - await this._webviewWorkbenchService.resolveWebview(this); + if (!this.#resolved) { + this.#resolved = true; + await this._webviewWorkbenchService.resolveWebview(this); + } return super.resolve(); } + + protected transfer(other: LazilyResolvedWebviewEditorInput): WebviewInput | undefined { + if (!super.transfer(other)) { + return; + } + + other.#resolved = this.#resolved; + return other; + } } diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index e3d94dd92b4..800f1f03096 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; } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 776821d3cb4..71d4b4dc916 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -251,10 +251,10 @@ export class NativeWindow extends Disposable { const file = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file }); // Represented Filename - this.updateRepresentedFilename(file ? file.fsPath : undefined); + this.updateRepresentedFilename(file?.fsPath); // Custom title menu - this.provideCustomTitleContextMenu(file ? file.fsPath : undefined); + this.provideCustomTitleContextMenu(file?.fsPath); })); } diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 66750a668b8..2d3c0217589 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -10,8 +10,8 @@ import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model'; export const IBackupFileService = createDecorator('backupFileService'); export interface IResolvedBackup { - value: ITextBufferFactory; - meta?: T; + readonly value: ITextBufferFactory; + readonly meta?: T; } /** diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index a334a34a15e..e7414fcbb97 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 bdcff69df4f..7dc7647ffbf 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 36e903296cb..a7c6d54baa4 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 d64573ec6dc..cfac383e8af 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -146,6 +146,12 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } } + if (extensionKind === 'web') { + // Web extensions are not yet supported to be disabled by kind. Enable them always on web. + if (this.extensionManagementServerService.localExtensionManagementServer === null) { + 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 7a4ee7ed3cc..66bdd7eefae 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,38 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true); }); + test('test web extension on local server is 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.DisabledByExtensionKind); + }); + + test('test web extension on remote server is not disabled by kind when there is no local server', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', 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 when there is no local server', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', 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); + }); + + test('test web extension with no server is not disabled by kind when there is no local and remote server', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, null)); + 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 70282328684..32c51b41e3a 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 282f9307e70..74120ad40c3 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -666,7 +666,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, @@ -682,4 +682,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/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 0f35c544319..e35e1736651 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -25,6 +25,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; interface ParsedExtHostArgs { uriTransformerPath?: string; + useHostProxy?: string; } // workaround for https://github.com/microsoft/vscode/issues/85490 @@ -40,7 +41,8 @@ interface ParsedExtHostArgs { const args = minimist(process.argv.slice(2), { string: [ - 'uriTransformerPath' + 'uriTransformerPath', + 'useHostProxy' ] }) as ParsedExtHostArgs; @@ -293,6 +295,7 @@ export async function startExtensionHostProcess(): Promise { const { initData } = renderer; // setup things patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) + initData.environment.useHostProxy = args.useHostProxy !== undefined ? args.useHostProxy !== 'false' : undefined; // host abstraction const hostUtils = new class NodeHost implements IHostUtils { diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 4c72df591f5..ad9308548a7 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -16,7 +16,7 @@ import { endsWith } from 'vs/base/common/strings'; import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { ProxyAgent } from 'vscode-proxy-agent'; -import { MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTelemetryShape, IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; @@ -35,9 +35,10 @@ export function connectProxyResolver( configProvider: ExtHostConfigProvider, extensionService: ExtHostExtensionService, extHostLogService: ILogService, - mainThreadTelemetry: MainThreadTelemetryShape + mainThreadTelemetry: MainThreadTelemetryShape, + initData: IInitData, ) { - const resolveProxy = setupProxyResolution(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry); + const resolveProxy = setupProxyResolution(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry, initData); const lookup = createPatchedModules(configProvider, resolveProxy); return configureModuleLoading(extensionService, lookup); } @@ -48,7 +49,8 @@ function setupProxyResolution( extHostWorkspace: IExtHostWorkspaceProvider, configProvider: ExtHostConfigProvider, extHostLogService: ILogService, - mainThreadTelemetry: MainThreadTelemetryShape + mainThreadTelemetry: MainThreadTelemetryShape, + initData: IInitData, ) { const env = process.env; @@ -139,12 +141,14 @@ function setupProxyResolution( timeout = setTimeout(logEvent, 10 * 60 * 1000); } + const useHostProxy = initData.environment.useHostProxy; + const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; useSystemCertificates(extHostLogService, flags.useSystemCertificates, opts, () => { - useProxySettings(flags.useProxySettings, req, opts, url, callback); + useProxySettings(doUseHostProxy, flags.useProxySettings, req, opts, url, callback); }); } - function useProxySettings(useProxySettings: boolean, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { + function useProxySettings(useHostProxy: boolean, useProxySettings: boolean, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { if (!useProxySettings) { callback('DIRECT'); @@ -192,6 +196,12 @@ function setupProxyResolution( return; } + if (!useHostProxy) { + callback('DIRECT'); + extHostLogService.trace('ProxyResolver#resolveProxy unconfigured', url, 'DIRECT'); + return; + } + const start = Date.now(); extHostWorkspace.resolveProxy(url) // Use full URL to ensure it is an actually used one. .then(proxy => { diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index 7a9048cba8d..b40e932de44 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/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 40e61072372..7e6f234fbf6 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -9,21 +9,19 @@ import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files'; +import { FileChangesEvent, IFileService, FileChangeType } from 'vs/platform/files/common/files'; import { Selection } from 'vs/editor/common/core/selection'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; import { Event } from 'vs/base/common/event'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search'; -import { IExpression } from 'vs/base/common/glob'; +import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -98,8 +96,8 @@ export class HistoryService extends Disposable implements IHistoryService { private readonly activeEditorListeners = this._register(new DisposableStore()); private lastActiveEditor?: IEditorIdentifier; - private readonly editorHistoryListeners: Map = new Map(); - private readonly editorStackListeners: Map = new Map(); + private readonly editorHistoryListeners = new Map(); + private readonly editorStackListeners = new Map(); constructor( @IEditorService private readonly editorService: EditorServiceImpl, @@ -124,7 +122,7 @@ export class HistoryService extends Disposable implements IHistoryService { this._register(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event))); this._register(this.storageService.onWillSaveState(() => this.saveState())); this._register(this.fileService.onDidFilesChange(event => this.onDidFilesChange(event))); - this._register(this.resourceFilter.onExpressionChange(() => this.removeExcludedFromHistory())); + this._register(this.resourceExcludeMatcher.onExpressionChange(() => this.removeExcludedFromHistory())); this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack())); // if the service is created late enough that an editor is already opened @@ -718,17 +716,7 @@ export class HistoryService extends Disposable implements IHistoryService { private history: Array | undefined = undefined; - private readonly resourceFilter = this._register(this.instantiationService.createInstance( - ResourceGlobMatcher, - (root?: URI) => this.getExcludes(root), - (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration('search.exclude') - )); - - private getExcludes(root?: URI): IExpression { - const scope = root ? { resource: root } : undefined; - - return getExcludes(scope ? this.configurationService.getValue(scope) : this.configurationService.getValue())!; - } + private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); private handleEditorEventInHistory(editor?: IEditorPane): void { @@ -764,7 +752,7 @@ export class HistoryService extends Disposable implements IHistoryService { const resourceEditorInput = input as IResourceEditorInput; - return !this.resourceFilter.matches(resourceEditorInput.resource); + return !this.resourceExcludeMatcher.matches(resourceEditorInput.resource); } private removeExcludedFromHistory(): void { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index fc22f855180..1e2b68241c2 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -55,6 +55,8 @@ import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/se import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestTextResourcePropertiesService, TestContextService, TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -107,6 +109,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(IFilesConfigurationService, instantiationService.createInstance(FilesConfigurationService)); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); + instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 41e6b4e34c4..0df88daa425 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -28,6 +28,9 @@ export class BrowserLifecycleService extends AbstractLifecycleService { } private onBeforeUnload(): string | null { + const logService = this.logService; + logService.info('[lifecycle] onBeforeUnload triggered'); + let veto = false; // Before Shutdown @@ -36,7 +39,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { if (value === true) { veto = true; } else if (value instanceof Promise && !veto) { - console.warn(new Error('Long running onBeforeShutdown currently not supported in the web')); + logService.error('[lifecycle] Long running onBeforeShutdown currently not supported in the web'); veto = true; } }, @@ -51,7 +54,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // No Veto: continue with Will Shutdown this._onWillShutdown.fire({ join() { - console.warn(new Error('Long running onWillShutdown currently not supported in the web')); + logService.error('[lifecycle] Long running onWillShutdown currently not supported in the web'); }, reason: ShutdownReason.QUIT }); diff --git a/src/vs/workbench/services/progress/browser/progressIndicator.ts b/src/vs/workbench/services/progress/browser/progressIndicator.ts index dc0b538df55..ebbfd5bc0e4 100644 --- a/src/vs/workbench/services/progress/browser/progressIndicator.ts +++ b/src/vs/workbench/services/progress/browser/progressIndicator.ts @@ -10,6 +10,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IProgressRunner, IProgressIndicator, emptyProgressRunner } from 'vs/platform/progress/common/progress'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IViewsService } from 'vs/workbench/common/views'; export class ProgressBarIndicator extends Disposable implements IProgressIndicator { @@ -153,6 +154,7 @@ export abstract class CompositeScope extends Disposable { constructor( private viewletService: IViewletService, private panelService: IPanelService, + private viewsService: IViewsService, private scopeId: string ) { super(); @@ -161,6 +163,8 @@ export abstract class CompositeScope extends Disposable { } registerListeners(): void { + this._register(this.viewsService.onDidChangeViewVisibility(e => e.visible ? this.onScopeOpened(e.id) : this.onScopeClosed(e.id))); + this._register(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId()))); this._register(this.panelService.onDidPanelOpen(({ panel }) => this.onScopeOpened(panel.getId()))); @@ -195,9 +199,10 @@ export class CompositeProgressIndicator extends CompositeScope implements IProgr scopeId: string, isActive: boolean, @IViewletService viewletService: IViewletService, - @IPanelService panelService: IPanelService + @IPanelService panelService: IPanelService, + @IViewsService viewsService: IViewsService ) { - super(viewletService, panelService, scopeId); + super(viewletService, panelService, viewsService, scopeId); this.progressbar = progressbar; this.isActive = isActive || isUndefinedOrNull(scopeId); // If service is unscoped, enable by default @@ -205,6 +210,8 @@ export class CompositeProgressIndicator extends CompositeScope implements IProgr onScopeDeactivated(): void { this.isActive = false; + + this.progressbar.stop().hide(); } onScopeActivated(): void { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index b96b974ea2d..ed8b9499f69 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -25,6 +25,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventHelper } from 'vs/base/browser/dom'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { parseLinkedText } from 'vs/base/common/linkedText'; +import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; export class ProgressService extends Disposable implements IProgressService { @@ -33,6 +34,8 @@ export class ProgressService extends Disposable implements IProgressService { constructor( @IActivityService private readonly activityService: IActivityService, @IViewletService private readonly viewletService: IViewletService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IViewsService private readonly viewsService: IViewsService, @IPanelService private readonly panelService: IPanelService, @INotificationService private readonly notificationService: INotificationService, @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -54,6 +57,10 @@ export class ProgressService extends Disposable implements IProgressService { return this.withPanelProgress(location, task, { ...options, location }); } + if (this.viewsService.getProgressIndicator(location)) { + return this.withViewProgress(location, task, { ...options, location }); + } + throw new Error(`Bad progress location: ${location}`); } @@ -376,18 +383,38 @@ export class ProgressService extends Disposable implements IProgressService { // show in viewlet const promise = this.withCompositeProgress(this.viewletService.getProgressIndicator(viewletId), task, options); - // show activity bar + // show on activity bar + this.showOnActivityBar(viewletId, options, promise); + + return promise; + } + + private withViewProgress

, R = unknown>(viewId: string, task: (progress: IProgress) => P, options: IProgressCompositeOptions): P { + + // show in viewlet + const promise = this.withCompositeProgress(this.viewsService.getProgressIndicator(viewId), task, options); + + const location = this.viewDescriptorService.getViewLocation(viewId); + if (location !== ViewContainerLocation.Sidebar) { + return promise; + } + + const viewletId = this.viewDescriptorService.getViewContainer(viewId)?.id; + if (viewletId === undefined) { + return promise; + } + + // show on activity bar + this.showOnActivityBar(viewletId, options, promise); + + return promise; + } + + private showOnActivityBar

, R = unknown>(viewletId: string, options: IProgressCompositeOptions, promise: P) { let activityProgress: IDisposable; let delayHandle: any = setTimeout(() => { delayHandle = undefined; - - const handle = this.activityService.showActivity( - viewletId, - new ProgressBadge(() => ''), - 'progress-badge', - 100 - ); - + const handle = this.activityService.showActivity(viewletId, new ProgressBadge(() => ''), 'progress-badge', 100); const startTimeVisible = Date.now(); const minTimeVisible = 300; activityProgress = { @@ -403,13 +430,10 @@ export class ProgressService extends Disposable implements IProgressService { } }; }, options.delay || 300); - promise.finally(() => { clearTimeout(delayHandle); dispose(activityProgress); }); - - return promise; } private withPanelProgress

, R = unknown>(panelid: string, task: (progress: IProgress) => P, options: IProgressCompositeOptions): P { diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index f5f47990d29..6893e8114cd 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -10,9 +10,9 @@ import { CompositeScope, CompositeProgressIndicator } from 'vs/workbench/service import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewlet } from 'vs/workbench/common/viewlet'; -import { TestViewletService, TestPanelService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestViewletService, TestPanelService, TestViewsService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Event } from 'vs/base/common/event'; -import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; +import { IView, IViewPaneContainer, IViewsService } from 'vs/workbench/common/views'; class TestViewlet implements IViewlet { @@ -38,8 +38,8 @@ class TestViewlet implements IViewlet { class TestCompositeScope extends CompositeScope { isActive: boolean = false; - constructor(viewletService: IViewletService, panelService: IPanelService, scopeId: string) { - super(viewletService, panelService, scopeId); + constructor(viewletService: IViewletService, panelService: IPanelService, viewsService: IViewsService, scopeId: string) { + super(viewletService, panelService, viewsService, scopeId); } onScopeActivated() { this.isActive = true; } @@ -106,7 +106,8 @@ suite('Progress Indicator', () => { test('CompositeScope', () => { let viewletService = new TestViewletService(); let panelService = new TestPanelService(); - let service = new TestCompositeScope(viewletService, panelService, 'test.scopeId'); + let viewsService = new TestViewsService(); + let service = new TestCompositeScope(viewletService, panelService, viewsService, 'test.scopeId'); const testViewlet = new TestViewlet('test.scopeId'); assert(!service.isActive); @@ -116,13 +117,19 @@ suite('Progress Indicator', () => { viewletService.onDidViewletCloseEmitter.fire(testViewlet); assert(!service.isActive); + viewsService.onDidChangeViewVisibilityEmitter.fire({ id: 'test.scopeId', visible: true }); + assert(service.isActive); + + viewsService.onDidChangeViewVisibilityEmitter.fire({ id: 'test.scopeId', visible: false }); + assert(!service.isActive); }); test('CompositeProgressIndicator', async () => { let testProgressBar = new TestProgressBar(); let viewletService = new TestViewletService(); let panelService = new TestPanelService(); - let service = new CompositeProgressIndicator((testProgressBar), 'test.scopeId', true, viewletService, panelService); + let viewsService = new TestViewsService(); + let service = new CompositeProgressIndicator((testProgressBar), 'test.scopeId', true, viewletService, panelService, viewsService); // Active: Show (Infinite) let fn = service.show(true); @@ -169,5 +176,19 @@ suite('Progress Indicator', () => { assert.strictEqual(true, testProgressBar.fDone); viewletService.onDidViewletOpenEmitter.fire(testViewlet); assert.strictEqual(true, testProgressBar.fDone); + + // Visible view: Show (Infinite) + viewsService.onDidChangeViewVisibilityEmitter.fire({ id: 'test.scopeId', visible: true }); + fn = service.show(true); + assert.strictEqual(true, testProgressBar.fInfinite); + fn.done(); + assert.strictEqual(true, testProgressBar.fDone); + + // Hidden view: Show (Infinite) + viewsService.onDidChangeViewVisibilityEmitter.fire({ id: 'test.scopeId', visible: false }); + service.show(true); + assert.strictEqual(false, !!testProgressBar.fInfinite); + viewsService.onDidChangeViewVisibilityEmitter.fire({ id: 'test.scopeId', visible: true }); + assert.strictEqual(true, testProgressBar.fInfinite); }); }); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index df5cdda37dd..582fb63b3b5 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -11,16 +11,20 @@ import * as objects from 'vs/base/common/objects'; import * as extpath from 'vs/base/common/extpath'; import { fuzzyContains, getNLines } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IFilesConfiguration } from 'vs/platform/files/common/files'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IFilesConfiguration, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { Event } from 'vs/base/common/event'; import { relative } from 'vs/base/common/path'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; export const VIEWLET_ID = 'workbench.view.search'; export const PANEL_ID = 'workbench.panel.search'; export const VIEW_ID = 'workbench.view.search'; +export const SEARCH_EXCLUDE_CONFIG = 'search.exclude'; + export const ISearchService = createDecorator('searchService'); /** @@ -372,6 +376,14 @@ export function getExcludes(configuration: ISearchConfiguration, includeSearchEx return allExcludes; } +export function createResourceExcludeMatcher(instantiationService: IInstantiationService, configurationService: IConfigurationService): ResourceGlobMatcher { + return instantiationService.createInstance( + ResourceGlobMatcher, + root => getExcludes(root ? configurationService.getValue({ resource: root }) : configurationService.getValue()) || Object.create(null), + event => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration(SEARCH_EXCLUDE_CONFIG) + ); +} + export function pathIncludedInQuery(queryProps: ICommonQueryProps, fsPath: string): boolean { if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath)) { return false; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 6f5824b53ed..b6b5919fe40 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -280,16 +280,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined, options ? options.mode : undefined); modelPromise = model.load(options); - // Install model listeners - const modelListeners = new DisposableStore(); - modelListeners.add(model.onDidLoad(reason => this._onDidLoad.fire({ model: newModel, reason }))); - modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(newModel))); - modelListeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(newModel))); - modelListeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: newModel, reason }))); - modelListeners.add(model.onDidRevert(() => this._onDidRevert.fire(newModel))); - modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(newModel))); - - this.mapResourceToModelListeners.set(resource, modelListeners); + this.registerModel(newModel); } // Store pending loads to avoid race conditions @@ -298,9 +289,15 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Make known to manager (if not already known) this.add(resource, model); - // Signal as event if we created the model + // Emit some events if we created the model if (didCreateModel) { this._onDidCreate.fire(model); + + // If the model is dirty right from the beginning, + // make sure to emit this as an event + if (model.isDirty()) { + this._onDidChangeDirty.fire(model); + } } try { @@ -335,6 +332,21 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + private registerModel(model: TextFileEditorModel): void { + + // Install model listeners + const modelListeners = new DisposableStore(); + modelListeners.add(model.onDidLoad(reason => this._onDidLoad.fire({ model, reason }))); + modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model))); + modelListeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(model))); + modelListeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: model, reason }))); + modelListeners.add(model.onDidRevert(() => this._onDidRevert.fire(model))); + modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model))); + + // Keep for disposal + this.mapResourceToModelListeners.set(model.resource, modelListeners); + } + add(resource: URI, model: TextFileEditorModel): void { const knownModel = this.mapResourceToModel.get(resource); if (knownModel === model) { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6f0d33b6f84..bc1cc740382 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -169,7 +169,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // restore theme this.setColorTheme(prevColorId, 'auto'); prevColorId = undefined; - } else { + } else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) { this.reloadCurrentColorTheme(); } } @@ -179,13 +179,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { let prevFileIconId: string | undefined = undefined; this.fileIconThemeRegistry.onDidChange(async event => { updateFileIconThemeConfigurationSchemas(event.themes); - if (await this.restoreFileIconTheme()) { // checks if theme from settings exists and is set // restore theme if (this.currentFileIconTheme.id === DEFAULT_FILE_ICON_THEME_ID && !types.isUndefined(prevFileIconId) && await this.fileIconThemeRegistry.findThemeById(prevFileIconId)) { this.setFileIconTheme(prevFileIconId, 'auto'); prevFileIconId = undefined; - } else { + } else if (event.added.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) { this.reloadCurrentFileIconTheme(); } } else { @@ -193,18 +192,18 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { prevFileIconId = this.currentFileIconTheme.id; this.setFileIconTheme(DEFAULT_FILE_ICON_THEME_ID, 'auto'); } + }); let prevProductIconId: string | undefined = undefined; this.productIconThemeRegistry.onDidChange(async event => { updateProductIconThemeConfigurationSchemas(event.themes); - if (await this.restoreProductIconTheme()) { // checks if theme from settings exists and is set // restore theme if (this.currentProductIconTheme.id === DEFAULT_PRODUCT_ICON_THEME_ID && !types.isUndefined(prevProductIconId) && await this.productIconThemeRegistry.findThemeById(prevProductIconId)) { this.setProductIconTheme(prevProductIconId, 'auto'); prevProductIconId = undefined; - } else { + } else if (event.added.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) { this.reloadCurrentProductIconTheme(); } } else { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index ca5fb5e9e3d..05992b71044 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -44,6 +44,8 @@ const tokenGroupToScopesMap = { export type TokenStyleDefinition = TokenStylingRule | ProbeScope[] | TokenStyleValue; export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDefinition | undefined }; +export type TextMateThemingRuleDefinitions = { [P in keyof TokenStyleData]?: ITextMateThemingRule | undefined; } & { scope?: ProbeScope; }; + const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; export class ColorThemeData implements IWorkbenchColorTheme { @@ -57,6 +59,9 @@ export class ColorThemeData implements IWorkbenchColorTheme { watch?: boolean; extensionData?: ExtensionData; + private themeSemanticHighlighting: boolean | undefined; + private customSemanticHighlighting: boolean | undefined; + private themeTokenColors: ITextMateThemingRule[] = []; private customTokenColors: ITextMateThemingRule[] = []; private colorMap: IColorMap = {}; @@ -78,6 +83,10 @@ export class ColorThemeData implements IWorkbenchColorTheme { this.isLoaded = false; } + get semanticHighlighting(): boolean { + return this.customSemanticHighlighting !== undefined ? this.customSemanticHighlighting : !!this.themeSemanticHighlighting; + } + get tokenColors(): ITextMateThemingRule[] { if (!this.textMateThemingRules) { const result: ITextMateThemingRule[] = []; @@ -271,7 +280,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { return colorRegistry.resolveDefaultColor(colorId, this); } - public resolveScopes(scopes: ProbeScope[]): TokenStyle | undefined { + + public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined { if (!this.themeTokenScopeMatchers) { this.themeTokenScopeMatchers = this.themeTokenColors.map(getScopeMatcher); @@ -285,17 +295,24 @@ export class ColorThemeData implements IWorkbenchColorTheme { let fontStyle: string | undefined = undefined; let foregroundScore = -1; let fontStyleScore = -1; + let fontStyleThemingRule: ITextMateThemingRule | undefined = undefined; + let foregroundThemingRule: ITextMateThemingRule | undefined = undefined; - function findTokenStyleForScopeInScopes(scopeMatchers: Matcher[], tokenColors: ITextMateThemingRule[]) { + function findTokenStyleForScopeInScopes(scopeMatchers: Matcher[], themingRules: ITextMateThemingRule[]) { for (let i = 0; i < scopeMatchers.length; i++) { const score = scopeMatchers[i](scope); if (score >= 0) { - const settings = tokenColors[i].settings; + const themingRule = themingRules[i]; + const settings = themingRules[i].settings; if (score >= foregroundScore && settings.foreground) { foreground = settings.foreground; + foregroundScore = score; + foregroundThemingRule = themingRule; } if (score >= fontStyleScore && types.isString(settings.fontStyle)) { fontStyle = settings.fontStyle; + fontStyleScore = score; + fontStyleThemingRule = themingRule; } } } @@ -303,6 +320,12 @@ export class ColorThemeData implements IWorkbenchColorTheme { findTokenStyleForScopeInScopes(this.themeTokenScopeMatchers, this.themeTokenColors); findTokenStyleForScopeInScopes(this.customTokenScopeMatchers, this.customTokenColors); if (foreground !== undefined || fontStyle !== undefined) { + if (definitions) { + definitions.foreground = foregroundThemingRule; + definitions.bold = definitions.italic = definitions.underline = fontStyleThemingRule; + definitions.scope = scope; + } + return TokenStyle.fromSettings(foreground, fontStyle); } } @@ -344,6 +367,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { public setCustomTokenColors(customTokenColors: ITokenColorCustomizations) { this.customTokenColors = []; + this.customSemanticHighlighting = undefined; // first add the non-theme specific settings this.addCustomTokenColors(customTokenColors); @@ -395,6 +419,9 @@ export class ColorThemeData implements IWorkbenchColorTheme { } } } + if (customTokenColors.semanticHighlighting !== undefined) { + this.customSemanticHighlighting = customTokenColors.semanticHighlighting; + } } public ensureLoaded(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise { @@ -415,13 +442,15 @@ export class ColorThemeData implements IWorkbenchColorTheme { const result = { colors: {}, textMateRules: [], - stylingRules: undefined + stylingRules: undefined, + semanticHighlighting: false }; return _loadColorTheme(extensionResourceLoaderService, this.location, result).then(_ => { this.isLoaded = true; this.tokenStylingRules = result.stylingRules; this.colorMap = result.colors; this.themeTokenColors = result.textMateRules; + this.themeSemanticHighlighting = result.semanticHighlighting; }); } @@ -445,6 +474,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { selector: this.id.split(' ').join('.'), // to not break old clients themeTokenColors: this.themeTokenColors, extensionData: this.extensionData, + themeSemanticHighlighting: this.themeSemanticHighlighting, colorMap: colorMapData, watch: this.watch }); @@ -452,7 +482,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } hasEqualData(other: ColorThemeData) { - return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.themeTokenColors, other.themeTokenColors); + return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.themeTokenColors, other.themeTokenColors) && this.themeSemanticHighlighting === other.themeSemanticHighlighting; } get baseTheme(): string { @@ -502,7 +532,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } break; case 'themeTokenColors': - case 'id': case 'label': case 'settingsId': case 'extensionData': case 'watch': + case 'id': case 'label': case 'settingsId': case 'extensionData': case 'watch': case 'themeSemanticHighlighting': (theme as any)[key] = data[key]; break; } @@ -546,7 +576,7 @@ function toCSSSelector(extensionId: string, path: string) { return str; } -function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, stylingRules: TokenStylingRule[] | undefined }): Promise { +function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, stylingRules: TokenStylingRule[] | undefined, semanticHighlighting: boolean }): Promise { if (resources.extname(themeLocation) === '.json') { return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => { let errors: Json.ParseError[] = []; @@ -565,6 +595,7 @@ function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoade convertSettings(contentValue.settings, result); return null; } + result.semanticHighlighting = result.semanticHighlighting || contentValue.semanticHighlighting; let colors = contentValue.colors; if (colors) { if (typeof colors !== 'object') { @@ -589,10 +620,10 @@ function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoade return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString()))); } } - let tokenStylingRules = contentValue.tokenStylingRules; - if (tokenStylingRules && typeof tokenStylingRules === 'object') { - result.stylingRules = readCustomTokenStyleRules(tokenStylingRules, result.stylingRules); - } + // let tokenStylingRules = contentValue.tokenStylingRules; + // if (tokenStylingRules && typeof tokenStylingRules === 'object') { + // result.stylingRules = readCustomTokenStyleRules(tokenStylingRules, result.stylingRules); + // } return null; }); }); @@ -657,7 +688,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/common/colorThemeSchema.ts b/src/vs/workbench/services/themes/common/colorThemeSchema.ts index 7f5d0f5c59a..163ee66ef5c 100644 --- a/src/vs/workbench/services/themes/common/colorThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/colorThemeSchema.ts @@ -222,6 +222,10 @@ const colorThemeSchema: IJSONSchema = { $ref: textmateColorsSchemaId } ] + }, + semanticHighlighting: { + type: 'boolean', + description: nls.localize('schema.supportsSemanticHighlighting', 'Whether semantic highlighting should be enabled for this theme.') } } }; diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index b45ebe90434..a5aac2386f4 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -132,6 +132,10 @@ const tokenColorSchema: IJSONSchema = { textMateRules: { description: nls.localize('editorColors.textMateRules', 'Sets colors and styles using textmate theming rules (advanced).'), $ref: textmateColorsSchemaId + }, + semanticHighlighting: { + description: nls.localize('editorColors.semanticHighlighting', 'Whether semantic highlighting should be enabled for this theme.'), + type: 'boolean' } } }; @@ -154,6 +158,7 @@ const tokenColorCustomizationConfiguration: IConfigurationNode = { [ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL]: experimentalTokenStylingCustomizationSchema } }; + configurationRegistry.registerConfiguration(tokenColorCustomizationConfiguration); export function updateColorThemeConfigurationSchemas(themes: IWorkbenchColorTheme[]) { diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 3e897cd371b..cf95aeb4451 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -97,7 +97,7 @@ export interface IColorCustomizations { } export interface ITokenColorCustomizations { - [groupIdOrThemeSettingsId: string]: string | ITokenColorizationSetting | ITokenColorCustomizations | undefined | ITextMateThemingRule[]; + [groupIdOrThemeSettingsId: string]: string | ITokenColorizationSetting | ITokenColorCustomizations | undefined | ITextMateThemingRule[] | boolean; comments?: string | ITokenColorizationSetting; strings?: string | ITokenColorizationSetting; numbers?: string | ITokenColorizationSetting; @@ -106,6 +106,7 @@ export interface ITokenColorCustomizations { functions?: string | ITokenColorizationSetting; variables?: string | ITokenColorizationSetting; textMateRules?: ITextMateThemingRule[]; + semanticHighlighting?: boolean; } export interface IExperimentalTokenStyleCustomizations { 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 8f5da088288..242182ce537 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/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index 10c06b14c5e..5c91d4512b6 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -219,11 +219,13 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe } private registerModel(model: UntitledTextEditorModel): void { - const modelDisposables = new DisposableStore(); - modelDisposables.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model))); - modelDisposables.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model))); - modelDisposables.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model))); - modelDisposables.add(model.onDispose(() => this._onDidDispose.fire(model))); + + // Install model listeners + const modelListeners = new DisposableStore(); + modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model))); + modelListeners.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model))); + modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model))); + modelListeners.add(model.onDispose(() => this._onDidDispose.fire(model))); // Remove from cache on dispose Event.once(model.onDispose)(() => { @@ -232,11 +234,17 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe this.mapResourceToModel.delete(model.resource); // Listeners - modelDisposables.dispose(); + modelListeners.dispose(); }); // Add to cache this.mapResourceToModel.set(model.resource, model); + + // If the model is dirty right from the beginning, + // make sure to emit this as an event + if (model.isDirty()) { + this._onDidChangeDirty.fire(model); + } } } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index cf3ce106c2a..bf8c715cb34 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -14,6 +14,7 @@ import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRe import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; suite('Untitled text editors', () => { @@ -120,15 +121,23 @@ suite('Untitled text editors', () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })); + let onDidChangeDirtyModel: IUntitledTextEditorModel | undefined = undefined; + const listener = service.onDidChangeDirty(model => { + onDidChangeDirtyModel = model; + }); + + const model = service.create({ associatedResource: file }); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, model); assert.ok(untitled.isDirty()); + assert.equal(model, onDidChangeDirtyModel); - const model = await untitled.resolve(); + const resolvedModel = await untitled.resolve(); - assert.ok(model.hasAssociatedFilePath); + assert.ok(resolvedModel.hasAssociatedFilePath); assert.equal(untitled.isDirty(), true); untitled.dispose(); + listener.dispose(); }); test('no longer dirty when content gets empty (not with associated resource)', async () => { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts deleted file mode 100644 index 7d48e03e84c..00000000000 --- a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts +++ /dev/null @@ -1,111 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { SyncStatus, ISettingsSyncService, IConflictSetting, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export class SettingsSyncService extends Disposable implements ISettingsSyncService { - - _serviceBrand: undefined; - - private readonly channel: IChannel; - - readonly resourceKey = 'settings'; - readonly source = SyncSource.Settings; - - private _status: SyncStatus = SyncStatus.Uninitialized; - get status(): SyncStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - private _conflicts: IConflictSetting[] = []; - get conflicts(): IConflictSetting[] { return this._conflicts; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; - - get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } - - constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService - ) { - super(); - this.channel = sharedProcessService.getChannel('settingsSync'); - this.channel.call('_getInitialStatus').then(status => { - this.updateStatus(status); - this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); - }); - this.channel.call('_getInitialConflicts').then(conflicts => { - if (conflicts.length) { - this.updateConflicts(conflicts); - } - this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); - }); - } - - pull(): Promise { - return this.channel.call('pull'); - } - - push(): Promise { - return this.channel.call('push'); - } - - sync(): Promise { - return this.channel.call('sync'); - } - - stop(): Promise { - return this.channel.call('stop'); - } - - resetLocal(): Promise { - return this.channel.call('resetLocal'); - } - - hasPreviouslySynced(): Promise { - return this.channel.call('hasPreviouslySynced'); - } - - hasLocalData(): Promise { - return this.channel.call('hasLocalData'); - } - - accept(content: string): Promise { - return this.channel.call('accept', [content]); - } - - resolveSettingsConflicts(conflicts: { key: string, value: any | undefined }[]): Promise { - return this.channel.call('resolveConflicts', [conflicts]); - } - - getRemoteContent(ref?: string, fragment?: string): Promise { - return this.channel.call('getRemoteContent', [ref, fragment]); - } - - getLocalBackupContent(ref?: string, fragment?: string): Promise { - return this.channel.call('getLocalBackupContent', [ref, fragment]); - } - - getRemoteContentFromPreview(): Promise { - return this.channel.call('getRemoteContentFromPreview', []); - } - - private async updateStatus(status: SyncStatus): Promise { - this._status = status; - this._onDidChangeStatus.fire(status); - } - - private async updateConflicts(conflicts: IConflictSetting[]): Promise { - this._conflicts = conflicts; - this._onDidChangeConflicts.fire(conflicts); - } - -} - -registerSingleton(ISettingsSyncService, SettingsSyncService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts index 3a395b1c5b4..7ce287cdc13 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceKey, IResourceRefHandle, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IResourceRefHandle, IUserDataSyncBackupStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -19,16 +19,16 @@ export class UserDataSyncBackupStoreService implements IUserDataSyncBackupStoreS this.channel = sharedProcessService.getChannel('userDataSyncBackupStoreService'); } - backup(key: ResourceKey, content: string): Promise { + backup(key: SyncResource, content: string): Promise { return this.channel.call('backup', [key, content]); } - getAllRefs(key: ResourceKey): Promise { + getAllRefs(key: SyncResource): Promise { return this.channel.call('getAllRefs', [key]); } - resolveContent(key: ResourceKey, ref: string): Promise { + resolveContent(key: SyncResource, ref: string): Promise { return this.channel.call('resolveContent', [key, ref]); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 686866cfc79..d4838228ff0 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, SyncSource, IUserDataSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -23,20 +23,20 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } - private _conflictsSources: SyncSource[] = []; - get conflictsSources(): SyncSource[] { return this._conflictsSources; } - private _onDidChangeConflicts: Emitter = this._register(new Emitter()); - readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _conflictsSources: SyncResource[] = []; + get conflictsSources(): SyncResource[] { return this._conflictsSources; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private _lastSyncTime: number | undefined = undefined; get lastSyncTime(): number | undefined { return this._lastSyncTime; } private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); readonly onDidChangeLastSyncTime: Event = this._onDidChangeLastSyncTime.event; - private _onSyncErrors: Emitter<[SyncSource, UserDataSyncError][]> = this._register(new Emitter<[SyncSource, UserDataSyncError][]>()); - readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]> = this._onSyncErrors.event; + private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>()); + readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event; constructor( @ISharedProcessService sharedProcessService: ISharedProcessService @@ -52,7 +52,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return userDataSyncChannel.listen(event, arg); } }; - this.channel.call<[SyncStatus, SyncSource[], number | undefined]>('_getInitialData').then(([status, conflicts, lastSyncTime]) => { + this.channel.call<[SyncStatus, SyncResource[], number | undefined]>('_getInitialData').then(([status, conflicts, lastSyncTime]) => { this.updateStatus(status); this.updateConflicts(conflicts); if (lastSyncTime) { @@ -61,8 +61,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); this._register(this.channel.listen('onDidChangeLastSyncTime')(lastSyncTime => this.updateLastSyncTime(lastSyncTime))); }); - this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); - this._register(this.channel.listen<[SyncSource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)]))))); + this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); + this._register(this.channel.listen<[SyncResource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)]))))); } pull(): Promise { @@ -73,7 +73,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('sync'); } - accept(source: SyncSource, content: string): Promise { + accept(source: SyncResource, content: string): Promise { return this.channel.call('accept', [source, content]); } @@ -102,7 +102,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._onDidChangeStatus.fire(status); } - private async updateConflicts(conflicts: SyncSource[]): Promise { + private async updateConflicts(conflicts: SyncResource[]): Promise { this._conflictsSources = conflicts; this._onDidChangeConflicts.fire(conflicts); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts index dd3273ab5f0..21365024770 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncSource, IUserDataSyncStoreService, IUserDataSyncStore, getUserDataSyncStore, ResourceKey, IUserData, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, IUserDataSyncStoreService, IUserDataSyncStore, getUserDataSyncStore, IUserData, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -25,11 +25,11 @@ export class UserDataSyncStoreService implements IUserDataSyncStoreService { this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); } - read(key: ResourceKey, oldValue: IUserData | null, source?: SyncSource): Promise { + read(key: SyncResource, oldValue: IUserData | null, source?: SyncResource): Promise { throw new Error('Not Supported'); } - write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise { + write(key: SyncResource, content: string, ref: string | null, source?: SyncResource): Promise { throw new Error('Not Supported'); } @@ -41,15 +41,15 @@ export class UserDataSyncStoreService implements IUserDataSyncStoreService { throw new Error('Not Supported'); } - getAllRefs(key: ResourceKey): Promise { + getAllRefs(key: SyncResource): Promise { return this.channel.call('getAllRefs', [key]); } - resolveContent(key: ResourceKey, ref: string): Promise { + resolveContent(key: SyncResource, ref: string): Promise { return this.channel.call('resolveContent', [key, ref]); } - delete(key: ResourceKey): Promise { + delete(key: SyncResource): Promise { return this.channel.call('delete', [key]); } diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index 96f97d1ad98..e974da14e70 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -35,6 +35,8 @@ suite('Workbench editor', () => { assert.equal(toResource(untitled)!.toString(), untitled.resource.toString()); assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), untitled.resource.toString()); + assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.DETAILS })!.toString(), untitled.resource.toString()); + assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.BOTH })!.toString(), untitled.resource.toString()); assert.equal(toResource(untitled, { filterByScheme: Schemas.untitled })!.toString(), untitled.resource.toString()); assert.equal(toResource(untitled, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), untitled.resource.toString()); assert.ok(!toResource(untitled, { filterByScheme: Schemas.file })); @@ -43,6 +45,8 @@ suite('Workbench editor', () => { assert.equal(toResource(file)!.toString(), file.resource.toString()); assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.DETAILS })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.BOTH })!.toString(), file.resource.toString()); assert.equal(toResource(file, { filterByScheme: Schemas.file })!.toString(), file.resource.toString()); assert.equal(toResource(file, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); assert.ok(!toResource(file, { filterByScheme: Schemas.untitled })); @@ -52,8 +56,20 @@ suite('Workbench editor', () => { assert.ok(!toResource(diffEditorInput)); assert.ok(!toResource(diffEditorInput, { filterByScheme: Schemas.file })); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file })!.toString(), file.resource.toString()); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file })!.toString(), file.resource.toString()); + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); + + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.DETAILS })!.toString(), untitled.resource.toString()); + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.DETAILS, filterByScheme: Schemas.untitled })!.toString(), untitled.resource.toString()); + assert.equal(toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.DETAILS, filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), untitled.resource.toString()); + + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH }) as { master: URI, detail: URI }).master.toString(), file.resource.toString()); + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH, filterByScheme: Schemas.file }) as { master: URI, detail: URI }).master.toString(), file.resource.toString()); + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH, filterByScheme: [Schemas.file, Schemas.untitled] }) as { master: URI, detail: URI }).master.toString(), file.resource.toString()); + + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH }) as { master: URI, detail: URI }).detail.toString(), untitled.resource.toString()); + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH, filterByScheme: Schemas.untitled }) as { master: URI, detail: URI }).detail.toString(), untitled.resource.toString()); + assert.equal((toResource(diffEditorInput, { supportSideBySide: SideBySideEditor.BOTH, filterByScheme: [Schemas.file, Schemas.untitled] }) as { master: URI, detail: URI }).detail.toString(), untitled.resource.toString()); }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index e74010b7c6e..4742a1c19d0 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -24,6 +24,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { @@ -84,6 +86,7 @@ suite('Workbench editor model', () => { instantiationService.stub(IDialogService, dialogService); instantiationService.stub(INotificationService, notificationService); instantiationService.stub(IUndoRedoService, undoRedoService); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts index 82285eeb738..224d20cfa21 100644 --- a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts @@ -22,6 +22,8 @@ import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommand import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; suite('Editor - Range decorations', () => { @@ -157,6 +159,7 @@ suite('Editor - Range decorations', () => { function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts index bd9990491e4..d265406e99b 100644 --- a/src/vs/workbench/test/browser/quickAccess.test.ts +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IQuickAccessRegistry, Extensions, IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +18,10 @@ suite('QuickAccess', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; + let providerDefaultCalled = false; + let providerDefaultCanceled = false; + let providerDefaultDisposed = false; + let provider1Called = false; let provider1Canceled = false; let provider1Disposed = false; @@ -30,9 +34,21 @@ suite('QuickAccess', () => { let provider3Canceled = false; let provider3Disposed = false; - let provider4Called = false; - let provider4Canceled = false; - let provider4Disposed = false; + class TestProviderDefault implements IQuickAccessProvider { + + constructor(@IQuickInputService private readonly quickInputService: IQuickInputService, disposables: DisposableStore) { } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + providerDefaultCalled = true; + token.onCancellationRequested(() => providerDefaultCanceled = true); + + // bring up provider #3 + setTimeout(() => this.quickInputService.quickAccess.show(providerDescriptor3.prefix)); + + return toDisposable(() => providerDefaultDisposed = true); + } + } class TestProvider1 implements IQuickAccessProvider { provide(picker: IQuickPick, token: CancellationToken): IDisposable { @@ -55,39 +71,22 @@ suite('QuickAccess', () => { } class TestProvider3 implements IQuickAccessProvider { - - constructor(@IQuickInputService private readonly quickInputService: IQuickInputService, disposables: DisposableStore) { } - provide(picker: IQuickPick, token: CancellationToken): IDisposable { assert.ok(picker); provider3Called = true; token.onCancellationRequested(() => provider3Canceled = true); - // bring up provider #4 - setTimeout(() => this.quickInputService.quickAccess.show(providerDescriptor4.prefix)); + // hide without picking + setTimeout(() => picker.hide()); return toDisposable(() => provider3Disposed = true); } } - class TestProvider4 implements IQuickAccessProvider { - provide(picker: IQuickPick, token: CancellationToken): IDisposable { - assert.ok(picker); - provider4Called = true; - token.onCancellationRequested(() => provider4Canceled = true); - - // hide without picking - setTimeout(() => picker.hide()); - - return toDisposable(() => provider4Disposed = true); - } - } - - const defaultProviderDescriptor = { ctor: TestProvider1, prefix: '', helpEntries: [] }; + const providerDescriptorDefault = { ctor: TestProviderDefault, prefix: '', helpEntries: [] }; const providerDescriptor1 = { ctor: TestProvider1, prefix: 'test', helpEntries: [] }; const providerDescriptor2 = { ctor: TestProvider2, prefix: 'test something', helpEntries: [] }; - const providerDescriptor3 = { ctor: TestProvider3, prefix: 'default', helpEntries: [] }; - const providerDescriptor4 = { ctor: TestProvider4, prefix: 'changed', helpEntries: [] }; + const providerDescriptor3 = { ctor: TestProvider3, prefix: 'changed', helpEntries: [] }; setup(() => { instantiationService = workbenchInstantiationService(); @@ -96,91 +95,101 @@ suite('QuickAccess', () => { test('registry', () => { const registry = (Registry.as(Extensions.Quickaccess)); - registry.defaultProvider = defaultProviderDescriptor; + const restore = (registry as QuickAccessRegistry).clear(); - const initialSize = registry.getQuickAccessProviders().length; + assert.ok(!registry.getQuickAccessProvider('test')); - const disposable = registry.registerQuickAccessProvider(providerDescriptor1); + const disposables = new DisposableStore(); + disposables.add(registry.registerQuickAccessProvider(providerDescriptorDefault)); + assert(registry.getQuickAccessProvider('') === providerDescriptorDefault); + assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault); + + const disposable = disposables.add(registry.registerQuickAccessProvider(providerDescriptor1)); assert(registry.getQuickAccessProvider('test') === providerDescriptor1); const providers = registry.getQuickAccessProviders(); assert(providers.some(provider => provider.prefix === 'test')); disposable.dispose(); + assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault); + + disposables.dispose(); assert.ok(!registry.getQuickAccessProvider('test')); - assert.equal(registry.getQuickAccessProviders().length - initialSize, 0); + + restore(); }); test('provider', async () => { const registry = (Registry.as(Extensions.Quickaccess)); - const defaultProvider = registry.defaultProvider; + const restore = (registry as QuickAccessRegistry).clear(); const disposables = new DisposableStore(); + disposables.add(registry.registerQuickAccessProvider(providerDescriptorDefault)); disposables.add(registry.registerQuickAccessProvider(providerDescriptor1)); disposables.add(registry.registerQuickAccessProvider(providerDescriptor2)); - disposables.add(registry.registerQuickAccessProvider(providerDescriptor4)); - registry.defaultProvider = providerDescriptor3; + disposables.add(registry.registerQuickAccessProvider(providerDescriptor3)); accessor.quickInputService.quickAccess.show('test'); + assert.equal(providerDefaultCalled, false); assert.equal(provider1Called, true); assert.equal(provider2Called, false); assert.equal(provider3Called, false); - assert.equal(provider4Called, false); + assert.equal(providerDefaultCanceled, false); assert.equal(provider1Canceled, false); assert.equal(provider2Canceled, false); assert.equal(provider3Canceled, false); - assert.equal(provider4Canceled, false); + assert.equal(providerDefaultDisposed, false); assert.equal(provider1Disposed, false); assert.equal(provider2Disposed, false); assert.equal(provider3Disposed, false); - assert.equal(provider4Disposed, false); provider1Called = false; accessor.quickInputService.quickAccess.show('test something'); + assert.equal(providerDefaultCalled, false); assert.equal(provider1Called, false); assert.equal(provider2Called, true); assert.equal(provider3Called, false); - assert.equal(provider4Called, false); + assert.equal(providerDefaultCanceled, false); assert.equal(provider1Canceled, true); assert.equal(provider2Canceled, false); assert.equal(provider3Canceled, false); - assert.equal(provider4Canceled, false); + assert.equal(providerDefaultDisposed, false); assert.equal(provider1Disposed, true); assert.equal(provider2Disposed, false); assert.equal(provider3Disposed, false); - assert.equal(provider4Disposed, false); provider2Called = false; provider1Canceled = false; provider1Disposed = false; accessor.quickInputService.quickAccess.show('usedefault'); + assert.equal(providerDefaultCalled, true); assert.equal(provider1Called, false); assert.equal(provider2Called, false); - assert.equal(provider3Called, true); - assert.equal(provider4Called, false); + assert.equal(provider3Called, false); + assert.equal(providerDefaultCanceled, false); assert.equal(provider1Canceled, false); assert.equal(provider2Canceled, true); assert.equal(provider3Canceled, false); - assert.equal(provider4Canceled, false); + assert.equal(providerDefaultDisposed, false); assert.equal(provider1Disposed, false); assert.equal(provider2Disposed, true); assert.equal(provider3Disposed, false); - assert.equal(provider4Disposed, false); + + await timeout(1); + + assert.equal(providerDefaultCanceled, true); + assert.equal(providerDefaultDisposed, true); + assert.equal(provider3Called, true); await timeout(1); assert.equal(provider3Canceled, true); assert.equal(provider3Disposed, true); - assert.equal(provider4Called, true); - - await timeout(1); - - assert.equal(provider4Canceled, true); - assert.equal(provider4Disposed, true); disposables.dispose(); - registry.defaultProvider = defaultProvider; + + restore(); }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 2e064554f57..7910881aa2b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -104,6 +104,7 @@ import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quic import { IListService } from 'vs/platform/list/browser/listService'; import { win32, posix } from 'vs/base/common/path'; import { TestWorkingCopyService, TestContextService, TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IViewsService, IView } from 'vs/workbench/common/views'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -125,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()); @@ -140,6 +141,8 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IHistoryService, new TestHistoryService()); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(configService)); instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); + const themeService = new TestThemeService(); + instantiationService.stub(IThemeService, themeService); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); instantiationService.stub(IFileService, new TestFileService()); instantiationService.stub(IBackupFileService, new TestBackupFileService()); @@ -155,8 +158,6 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : instantiationService.createInstance(TestTextFileService)); instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - const themeService = new TestThemeService(); - instantiationService.stub(IThemeService, themeService); instantiationService.stub(ILogService, new NullLogService()); const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); instantiationService.stub(IEditorGroupsService, editorGroupService); @@ -451,7 +452,7 @@ export class TestPanelService implements IPanelService { getPanel(id: string): any { return activeViewlet; } getPanels() { return []; } getPinnedPanels() { return []; } - getActivePanel(): IViewlet { return activeViewlet; } + getActivePanel(): IPanel { return activeViewlet; } setPanelEnablement(id: string, enabled: boolean): void { } dispose() { } showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable { throw new Error('Method not implemented.'); } @@ -460,6 +461,19 @@ export class TestPanelService implements IPanelService { getLastActivePanelId(): string { return undefined!; } } +export class TestViewsService implements IViewsService { + _serviceBrand: undefined; + + onDidChangeViewVisibilityEmitter = new Emitter<{ id: string; visible: boolean; }>(); + + onDidChangeViewVisibility = this.onDidChangeViewVisibilityEmitter.event; + isViewVisible(id: string): boolean { return true; } + getActiveViewWithId(id: string): T | null { return null; } + openView(id: string, focus?: boolean | undefined): Promise { return Promise.resolve(null); } + closeView(id: string): void { } + getProgressIndicator(id: string) { return null!; } +} + export class TestEditorGroupsService implements IEditorGroupsService { _serviceBrand: undefined; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index a8f926566d4..d5ab3c6eb15 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -50,7 +50,6 @@ import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; -import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index df598774a44..400cce7151c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -62,13 +62,12 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; -import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { IAuthenticationTokenService, AuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; @@ -87,7 +86,6 @@ registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IAuthenticationTokenService, AuthenticationTokenService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); -registerSingleton(ISettingsSyncService, SettingsSynchroniser); registerSingleton(IUserDataSyncService, UserDataSyncService); registerSingleton(ITitleService, TitlebarPart); diff --git a/yarn.lock b/yarn.lock index c2f11789a2a..f7743cbccd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,9 +574,9 @@ acorn-jsx@^5.1.0: integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== acorn@^5.0.0, acorn@^5.6.2: - version "5.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" - integrity sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ== + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== acorn@^6.0.2: version "6.0.7" @@ -9220,16 +9220,16 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@3.9.0-dev.20200304: - version "3.9.0-dev.20200304" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200304.tgz#3cc35357eff29dc5604b4fa56d6597e13daf86ed" - integrity sha512-eUip/GgJmjp4qtHiJDxVhE5SDDiPzBUg7KBAFUgb7HgL/tv10JAHej7fnS1i+7xrq1eDtbkJyPaYOVnhL9db7Q== - typescript@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= +typescript@^3.9.0-dev.20200313: + version "3.9.0-dev.20200313" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200313.tgz#f66aeb2c08268f2b1fc6d1d96e15554c6e7ed29b" + integrity sha512-85/IJPm1nEUbQDxK3aN+svIy4X3kPcAipihB3704NY1HXncJ1daNLJW1OktOacb8tD/URpIGs9nMgbUrKvglGg== + uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"