diff --git a/.vscode/notebooks/grooming-delta.github-issues b/.vscode/notebooks/grooming-delta.github-issues index a91e6e0156f..97f86b4f386 100644 --- a/.vscode/notebooks/grooming-delta.github-issues +++ b/.vscode/notebooks/grooming-delta.github-issues @@ -158,7 +158,22 @@ { "kind": 1, "language": "markdown", - "value": "# vscode-chrome-debug", + "value": "# vscode-test" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-test is:issue closed:>$since" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-test is:issue created:>$since" + }, + { + "kind": 1, + "language": "markdown", + "value": "# vscode-chrome-debug (deprecated)", "editable": true }, { diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 47f0155d8a5..6921e6c6525 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -365,6 +365,10 @@ { "name": "vs/workbench/services/authentication", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/extensionRecommendations", + "project": "vscode-workbench" } ] } diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index fbb17884ad9..9898e0de97a 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -433,7 +433,7 @@ "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", "@emmetio/html-matcher": "^0.3.3", - "@emmetio/math-expression": "^0.1.1", + "@emmetio/math-expression": "^1.0.4", "image-size": "^0.5.2", "vscode-emmet-helper": "~2.0.0", "vscode-html-languageservice": "^3.0.3" diff --git a/extensions/emmet/src/evaluateMathExpression.ts b/extensions/emmet/src/evaluateMathExpression.ts index 8a7de6ca423..588d4dce9ba 100644 --- a/extensions/emmet/src/evaluateMathExpression.ts +++ b/extensions/emmet/src/evaluateMathExpression.ts @@ -6,24 +6,42 @@ /* Based on @sergeche's work in his emmet plugin */ import * as vscode from 'vscode'; -import evaluate from '@emmetio/math-expression'; +import evaluate, { extract } from '@emmetio/math-expression'; import { DocumentStreamReader } from './bufferStream'; -export function evaluateMathExpression() { +export function evaluateMathExpression(): Thenable { if (!vscode.window.activeTextEditor) { vscode.window.showInformationMessage('No editor is active'); - return; + return Promise.resolve(false); } const editor = vscode.window.activeTextEditor; const stream = new DocumentStreamReader(editor.document); - editor.edit(editBuilder => { + return editor.edit(editBuilder => { editor.selections.forEach(selection => { - const pos = selection.isReversed ? selection.anchor : selection.active; - stream.pos = pos; + // startpos always comes before endpos + const startpos = selection.isReversed ? selection.active : selection.anchor; + const endpos = selection.isReversed ? selection.anchor : selection.active; + const selectionText = stream.substring(startpos, endpos); try { - const result = String(evaluate(stream, true)); - editBuilder.replace(new vscode.Range(stream.pos, pos), result); + if (selectionText) { + // respect selections + const result = String(evaluate(selectionText)); + editBuilder.replace(new vscode.Range(startpos, endpos), result); + } else { + // no selection made, extract expression from line + const lineToSelectionEnd = stream.substring(new vscode.Position(selection.end.line, 0), endpos); + const extractedIndices = extract(lineToSelectionEnd); + if (!extractedIndices) { + throw new Error('Invalid extracted indices'); + } + const result = String(evaluate(lineToSelectionEnd.substr(extractedIndices[0], extractedIndices[1]))); + const rangeToReplace = new vscode.Range( + new vscode.Position(selection.end.line, extractedIndices[0]), + new vscode.Position(selection.end.line, extractedIndices[1]) + ); + editBuilder.replace(rangeToReplace, result); + } } catch (err) { vscode.window.showErrorMessage('Could not evaluate expression'); // Ignore error since most likely it’s because of non-math expression @@ -31,5 +49,4 @@ export function evaluateMathExpression() { } }); }); - } diff --git a/extensions/emmet/src/test/evaluateMathExpression.test.ts b/extensions/emmet/src/test/evaluateMathExpression.test.ts new file mode 100644 index 00000000000..553d0417123 --- /dev/null +++ b/extensions/emmet/src/test/evaluateMathExpression.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import { Position, Selection } from 'vscode'; +import { withRandomFileEditor, closeAllEditors } from './testUtils'; +import { evaluateMathExpression } from '../evaluateMathExpression'; + +suite('Tests for Evaluate Math Expression', () => { + teardown(closeAllEditors); + + function testEvaluateMathExpression(fileContents: string, selection: [number, number] | number, expectedFileContents: string): Thenable { + return withRandomFileEditor(fileContents, 'html', async (editor, _doc) => { + const selectionToUse = typeof selection === 'number' ? + new Selection(new Position(0, selection), new Position(0, selection)) : + new Selection(new Position(0, selection[0]), new Position(0, selection[1])); + editor.selection = selectionToUse; + + await evaluateMathExpression(); + + assert.strictEqual(editor.document.getText(), expectedFileContents); + return Promise.resolve(); + }); + } + + test('Selected sanity check', () => { + return testEvaluateMathExpression('1 + 2', [0, 5], '3'); + }); + + test('Selected with surrounding text', () => { + return testEvaluateMathExpression('test1 + 2test', [4, 9], 'test3test'); + }); + + test('Selected with number not part of selection', () => { + return testEvaluateMathExpression('test3 1+2', [6, 9], 'test3 3'); + }); + + test('Non-selected sanity check', () => { + return testEvaluateMathExpression('1 + 2', 5, '3'); + }); + + test('Non-selected midway', () => { + return testEvaluateMathExpression('1 + 2', 1, '1 + 2'); + }); + + test('Non-selected with surrounding text', () => { + return testEvaluateMathExpression('test1 + 3test', 9, 'test4test'); + }); +}); diff --git a/extensions/emmet/src/typings/emmetio__math-expression.d.ts b/extensions/emmet/src/typings/emmetio__math-expression.d.ts deleted file mode 100644 index 4c3d862b0d4..00000000000 --- a/extensions/emmet/src/typings/emmetio__math-expression.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -declare module '@emmetio/math-expression' { - import { BufferStream } from 'EmmetNode'; - - function index(stream: BufferStream, backward: boolean): number; - - export default index; -} - diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index bb19fbcd08a..d956865f4e7 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -31,13 +31,12 @@ "@emmetio/stream-reader" "^2.0.0" "@emmetio/stream-reader-utils" "^0.1.0" -"@emmetio/math-expression@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@emmetio/math-expression/-/math-expression-0.1.1.tgz#1ff2c7f05800f64c57ca89038ee18bce9f5776dc" - integrity sha1-H/LH8FgA9kxXyokDjuGLzp9Xdtw= +"@emmetio/math-expression@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@emmetio/math-expression/-/math-expression-1.0.4.tgz#cb657ed944f82b3728f863bf5ece1b1ff3ae7497" + integrity sha512-1m7y8/VeXCAfgFoPGTerbqCIadApcIINujd3TaM/LRLPPKiod8aT1PPmh542spnsUSsSnZJjbuF7xiO4WFA42g== dependencies: - "@emmetio/stream-reader" "^2.0.1" - "@emmetio/stream-reader-utils" "^0.1.0" + "@emmetio/scanner" "^1.0.0" "@emmetio/scanner@^1.0.0": version "1.0.0" @@ -49,15 +48,15 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz#244cb02c77ec2e74f78a9bd318218abc9c500a61" integrity sha1-JEywLHfsLnT3ipvTGCGKvJxQCmE= -"@emmetio/stream-reader@^2.0.0", "@emmetio/stream-reader@^2.0.1", "@emmetio/stream-reader@^2.2.0": +"@emmetio/stream-reader@^2.0.0", "@emmetio/stream-reader@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= "@types/node@^12.11.7": - version "12.19.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.2.tgz#9565ed5c72ba96038fc3add643edd5e7820598e7" - integrity sha512-SRH6QM0IMOBBFmDiJ75vlhcbUEYEquvSuhsVW9ijG20JvdFTfOrB1p6ddZxz5y/JNnbf+9HoHhjhOVSX2hsJyA== + version "12.19.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" + integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== ajv@^6.12.3: version "6.12.6" @@ -178,9 +177,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== balanced-match@^1.0.0: version "1.0.0" @@ -392,12 +391,12 @@ debug@^2.2.0: dependencies: ms "2.0.0" -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== dependencies: - ms "^2.1.1" + ms "2.1.2" decamelize@^1.1.2: version "1.2.0" @@ -994,9 +993,9 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-core-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" - integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== dependencies: has "^1.0.3" @@ -1410,7 +1409,7 @@ lodash.values@~2.4.1: dependencies: lodash.keys "~2.4.1" -lodash@^4.16.4: +lodash@^4.17.15: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -1572,12 +1571,12 @@ mocha-junit-reporter@^1.17.0: xml "^1.0.0" mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= + version "1.5.1" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz#c73486bed5519e1d59c9ce39ac7a9792600e5676" + integrity sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg== dependencies: - debug "^3.1.0" - lodash "^4.16.4" + debug "^4.1.1" + lodash "^4.17.15" mocha@^2.3.3: version "2.5.3" @@ -1605,7 +1604,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -2413,14 +2412,15 @@ vinyl@~2.0.1: replace-ext "^1.0.0" vscode-emmet-helper@~2.0.0: - version "2.0.8" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.8.tgz#7c3cf8027d1a75d29625e029d516da7bc56c1fdb" - integrity sha512-Wyf+b5pua+13eZSHCpmob1x915x/od4z6lIia9T2N4v7+CUYNxDisBu3/ShIM3qg3YiYvTCOm+Yx/CLd2khcVw== + version "2.0.9" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.9.tgz#16244c087cba4e379116f268384bb644649db6ad" + integrity sha512-S6RjnR9gUicl8LsYnQAMNqqOxolud9gcj+NpPyEnxfxp1YIBuC9oetj6l6N9VMZBWu6tL77wmf+/EJsRx1PDPA== dependencies: emmet "^2.1.5" jsonc-parser "^2.3.0" vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" + vscode-nls "^5.0.0" vscode-uri "^2.1.2" vscode-html-languageservice@^3.0.3: diff --git a/extensions/git/package.json b/extensions/git/package.json index 72428c5063a..0475c177a04 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -181,6 +181,12 @@ "category": "Git", "icon": "$(discard)" }, + { + "command": "git.rename", + "title": "%command.rename%", + "category": "Git", + "icon": "$(discard)" + }, { "command": "git.commit", "title": "%command.commit%", @@ -373,6 +379,11 @@ "title": "%command.pushToForce%", "category": "Git" }, + { + "command": "git.pushTags", + "title": "%command.pushTags%", + "category": "Git" + }, { "command": "git.pushWithTags", "title": "%command.pushFollowTags%", @@ -383,6 +394,11 @@ "title": "%command.pushFollowTagsForce%", "category": "Git" }, + { + "command": "git.cherryPick", + "title": "%command.cherryPick%", + "category": "Git" + }, { "command": "git.addRemote", "title": "%command.addRemote%", @@ -610,6 +626,10 @@ "command": "git.cleanAllUntracked", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.rename", + "when": "false" + }, { "command": "git.commit", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -710,6 +730,10 @@ "command": "git.renameBranch", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.cherryPick", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.pull", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -774,6 +798,10 @@ "command": "git.pushWithTagsForce", "when": "config.git.enabled && !git.missing && config.git.allowForcePush && gitOpenRepositoryCount != 0" }, + { + "command": "git.pushTags", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.addRemote", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1284,17 +1312,17 @@ { "command": "git.stageSelectedRanges", "group": "2_git@1", - "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && !isInEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.unstageSelectedRanges", "group": "2_git@2", - "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && !isInEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.revertSelectedRanges", "group": "2_git@3", - "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && !isInEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" } ], "scm/change/title": [ @@ -1324,6 +1352,18 @@ "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/" } ], + "explorer/context": [ + { + "submenu": "git.explorer", + "group": "7_git", + "when": "config.git.enabled && !git.missing && !explorerResourceIsRoot" + } + ], + "git.explorer": [ + { + "command": "git.rename" + } + ], "git.commit": [ { "command": "git.commit", @@ -1542,6 +1582,10 @@ ] }, "submenus": [ + { + "id": "git.explorer", + "label": "%submenu.explorer%" + }, { "id": "git.commit", "label": "%submenu.commit%" @@ -1662,21 +1706,27 @@ "scope": "resource" }, "git.checkoutType": { - "type": "string", - "enum": [ - "all", - "local", - "tags", - "remote" - ], - "enumDescriptions": [ - "%config.checkoutType.all%", - "%config.checkoutType.local%", - "%config.checkoutType.tags%", - "%config.checkoutType.remote%" - ], + "type": "array", + "items": { + "type": "string", + "enum": [ + "local", + "tags", + "remote" + ], + "enumDescriptions": [ + "%config.checkoutType.local%", + "%config.checkoutType.tags%", + "%config.checkoutType.remote%" + ] + }, + "uniqueItems": true, "markdownDescription": "%config.checkoutType%", - "default": "all" + "default": [ + "local", + "remote", + "tags" + ] }, "git.ignoreLegacyWarning": { "type": "boolean", @@ -1756,6 +1806,12 @@ "description": "%config.enableStatusBarSync%", "scope": "resource" }, + "git.followTagsWhenSync": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "%config.followTagsWhenSync%" + }, "git.promptToSaveFilesBeforeStash": { "type": "string", "enum": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index c013d399f9f..6345be0c8d2 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -23,6 +23,7 @@ "command.unstage": "Unstage Changes", "command.unstageAll": "Unstage All Changes", "command.unstageSelectedRanges": "Unstage Selected Ranges", + "command.rename": "Rename", "command.clean": "Discard Changes", "command.cleanAll": "Discard All Changes", "command.cleanAllTracked": "Discard All Tracked Changes", @@ -51,6 +52,7 @@ "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", "command.renameBranch": "Rename Branch...", + "command.cherryPick": "Cherry Pick...", "command.merge": "Merge Branch...", "command.rebase": "Rebase Branch...", "command.createTag": "Create Tag", @@ -67,6 +69,7 @@ "command.pushToForce": "Push to... (Force)", "command.pushFollowTags": "Push (Follow Tags)", "command.pushFollowTagsForce": "Push (Follow Tags, Force)", + "command.pushTags": "Push Tags", "command.addRemote": "Add Remote...", "command.removeRemote": "Remove Remote", "command.sync": "Sync", @@ -101,11 +104,10 @@ "config.countBadge.all": "Count all changes.", "config.countBadge.tracked": "Count only tracked changes.", "config.countBadge.off": "Turn off counter.", - "config.checkoutType": "Controls what type of branches are listed when running `Checkout to...`.", - "config.checkoutType.all": "Show all references.", - "config.checkoutType.local": "Show only local branches.", - "config.checkoutType.tags": "Show only tags.", - "config.checkoutType.remote": "Show only remote branches.", + "config.checkoutType": "Controls what type of git refs are listed when running `Checkout to...`.", + "config.checkoutType.local": "Local branches", + "config.checkoutType.tags": "Tags", + "config.checkoutType.remote": "Remote branches", "config.branchValidationRegex": "A regular expression to validate new branch names.", "config.branchWhitespaceChar": "The character to replace whitespace in new branch names.", "config.ignoreLegacyWarning": "Ignores the legacy Git warning.", @@ -122,6 +124,7 @@ "config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.", "config.decorations.enabled": "Controls whether Git contributes colors and badges to the explorer and the open editors view.", "config.enableStatusBarSync": "Controls whether the Git Sync command appears in the status bar.", + "config.followTagsWhenSync": "Follow push all tags when running the sync command.", "config.promptToSaveFilesBeforeStash": "Controls whether Git should check for unsaved files before stashing changes.", "config.promptToSaveFilesBeforeStash.always": "Check for any unsaved files.", "config.promptToSaveFilesBeforeStash.staged": "Check only for unsaved staged files.", @@ -177,6 +180,7 @@ "config.timeline.date.committed": "Use the committed date", "config.timeline.date.authored": "Use the authored date", "config.defaultStashMessage": "Controls whether to use message from commit input box (if populated) as default stash messages", + "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", "submenu.commit.signoff": "Sign Off", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 42992ad9b06..ef39dce7804 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -207,18 +207,53 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{ function createCheckoutItems(repository: Repository): CheckoutItem[] { const config = workspace.getConfiguration('git'); - const checkoutType = config.get('checkoutType') || 'all'; - const includeTags = checkoutType === 'all' || checkoutType === 'tags'; - const includeRemotes = checkoutType === 'all' || checkoutType === 'remote'; + const checkoutTypeConfig = config.get('checkoutType'); + let checkoutTypes: string[]; - const heads = repository.refs.filter(ref => ref.type === RefType.Head) - .map(ref => new CheckoutItem(ref)); - const tags = (includeTags ? repository.refs.filter(ref => ref.type === RefType.Tag) : []) - .map(ref => new CheckoutTagItem(ref)); - const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : []) - .map(ref => new CheckoutRemoteHeadItem(ref)); + if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) { + checkoutTypes = ['local', 'remote', 'tags']; + } else if (typeof checkoutTypeConfig === 'string') { + checkoutTypes = [checkoutTypeConfig]; + } else { + checkoutTypes = checkoutTypeConfig; + } - return [...heads, ...tags, ...remoteHeads]; + const processors = checkoutTypes.map(getCheckoutProcessor) + .filter(p => !!p) as CheckoutProcessor[]; + + for (const ref of repository.refs) { + for (const processor of processors) { + processor.onRef(ref); + } + } + + return processors.reduce((r, p) => r.concat(...p.items), []); +} + +class CheckoutProcessor { + + private refs: Ref[] = []; + get items(): CheckoutItem[] { return this.refs.map(r => new this.ctor(r)); } + constructor(private type: RefType, private ctor: { new(ref: Ref): CheckoutItem }) { } + + onRef(ref: Ref): void { + if (ref.type === this.type) { + this.refs.push(ref); + } + } +} + +function getCheckoutProcessor(type: string): CheckoutProcessor | undefined { + switch (type) { + case 'local': + return new CheckoutProcessor(RefType.Head, CheckoutItem); + case 'remote': + return new CheckoutProcessor(RefType.RemoteHead, CheckoutRemoteHeadItem); + case 'tags': + return new CheckoutProcessor(RefType.Tag, CheckoutTagItem); + } + + return undefined; } function sanitizeRemoteName(name: string) { @@ -236,6 +271,7 @@ enum PushType { Push, PushTo, PushFollowTags, + PushTags } interface PushOptions { @@ -389,7 +425,7 @@ export class CommandCenter { } if (!left) { - await commands.executeCommand('vscode.open', right, opts, title); + await commands.executeCommand('vscode.open', right, { ...opts, override: resource.type === Status.BOTH_MODIFIED ? false : undefined }, title); } else { await commands.executeCommand('vscode.diff', left, right, title, opts); } @@ -792,7 +828,10 @@ export class CommandCenter { try { document = await workspace.openTextDocument(uri); } catch (error) { - await commands.executeCommand('vscode.open', uri, opts); + await commands.executeCommand('vscode.open', uri, { + ...opts, + override: arg instanceof Resource && arg.type === Status.BOTH_MODIFIED ? false : undefined + }); continue; } @@ -884,6 +923,27 @@ export class CommandCenter { } } + @command('git.rename', { repository: true }) + async rename(repository: Repository, fromUri: Uri): Promise { + if (!fromUri) { + return; + } + + const from = path.relative(repository.root, fromUri.path); + let to = await window.showInputBox({ + value: from, + valueSelection: [from.length - path.basename(from).length, from.length] + }); + + to = to?.trim(); + + if (!to) { + return; + } + + await repository.move(from, to); + } + @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { this.outputChannel.appendLine(`git.stage ${resourceStates.length}`); @@ -1428,7 +1488,7 @@ export class CommandCenter { ? localize('unsaved files single', "The following file has unsaved changes which won't be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?", path.basename(documents[0].uri.fsPath)) : localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length); const saveAndCommit = localize('save and commit', "Save All & Commit"); - const commit = localize('commit', "Commit Anyway"); + const commit = localize('commit', "Commit Staged Changes"); const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); if (pick === saveAndCommit) { @@ -1730,25 +1790,20 @@ export class CommandCenter { @command('git.checkout', { repository: true }) async checkout(repository: Repository, treeish?: string): Promise { - if (typeof treeish === 'string') { - await repository.checkout(treeish); - return true; - } - - return this._checkout(repository); + return this._checkout(repository, { treeish }); } @command('git.checkoutDetached', { repository: true }) async checkoutDetached(repository: Repository, treeish?: string): Promise { - if (typeof treeish === 'string') { - await repository.checkout(treeish, { detached: true }); + return this._checkout(repository, { detached: true, treeish }); + } + + private async _checkout(repository: Repository, opts?: { detached?: boolean, treeish?: string }): Promise { + if (typeof opts?.treeish === 'string') { + await repository.checkout(opts?.treeish, opts); return true; } - return this._checkout(repository, { detached: true }); - } - - private async _checkout(repository: Repository, opts?: { detached?: boolean }): Promise { const createBranch = new CreateBranchItem(); const createBranchFrom = new CreateBranchFromItem(); const checkoutDetached = new CheckoutDetachedItem(); @@ -1782,7 +1837,28 @@ export class CommandCenter { } else if (choice === checkoutDetached) { return this._checkout(repository, { detached: true }); } else { - await (choice as CheckoutItem).run(repository, opts); + const item = choice as CheckoutItem; + + try { + await item.run(repository, opts); + } catch (err) { + if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) { + throw err; + } + + const force = localize('force', "Force Checkout"); + const stash = localize('stashcheckout', "Stash & Checkout"); + const choice = await window.showWarningMessage(localize('local changes', "Your local changes would be overwritten by checkout."), { modal: true }, force, stash); + + if (choice === force) { + await this.cleanAll(repository); + await item.run(repository, opts); + } else if (choice === stash) { + await this.stash(repository); + await item.run(repository, opts); + await this.stashPopLatest(repository); + } + } } return true; @@ -2150,6 +2226,10 @@ export class CommandCenter { return; } + if (pushOptions.pushType === PushType.PushTags) { + await repository.pushTags(undefined, forcePushMode); + } + if (!repository.HEAD || !repository.HEAD.name) { if (!pushOptions.silent) { window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote.")); @@ -2221,6 +2301,21 @@ export class CommandCenter { await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true }); } + @command('git.cherryPick', { repository: true }) + async cherryPick(repository: Repository): Promise { + const hash = await window.showInputBox({ + placeHolder: localize('commit hash', "Commit Hash"), + prompt: localize('provide commit hash', "Please provide the commit hash"), + ignoreFocusOut: true + }); + + if (!hash) { + return; + } + + await repository.cherryPick(hash); + } + @command('git.pushTo', { repository: true }) async pushTo(repository: Repository): Promise { await this._push(repository, { pushType: PushType.PushTo }); @@ -2231,6 +2326,11 @@ export class CommandCenter { await this._push(repository, { pushType: PushType.PushTo, forcePush: true }); } + @command('git.pushTags', { repository: true }) + async pushTags(repository: Repository): Promise { + await this._push(repository, { pushType: PushType.PushTags }); + } + @command('git.addRemote', { repository: true }) async addRemote(repository: Repository): Promise { const url = await pickRemoteSource(this.model, { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 06dfff4adf2..0846ca91f42 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1325,6 +1325,7 @@ export class Repository { } catch (err) { if (/Please,? commit your changes or stash them/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.DirtyWorkTree; + err.gitTreeish = treeish; } throw err; @@ -1422,6 +1423,11 @@ export class Repository { await this.run(args); } + async move(from: string, to: string): Promise { + const args = ['mv', from, to]; + await this.run(args); + } + async setBranchUpstream(name: string, upstream: string): Promise { const args = ['branch', '--set-upstream-to', upstream, name]; await this.run(args); @@ -1544,9 +1550,11 @@ export class Repository { await this.run(args); } - async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean } = {}): Promise { + async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean, readonly cancellationToken?: CancellationToken } = {}): Promise { const args = ['fetch']; - const spawnOptions: SpawnOptions = {}; + const spawnOptions: SpawnOptions = { + cancellationToken: options.cancellationToken, + }; if (options.remote) { args.push(options.remote); @@ -1643,7 +1651,7 @@ export class Repository { } } - async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise { + async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise { const args = ['push']; if (forcePushMode === ForcePushMode.ForceWithLease) { @@ -1656,10 +1664,14 @@ export class Repository { args.push('-u'); } - if (tags) { + if (followTags) { args.push('--follow-tags'); } + if (tags) { + args.push('--tags'); + } + if (remote) { args.push(remote); } @@ -1685,6 +1697,11 @@ export class Repository { } } + async cherryPick(commitHash: string): Promise { + const args = ['cherry-pick', commitHash]; + await this.run(args); + } + async blame(path: string): Promise { try { const args = ['blame', sanitizePath(path)]; diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index b736f606e67..03c890757f7 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -80,12 +80,22 @@ class RemoteSourceProviderQuickPick { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; readonly urlLabel?: string; + readonly providerName?: string; } export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>(); quickpick.ignoreFocusOut = true; + if (options.providerName) { + const provider = model.getRemoteProviders() + .filter(provider => provider.name === options.providerName)[0]; + + if (provider) { + return await pickProviderSource(provider); + } + } + const providers = model.getRemoteProviders() .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider })); @@ -116,16 +126,22 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp if (result.url) { return result.url; } else if (result.provider) { - const quickpick = new RemoteSourceProviderQuickPick(result.provider); - const remote = await quickpick.pick(); - - if (remote) { - if (typeof remote.url === 'string') { - return remote.url; - } else if (remote.url.length > 0) { - return await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") }); - } - } + return await pickProviderSource(result.provider); + } + } + + return undefined; +} + +async function pickProviderSource(provider: RemoteSourceProvider): Promise { + const quickpick = new RemoteSourceProviderQuickPick(provider); + const remote = await quickpick.pick(); + + if (remote) { + if (typeof remote.url === 'string') { + return remote.url; + } else if (remote.url.length > 0) { + return await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") }); } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 395fbe15d21..7ff4418e6a3 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -292,6 +292,7 @@ export const enum Operation { Fetch = 'Fetch', Pull = 'Pull', Push = 'Push', + CherryPick = 'CherryPick', Sync = 'Sync', Show = 'Show', Stage = 'Stage', @@ -315,6 +316,8 @@ export const enum Operation { Blame = 'Blame', Log = 'Log', LogFile = 'LogFile', + + Move = 'Move' } function isReadOnly(operation: Operation): boolean { @@ -1059,6 +1062,14 @@ export class Repository implements Disposable { await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name)); } + async cherryPick(commitHash: string): Promise { + await this.run(Operation.CherryPick, () => this.repository.cherryPick(commitHash)); + } + + async move(from: string, to: string): Promise { + await this.run(Operation.Move, () => this.repository.move(from, to)); + } + async getBranch(name: string): Promise { return await this.run(Operation.GetBranch, () => this.repository.getBranch(name)); } @@ -1185,11 +1196,12 @@ export class Repository implements Disposable { const fetchOnPull = config.get('fetchOnPull'); const tags = config.get('pullTags'); + // When fetchOnPull is enabled, fetch all branches when pulling if (fetchOnPull) { - await this.repository.pull(rebase, undefined, undefined, { unshallow, tags }); - } else { - await this.repository.pull(rebase, remote, branch, { unshallow, tags }); + await this.repository.fetch({ all: true }); } + + await this.repository.pull(rebase, remote, branch, { unshallow, tags }); }); }); } @@ -1215,6 +1227,10 @@ export class Repository implements Disposable { await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode)); } + async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise { + await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true)); + } + async blame(path: string): Promise { return await this.run(Operation.Blame, () => this.repository.blame(path)); } @@ -1245,11 +1261,18 @@ export class Repository implements Disposable { const config = workspace.getConfiguration('git', Uri.file(this.root)); const fetchOnPull = config.get('fetchOnPull'); const tags = config.get('pullTags'); + const followTags = config.get('followTagsWhenSync'); const supportCancellation = config.get('supportCancellation'); - const fn = fetchOnPull - ? async (cancellationToken?: CancellationToken) => await this.repository.pull(rebase, undefined, undefined, { tags, cancellationToken }) - : async (cancellationToken?: CancellationToken) => await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken }); + const fn = async (cancellationToken?: CancellationToken) => { + // When fetchOnPull is enabled, fetch all branches when pulling + if (fetchOnPull) { + await this.repository.fetch({ all: true, cancellationToken }); + } + + await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken }); + }; + if (supportCancellation) { const opts: ProgressOptions = { @@ -1272,7 +1295,7 @@ export class Repository implements Disposable { const shouldPush = this.HEAD && (typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true); if (shouldPush) { - await this._push(remoteName, pushBranch); + await this._push(remoteName, pushBranch, false, followTags); } }); }); @@ -1434,9 +1457,9 @@ export class Repository implements Disposable { return ignored; } - private async _push(remote?: string, refspec?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise { + private async _push(remote?: string, refspec?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise { try { - await this.repository.push(remote, refspec, setUpstream, tags, forcePushMode); + await this.repository.push(remote, refspec, setUpstream, followTags, forcePushMode, tags); } catch (err) { if (!remote || !refspec) { throw err; diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index f099255924e..b1719635c96 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -202,9 +202,9 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository) } const openInGitHub = 'Open In GitHub'; - const action = await vscode.window.showInformationMessage(`Successfully published the '${owner}/${repo}' repository on GitHub.`, openInGitHub); - - if (action === openInGitHub) { - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url)); - } + vscode.window.showInformationMessage(`Successfully published the '${owner}/${repo}' repository on GitHub.`, openInGitHub).then(action => { + if (action === openInGitHub) { + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url)); + } + }); } diff --git a/extensions/markdown-language-features/src/features/foldingProvider.ts b/extensions/markdown-language-features/src/features/foldingProvider.ts index 553c1a3257b..62c245c9ad2 100644 --- a/extensions/markdown-language-features/src/features/foldingProvider.ts +++ b/extensions/markdown-language-features/src/features/foldingProvider.ts @@ -7,7 +7,6 @@ import { Token } from 'markdown-it'; import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContentsProvider } from '../tableOfContentsProvider'; -import { flatten } from '../util/arrays'; const rangeLimit = 5000; @@ -27,7 +26,7 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi this.getHeaderFoldingRanges(document), this.getBlockFoldingRanges(document) ]); - return flatten(foldables).slice(0, rangeLimit); + return foldables.flat().slice(0, rangeLimit); } private async getRegions(document: vscode.TextDocument): Promise { diff --git a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts index f97bb021914..91f9dfcd3ec 100644 --- a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts +++ b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts @@ -9,7 +9,6 @@ import { isMarkdownFile } from '../util/file'; import { Lazy, lazy } from '../util/lazy'; import MDDocumentSymbolProvider from './documentSymbolProvider'; import { SkinnyTextDocument, SkinnyTextLine } from '../tableOfContentsProvider'; -import { flatten } from '../util/arrays'; export interface WorkspaceMarkdownDocumentProvider { getAllMarkdownDocuments(): Thenable>; @@ -136,7 +135,7 @@ export default class MarkdownWorkspaceSymbolProvider extends Disposable implemen } const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values()).map(x => x.value)); - const allSymbols = flatten(allSymbolsSets); + const allSymbols = allSymbolsSets.flat(); return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1); } diff --git a/extensions/markdown-language-features/src/util/arrays.ts b/extensions/markdown-language-features/src/util/arrays.ts index ec0ed25c55d..b778a24ec9d 100644 --- a/extensions/markdown-language-features/src/util/arrays.ts +++ b/extensions/markdown-language-features/src/util/arrays.ts @@ -15,8 +15,4 @@ export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEq } return true; -} - -export function flatten(arr: ReadonlyArray[]): T[] { - return ([] as T[]).concat.apply([], arr); } \ No newline at end of file diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index a5bb9b92b49..b02362c2cbe 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -6,6 +6,7 @@ "lib": [ "es6", "es2015.promise", + "es2019.array", "es2020.string", "dom" ] diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 20f54daa8ba..4548b98125d 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "7ea773d195eac3f40261897b49a2499815e9346c" + "commitHash": "4bbf2132df28c71302e305077ce20a811bf7d64b" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 021e5270200..a7c47a3ce0c 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index bca70445720..cbfc6578024 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -438,6 +438,14 @@ "fontCharacter": "\\E032", "fontColor": "#41535b" }, + "_github_light": { + "fontCharacter": "\\E035", + "fontColor": "#bfc2c1" + }, + "_github": { + "fontCharacter": "\\E035", + "fontColor": "#d4d7d6" + }, "_go_light": { "fontCharacter": "\\E036", "fontColor": "#498ba7" @@ -871,526 +879,536 @@ "fontColor": "#e37933" }, "_nim_light": { - "fontCharacter": "\\E061", "fontColor": "#b7b73b" }, "_nim": { - "fontCharacter": "\\E061", "fontColor": "#cbcb41" }, + "_notebook_light": { + "fontColor": "#498ba7" + }, + "_notebook": { + "fontColor": "#519aba" + }, "_npm_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#3b4b52" }, "_npm": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#41535b" }, "_npm_1_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#b8383d" }, "_npm_1": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#cc3e44" }, "_npm_ignored_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#3b4b52" }, "_npm_ignored": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#41535b" }, "_nunjucks_light": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E063", "fontColor": "#7fae42" }, "_nunjucks": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E063", "fontColor": "#8dc149" }, "_ocaml_light": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E064", "fontColor": "#cc6d2e" }, "_ocaml": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E064", "fontColor": "#e37933" }, "_odata_light": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E065", "fontColor": "#cc6d2e" }, "_odata": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E065", "fontColor": "#e37933" }, "_pddl_light": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E066", "fontColor": "#9068b0" }, "_pddl": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E066", "fontColor": "#a074c4" }, "_pdf_light": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E067", "fontColor": "#b8383d" }, "_pdf": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E067", "fontColor": "#cc3e44" }, "_perl_light": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E068", "fontColor": "#498ba7" }, "_perl": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E068", "fontColor": "#519aba" }, "_photoshop_light": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E069", "fontColor": "#498ba7" }, "_photoshop": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E069", "fontColor": "#519aba" }, "_php_light": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06A", "fontColor": "#9068b0" }, "_php": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06A", "fontColor": "#a074c4" }, "_plan_light": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06B", "fontColor": "#7fae42" }, "_plan": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06B", "fontColor": "#8dc149" }, "_platformio_light": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06C", "fontColor": "#cc6d2e" }, "_platformio": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06C", "fontColor": "#e37933" }, "_powershell_light": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06D", "fontColor": "#498ba7" }, "_powershell": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06D", + "fontColor": "#519aba" + }, + "_prisma_light": { + "fontColor": "#498ba7" + }, + "_prisma": { "fontColor": "#519aba" }, "_prolog_light": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E06F", "fontColor": "#cc6d2e" }, "_prolog": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E06F", "fontColor": "#e37933" }, "_pug_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E070", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E070", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E071", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E071", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E072", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E072", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E075", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E075", "fontColor": "#cc3e44" }, "_rollup_light": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E076", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E076", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E077", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E077", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E078", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E078", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E079", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E079", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07A", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07A", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07B", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07B", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07C", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07C", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E07F", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E07F", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E080", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E080", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E081", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E081", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E082", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E082", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E083", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E083", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E083", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E083", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E084", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E084", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E085", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E085", "fontColor": "#e37933" }, "_svg_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E086", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E086", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E086", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E086", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E087", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E087", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E088", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E088", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E089", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E08C" + "fontCharacter": "\\E08B" }, "_tsconfig_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08C", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08C", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08D", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08D", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E08E", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E08E", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E08E", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E08E", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E08F", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E08F", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E090", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E090", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E091", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E091", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E092", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E092", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E093", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E093", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E094", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E094", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E096", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E096", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E097", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E097", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E098", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E098", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E099", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E099", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09A", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09A", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09B", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09B", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09C", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09C", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09C", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09C", "fontColor": "#6d8086" } }, @@ -1449,6 +1467,7 @@ "gsp": "_grails", "gql": "_graphql", "graphql": "_graphql", + "graphqls": "_graphql", "haml": "_haml", "hs": "_haskell", "lhs": "_haskell", @@ -1480,6 +1499,8 @@ "stache": "_mustache", "nim": "_nim", "nims": "_nim", + "github-issues": "_github", + "ipynb": "_notebook", "njk": "_nunjucks", "nunjucks": "_nunjucks", "nunjs": "_nunjucks", @@ -1496,6 +1517,7 @@ "pddl": "_pddl", "plan": "_plan", "happenings": "_happenings", + "prisma": "_prisma", "pp": "_puppet", "epp": "_puppet", "spec.jsx": "_react_1", @@ -1786,6 +1808,7 @@ "gsp": "_grails_light", "gql": "_graphql_light", "graphql": "_graphql_light", + "graphqls": "_graphql_light", "haml": "_haml_light", "hs": "_haskell_light", "lhs": "_haskell_light", @@ -1817,6 +1840,8 @@ "stache": "_mustache_light", "nim": "_nim_light", "nims": "_nim_light", + "github-issues": "_github_light", + "ipynb": "_notebook_light", "njk": "_nunjucks_light", "nunjucks": "_nunjucks_light", "nunjs": "_nunjucks_light", @@ -1833,6 +1858,7 @@ "pddl": "_pddl_light", "plan": "_plan_light", "happenings": "_happenings_light", + "prisma": "_prisma_light", "pp": "_puppet_light", "epp": "_puppet_light", "spec.jsx": "_react_1_light", @@ -2066,5 +2092,5 @@ "npm-debug.log": "_npm_ignored_light" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/7ea773d195eac3f40261897b49a2499815e9346c" + "version": "https://github.com/jesseweed/seti-ui/commit/4bbf2132df28c71302e305077ce20a811bf7d64b" } \ No newline at end of file diff --git a/extensions/theme-tomorrow-night-blue/package.nls.json b/extensions/theme-tomorrow-night-blue/package.nls.json index a37a31b282d..77b44577c81 100644 --- a/extensions/theme-tomorrow-night-blue/package.nls.json +++ b/extensions/theme-tomorrow-night-blue/package.nls.json @@ -1,5 +1,5 @@ { "displayName": "Tomorrow Night Blue Theme", "description": "Tomorrow night blue theme for Visual Studio Code", - "themeLabel": "Quiet Light" + "themeLabel": "Tomorrow Night Blue" } diff --git a/extensions/typescript-language-features/src/languageFeatures/folding.ts b/extensions/typescript-language-features/src/languageFeatures/folding.ts index 31c3169fa70..5dc6dbd1cca 100644 --- a/extensions/typescript-language-features/src/languageFeatures/folding.ts +++ b/extensions/typescript-language-features/src/languageFeatures/folding.ts @@ -58,7 +58,7 @@ class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { return new vscode.FoldingRange(start, end, kind); } - private static readonly foldEndPairCharacters = ['}', ']', ')']; + private static readonly foldEndPairCharacters = ['}', ']', ')', '`']; private adjustFoldingEnd(range: vscode.Range, document: vscode.TextDocument) { // workaround for #47240 diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 0965a574721..2af23b2700d 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -51,6 +51,9 @@ class ApplyCodeActionCommand implements Command { } } +type ApplyFixAllCodeAction_args = { + readonly action: VsCodeFixAllCodeAction; +}; class ApplyFixAllCodeAction implements Command { public static readonly ID = '_typescript.applyFixAllCodeAction'; @@ -61,14 +64,7 @@ class ApplyFixAllCodeAction implements Command { private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute( - file: string, - tsAction: Proto.CodeFixAction, - ): Promise { - if (!tsAction.fixId) { - return; - } - + public async execute(args: ApplyFixAllCodeAction_args): Promise { /* __GDPR__ "quickFixAll.execute" : { "fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, @@ -78,25 +74,12 @@ class ApplyFixAllCodeAction implements Command { } */ this.telemetryReporter.logTelemetry('quickFixAll.execute', { - fixName: tsAction.fixName + fixName: args.action.tsAction.fixName }); - const args: Proto.GetCombinedCodeFixRequestArgs = { - scope: { - type: 'file', - args: { file } - }, - fixId: tsAction.fixId, - }; - - const response = await this.client.execute('getCombinedCodeFix', args, nulToken); - if (response.type !== 'response' || !response.body) { - return undefined; + if (args.action.combinedResponse) { + await applyCodeActionCommands(this.client, args.action.combinedResponse.body.commands, nulToken); } - - const edit = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body.changes); - await vscode.workspace.applyEdit(edit); - await applyCodeActionCommands(this.client, response.body.commands, nulToken); } } @@ -134,13 +117,25 @@ class VsCodeCodeAction extends vscode.CodeAction { constructor( public readonly tsAction: Proto.CodeFixAction, title: string, - kind: vscode.CodeActionKind, - public readonly isFixAll: boolean, + kind: vscode.CodeActionKind ) { super(title, kind); } } +class VsCodeFixAllCodeAction extends VsCodeCodeAction { + constructor( + tsAction: Proto.CodeFixAction, + public readonly file: string, + title: string, + kind: vscode.CodeActionKind + ) { + super(tsAction, title, kind); + } + + public combinedResponse?: Proto.GetCombinedCodeFixResponse; +} + class CodeActionSet { private readonly _actions = new Set(); private readonly _fixAllActions = new Map<{}, VsCodeCodeAction>(); @@ -202,7 +197,7 @@ class SupportedCodeActionProvider { } } -class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { +class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { public static readonly metadata: vscode.CodeActionProviderMetadata = { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] @@ -257,6 +252,28 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { return allActions; } + public async resolveCodeAction(codeAction: VsCodeCodeAction, token: vscode.CancellationToken): Promise { + if (!(codeAction instanceof VsCodeFixAllCodeAction) || !codeAction.tsAction.fixId) { + return codeAction; + } + + const arg: Proto.GetCombinedCodeFixRequestArgs = { + scope: { + type: 'file', + args: { file: codeAction.file } + }, + fixId: codeAction.tsAction.fixId, + }; + + const response = await this.client.execute('getCombinedCodeFix', arg, token); + if (response.type === 'response') { + codeAction.combinedResponse = response; + codeAction.edit = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body.changes); + } + + return codeAction; + } + private async getFixesForDiagnostic( document: vscode.TextDocument, file: string, @@ -295,7 +312,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { diagnostic: vscode.Diagnostic, tsAction: Proto.CodeFixAction ): VsCodeCodeAction { - const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix, false); + const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix); codeAction.edit = getEditForCodeAction(this.client, tsAction); codeAction.diagnostics = [diagnostic]; codeAction.command = { @@ -328,14 +345,16 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { return results; } - const action = new VsCodeCodeAction( + const action = new VsCodeFixAllCodeAction( tsAction, + file, tsAction.fixAllDescription || localize('fixAllInFileLabel', '{0} (Fix all in file)', tsAction.description), - vscode.CodeActionKind.QuickFix, true); + vscode.CodeActionKind.QuickFix); + action.diagnostics = [diagnostic]; action.command = { command: ApplyFixAllCodeAction.ID, - arguments: [file, tsAction], + arguments: [{ action }], title: '' }; results.addFixAllAction(tsAction.fixId, action); @@ -370,7 +389,7 @@ function isPreferredFix( action: VsCodeCodeAction, allActions: readonly VsCodeCodeAction[] ): boolean { - if (action.isFixAll) { + if (action instanceof VsCodeFixAllCodeAction) { return false; } @@ -384,7 +403,7 @@ function isPreferredFix( return true; } - if (otherAction.isFixAll) { + if (otherAction instanceof VsCodeFixAllCodeAction) { return true; } diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index 752e4ab63ed..228c3d59e63 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -29,7 +29,7 @@ if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { } // Pseudo NLS support -if (nlsConfig.pseudo) { +if (nlsConfig && nlsConfig.pseudo) { loader(['vs/nls'], function (nlsPlugin) { nlsPlugin.setPseudoTranslation(nlsConfig.pseudo); }); diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 97a4d9934cd..0cd75027286 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -81,7 +81,9 @@ function pipeLoggingToParent() { // to start the stacktrace where the console message was being written if (process.env.VSCODE_LOG_STACK === 'true') { const stack = new Error().stack; - argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') }); + if (stack) { + argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') }); + } } try { @@ -114,7 +116,9 @@ function pipeLoggingToParent() { */ function safeSend(arg) { try { - process.send(arg); + if (process.send) { + process.send(arg); + } } catch (error) { // Can happen if the parent channel is closed meanwhile } diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 3fc0f419b97..38867c15c49 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -41,7 +41,7 @@ * extensionDevelopmentPath?: string[], * extensionTestsPath?: string, * userEnv?: { [key: string]: string | undefined }, - * appRoot?: string, + * appRoot: string, * nodeCachedDataDir?: string * }} */ const configuration = JSON.parse(args['config'] || '{}') || {}; @@ -182,7 +182,7 @@ } /** - * @param {boolean} disallowReloadKeybinding + * @param {boolean | undefined} disallowReloadKeybinding * @returns {() => void} */ function registerDeveloperKeybindings(disallowReloadKeybinding) { @@ -203,6 +203,7 @@ const TOGGLE_DEV_TOOLS_KB_ALT = '123'; // F12 const RELOAD_KB = (safeProcess.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R + /** @type {((e: any) => void) | undefined} */ let listener = function (e) { const key = extractKey(e); if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) { diff --git a/src/bootstrap.js b/src/bootstrap.js index 0cb6466aeaf..4ec4c258967 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -42,7 +42,7 @@ //#region Add support for using node_modules.asar /** - * @param {string} appRoot + * @param {string | undefined} appRoot */ function enableASARSupport(appRoot) { if (!path || !Module) { @@ -124,7 +124,7 @@ //#region NLS helpers /** - * @returns {{locale?: string, availableLanguages: {[lang: string]: string;}, pseudo?: boolean }} + * @returns {{locale?: string, availableLanguages: {[lang: string]: string;}, pseudo?: boolean } | undefined} */ function setupNLS() { if (!path || !fs) { @@ -181,7 +181,7 @@ /** * @param {{ portable: string; applicationName: string; }} product - * @returns {{ portableDataPath: string; isPortable: boolean; }} + * @returns {{ portableDataPath: string; isPortable: boolean; } | undefined} */ function configurePortable(product) { if (!path || !fs) { diff --git a/src/main.js b/src/main.js index 2a9756ab2fa..b75dd6ad33f 100644 --- a/src/main.js +++ b/src/main.js @@ -107,7 +107,7 @@ crashReporter.start({ // to ensure that no 'logs' folder is created on disk at a // location outside of the portable directory // (https://github.com/microsoft/vscode/issues/56651) -if (portable.isPortable) { +if (portable && portable.isPortable) { app.setAppLogsPath(path.join(userDataPath, 'logs')); } @@ -145,7 +145,7 @@ const nodeCachedDataDir = getNodeCachedDir(); * Support user defined locale: load it early before app('ready') * to have more things running in parallel. * - * @type {Promise} nlsConfig | undefined + * @type {Promise | undefined} */ let nlsConfigurationPromise = undefined; @@ -359,7 +359,7 @@ function getArgvConfigPath() { /** * @param {NativeParsedArgs} cliArgs - * @returns {string} + * @returns {string | null} */ function getJSFlags(cliArgs) { const jsFlags = []; @@ -387,7 +387,7 @@ function getUserDataPath(cliArgs) { return path.join(portable.portableDataPath, 'user-data'); } - return path.resolve(cliArgs['user-data-dir'] || paths.getDefaultUserDataPath(process.platform)); + return path.resolve(cliArgs['user-data-dir'] || paths.getDefaultUserDataPath()); } /** @@ -468,12 +468,14 @@ function getNodeCachedDir() { } async ensureExists() { - try { - await mkdirp(this.value); + if (typeof this.value === 'string') { + try { + await mkdirp(this.value); - return this.value; - } catch (error) { - // ignore + return this.value; + } catch (error) { + // ignore + } } } diff --git a/src/paths.js b/src/paths.js index a88927d200b..2042123d726 100644 --- a/src/paths.js +++ b/src/paths.js @@ -11,25 +11,38 @@ const path = require('path'); const os = require('os'); /** - * @param {string} platform * @returns {string} */ -function getAppDataPath(platform) { - switch (platform) { - case 'win32': return process.env['VSCODE_APPDATA'] || process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming'); - case 'darwin': return process.env['VSCODE_APPDATA'] || path.join(os.homedir(), 'Library', 'Application Support'); - case 'linux': return process.env['VSCODE_APPDATA'] || process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); - default: throw new Error('Platform not supported'); +function getDefaultUserDataPath() { + + // Support global VSCODE_APPDATA environment variable + let appDataPath = process.env['VSCODE_APPDATA']; + + // Otherwise check per platform + if (!appDataPath) { + switch (process.platform) { + case 'win32': + appDataPath = process.env['APPDATA']; + if (!appDataPath) { + const userProfile = process.env['USERPROFILE']; + if (typeof userProfile !== 'string') { + throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable'); + } + appDataPath = path.join(userProfile, 'AppData', 'Roaming'); + } + break; + case 'darwin': + appDataPath = path.join(os.homedir(), 'Library', 'Application Support'); + break; + case 'linux': + appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); + break; + default: + throw new Error('Platform not supported'); + } } + + return path.join(appDataPath, pkg.name); } -/** - * @param {string} platform - * @returns {string} - */ -function getDefaultUserDataPath(platform) { - return path.join(getAppDataPath(platform), pkg.name); -} - -exports.getAppDataPath = getAppDataPath; exports.getDefaultUserDataPath = getDefaultUserDataPath; diff --git a/src/tsconfig.json b/src/tsconfig.json index 5e81e53802a..85e7f18d516 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -13,6 +13,12 @@ "sinon", "winreg", "trusted-types" + ], + "plugins": [ + { + // the path is relative to TS server, "../../" points to the root dir + "name": "../../node_modules/tsec/lib/tsec_lib/language_service_plugin.js" + } ] }, "include": [ diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index a5390be414d..c2e11fa89f1 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -87,7 +87,6 @@ .monaco-hover .monaco-tokenized-source { white-space: pre-wrap; - word-break: break-all; } .monaco-hover .hover-row.status-bar { diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 6ef2c31036f..0e1ba77f691 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -201,10 +201,11 @@ export class IconLabel extends Disposable { function mouseMove(this: HTMLElement, e: MouseEvent): any { mouseX = e.x; } - function mouseLeave(this: HTMLElement, e: MouseEvent): any { + function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any { isHovering = false; } - const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeave.bind(htmlElement)); + const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); + const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); setTimeout(async () => { if (isHovering && tooltip) { @@ -232,6 +233,7 @@ export class IconLabel extends Disposable { } mouseMoveDisposable.dispose(); mouseLeaveDisposable.dispose(); + mouseDownDisposable.dispose(); }, hoverDelay); } const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 23bfdb2c7bf..133ec935046 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -38,12 +38,14 @@ export interface AbstractScrollbarOptions { visibility: ScrollbarVisibility; extraScrollbarClassName: string; scrollable: Scrollable; + scrollByPage: boolean; } export abstract class AbstractScrollbar extends Widget { protected _host: ScrollbarHost; protected _scrollable: Scrollable; + protected _scrollByPage: boolean; private _lazyRender: boolean; protected _scrollbarState: ScrollbarState; private _visibilityController: ScrollbarVisibilityController; @@ -59,6 +61,7 @@ export abstract class AbstractScrollbar extends Widget { this._lazyRender = opts.lazyRender; this._host = opts.host; this._scrollable = opts.scrollable; + this._scrollByPage = opts.scrollByPage; this._scrollbarState = opts.scrollbarState; this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName)); this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()); @@ -210,7 +213,14 @@ export abstract class AbstractScrollbar extends Widget { offsetX = e.posx - domNodePosition.left; offsetY = e.posy - domNodePosition.top; } - this._setDesiredScrollPositionNow(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(offsetX, offsetY))); + + const offset = this._mouseDownRelativePosition(offsetX, offsetY); + this._setDesiredScrollPositionNow( + this._scrollByPage + ? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset) + : this._scrollbarState.getDesiredScrollPositionFromOffset(offset) + ); + if (e.leftButton) { e.preventDefault(); this._sliderMouseDown(e, () => { /*nothing to do*/ }); diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index 6e7f132e99f..636751f72c6 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -33,7 +33,8 @@ export class HorizontalScrollbar extends AbstractScrollbar { ), visibility: options.horizontal, extraScrollbarClassName: 'horizontal', - scrollable: scrollable + scrollable: scrollable, + scrollByPage: options.scrollByPage }); if (options.horizontalHasArrows) { diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 24fbe00f769..115864f3dfb 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -617,7 +617,9 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto), verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10), verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false), - verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0) + verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0), + + scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false) }; result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize); diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts index afb227be73b..556d2083912 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts @@ -114,6 +114,11 @@ export interface ScrollableElementCreationOptions { * Defaults to false. */ verticalHasArrows?: boolean; + /** + * Scroll gutter clicks move by page vs. jump to position. + * Defaults to false. + */ + scrollByPage?: boolean; } export interface ScrollableElementChangeOptions { @@ -146,4 +151,5 @@ export interface ScrollableElementResolvedOptions { verticalScrollbarSize: number; verticalSliderSize: number; verticalHasArrows: boolean; + scrollByPage: boolean; } diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarState.ts b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts index 48e20a5a033..d0d98b9a810 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts @@ -202,6 +202,28 @@ export class ScrollbarState { return Math.round(desiredSliderPosition / this._computedSliderRatio); } + /** + * Compute a desired `scrollPosition` from if offset is before or after the slider position. + * If offset is before slider, treat as a page up (or left). If after, page down (or right). + * `offset` and `_computedSliderPosition` are based on the same coordinate system. + * `_visibleSize` corresponds to a "page" of lines in the returned coordinate system. + */ + public getDesiredScrollPositionFromOffsetPaged(offset: number): number { + if (!this._computedIsNeeded) { + // no need for a slider + return 0; + } + + let correctedOffset = offset - this._arrowSize; // compensate if has arrows + let desiredScrollPosition = this._scrollPosition; + if (correctedOffset < this._computedSliderPosition) { + desiredScrollPosition -= this._visibleSize; // page up/left + } else { + desiredScrollPosition += this._visibleSize; // page down/right + } + return desiredScrollPosition; + } + /** * Compute a desired `scrollPosition` such that the slider moves by `delta`. */ diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index 296913a3fd8..4f8e8f30a19 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -33,7 +33,8 @@ export class VerticalScrollbar extends AbstractScrollbar { ), visibility: options.vertical, extraScrollbarClassName: 'vertical', - scrollable: scrollable + scrollable: scrollable, + scrollByPage: options.scrollByPage }); if (options.verticalHasArrows) { diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 39b03f06fd6..09430ded67d 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -6,6 +6,8 @@ import { URI } from 'vs/base/common/uri'; import { CharCode } from 'vs/base/common/charCode'; import { compareSubstringIgnoreCase, compare, compareSubstring, compareIgnoreCase } from 'vs/base/common/strings'; +import { isLinux } from 'vs/base/common/platform'; +import { Schemas } from 'vs/base/common/network'; export function getOrSet(map: Map, key: K, value: V): V { let result = map.get(key); @@ -138,7 +140,7 @@ export class UriIterator implements IKeyIterator { private _states: UriIteratorState[] = []; private _stateIdx: number = 0; - constructor(private readonly _ignorePathCasing: boolean) { } + constructor(private readonly _ignorePathCasing: (uri: URI) => boolean) { } reset(key: URI): this { this._value = key; @@ -150,7 +152,7 @@ export class UriIterator implements IKeyIterator { this._states.push(UriIteratorState.Authority); } if (this._value.path) { - this._pathIterator = new PathIterator(false, !this._ignorePathCasing); + this._pathIterator = new PathIterator(false, !this._ignorePathCasing(key)); this._pathIterator.reset(key.path); if (this._pathIterator.value()) { this._states.push(UriIteratorState.Path); @@ -226,7 +228,14 @@ class TernarySearchTreeNode { export class TernarySearchTree { - static forUris(ignorePathCasing: boolean = false): TernarySearchTree { + /** + * @deprecated + */ + static forUris(ignorePathCasing?: boolean): TernarySearchTree { + return new TernarySearchTree(new UriIterator(key => ignorePathCasing ?? (key.scheme === Schemas.file && isLinux))); + } + + static forUris2(ignorePathCasing: (key: URI) => boolean = () => false): TernarySearchTree { return new TernarySearchTree(new UriIterator(ignorePathCasing)); } diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 621c97368c7..3d68dc55942 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -58,6 +58,11 @@ export interface IExtUri { */ getComparisonKey(uri: URI, ignoreFragment?: boolean): string; + /** + * Whether the casing of the path-component of the uri should be ignored. + */ + ignorePathCasing(uri: URI): boolean; + // --- path math basenameOrAuthority(resource: URI): string; @@ -161,6 +166,10 @@ export class ExtUri implements IExtUri { }).toString(); } + ignorePathCasing(uri: URI): boolean { + return this._ignorePathCasing(uri); + } + isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment: boolean = false): boolean { if (base.scheme === parentCandidate.scheme) { if (base.scheme === Schemas.file) { diff --git a/src/vs/base/node/paths.ts b/src/vs/base/node/paths.ts index 977eaf8806f..eaf03e6e400 100644 --- a/src/vs/base/node/paths.ts +++ b/src/vs/base/node/paths.ts @@ -5,12 +5,7 @@ import { FileAccess } from 'vs/base/common/network'; -interface IPaths { - getAppDataPath(platform: string): string; - getDefaultUserDataPath(platform: string): string; -} - const pathsPath = FileAccess.asFileUri('paths', require).fsPath; -const paths = require.__$__nodeRequire(pathsPath); -export const getAppDataPath = paths.getAppDataPath; +const paths = require.__$__nodeRequire<{ getDefaultUserDataPath(): string }>(pathsPath); + export const getDefaultUserDataPath = paths.getDefaultUserDataPath; diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index d0cb6d4286b..d6a7d0446a2 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -223,6 +223,8 @@ * shell specific environment from the OS shell to ensure we are seeing * all development related environment variables. We do this from the * main process because it may involve spawning a shell. + * + * @returns {Promise} */ function resolveEnv() { return new Promise(function (resolve) { diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts index 5ab03d9cd4c..4d3c0944a0c 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts @@ -18,8 +18,11 @@ suite('ScrollbarState', () => { assert.equal(actual.getSliderSize(), 20); assert.equal(actual.getSliderPosition(), 249); - assert.equal(actual.getDesiredScrollPositionFromOffset(259), 32849); + + // 259 is greater than 230 so page down, 32787 + 339 = 33126 + assert.equal(actual.getDesiredScrollPositionFromOffsetPaged(259), 33126); + actual.setScrollPosition(32849); assert.equal(actual.getArrowSize(), 0); assert.equal(actual.getScrollPosition(), 32849); @@ -41,8 +44,11 @@ suite('ScrollbarState', () => { assert.equal(actual.getSliderSize(), 20); assert.equal(actual.getSliderPosition(), 230); - assert.equal(actual.getDesiredScrollPositionFromOffset(240 + 12), 32811); + + // 240 + 12 = 252; greater than 230 so page down, 32787 + 339 = 33126 + assert.equal(actual.getDesiredScrollPositionFromOffsetPaged(240 + 12), 33126); + actual.setScrollPosition(32811); assert.equal(actual.getArrowSize(), 12); assert.equal(actual.getScrollPosition(), 32811); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 2615f098e4c..90b43529204 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -368,7 +368,7 @@ suite('Map', () => { }); test('URIIterator', function () { - const iter = new UriIterator(false); + const iter = new UriIterator(() => false); iter.reset(URI.parse('file:///usr/bin/file.txt')); assert.equal(iter.value(), 'file'); @@ -680,7 +680,7 @@ suite('Map', () => { }); test('TernarySearchTree (URI) - basics', function () { - let trie = new TernarySearchTree(new UriIterator(false)); + let trie = new TernarySearchTree(new UriIterator(() => false)); trie.set(URI.file('/user/foo/bar'), 1); trie.set(URI.file('/user/foo'), 2); @@ -700,7 +700,7 @@ suite('Map', () => { test('TernarySearchTree (URI) - lookup', function () { - const map = new TernarySearchTree(new UriIterator(false)); + const map = new TernarySearchTree(new UriIterator(() => false)); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); map.set(URI.parse('http://foo.bar/user/foo?query'), 2); map.set(URI.parse('http://foo.bar/user/foo?QUERY'), 3); @@ -715,9 +715,19 @@ suite('Map', () => { assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); }); + test('TernarySearchTree (URI) - lookup, casing', function () { + + const map = new TernarySearchTree(new UriIterator(uri => /^https?$/.test(uri.scheme))); + map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); + assert.equal(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); + + map.set(URI.parse('foo://foo.bar/user/foo/bar'), 1); + assert.equal(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); + }); + test('TernarySearchTree (PathSegments) - superstr', function () { - const map = new TernarySearchTree(new UriIterator(false)); + const map = new TernarySearchTree(new UriIterator(() => false)); map.set(URI.file('/user/foo/bar'), 1); map.set(URI.file('/user/foo'), 2); map.set(URI.file('/user/foo/flip/flop'), 3); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 6e753f45e78..233fcfa3a34 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -591,12 +591,13 @@ export class CodeApplication extends Disposable { return undefined; } })).filter(pendingUriToHandle => { - // if URI should be blocked, filter it out + + // If URI should be blocked, filter it out if (this.shouldBlockURI(pendingUriToHandle)) { return false; } - // filter out any protocol link that wants to open as window so that + // Filter out any protocol link that wants to open as window so that // we open the right set of windows on startup and not restore the // previous workspace too. const windowOpenable = this.getWindowOpenableFromProtocolLink(pendingUriToHandle); @@ -614,7 +615,8 @@ export class CodeApplication extends Disposable { const environmentService = this.environmentService; urlService.registerHandler({ async handleURL(uri: URI): Promise { - // if URI should be blocked, behave as if it's handled + + // If URI should be blocked, behave as if it's handled if (app.shouldBlockURI(uri)) { return true; } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index cb61a91ef72..84adca8dd55 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -74,7 +74,14 @@ class EditorOpener implements IOpener { } await this._editorService.openCodeEditor( - { resource: target, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, + { + resource: target, + options: { + selection, + context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API, + ...options?.editorOptions + } + }, this._editorService.getFocusedCodeEditor(), options?.openToSide ); diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 21193bac130..5a6df759b50 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -56,6 +56,7 @@ export class EditorScrollbar extends ViewPart { mouseWheelScrollSensitivity: mouseWheelScrollSensitivity, fastScrollSensitivity: fastScrollSensitivity, scrollPredominantAxis: scrollPredominantAxis, + scrollByPage: scrollbar.scrollByPage, }; this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.getScrollable())); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 734fcf4d94b..eb86a1de662 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -37,7 +37,7 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import * as modes from 'vs/editor/common/modes'; import { editorUnnecessaryCodeBorder, editorUnnecessaryCodeOpacity } from 'vs/editor/common/view/editorColorRegistry'; -import { editorErrorBorder, editorErrorForeground, editorHintBorder, editorHintForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorErrorBorder, editorErrorForeground, editorHintBorder, editorHintForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, editorForeground, editorErrorBackground, editorInfoBackground, editorWarningBackground } from 'vs/platform/theme/common/colorRegistry'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; @@ -1985,6 +1985,10 @@ registerThemingParticipant((theme, collector) => { if (errorForeground) { collector.addRule(`.monaco-editor .${ClassName.EditorErrorDecoration} { background: url("data:image/svg+xml,${getSquigglySVGData(errorForeground)}") repeat-x bottom left; }`); } + const errorBackground = theme.getColor(editorErrorBackground); + if (errorBackground) { + collector.addRule(`.monaco-editor .${ClassName.EditorErrorDecoration}::before { display: block; content: ''; width: 100%; height: 100%; background: ${errorBackground}; }`); + } const warningBorderColor = theme.getColor(editorWarningBorder); if (warningBorderColor) { @@ -1994,6 +1998,10 @@ registerThemingParticipant((theme, collector) => { if (warningForeground) { collector.addRule(`.monaco-editor .${ClassName.EditorWarningDecoration} { background: url("data:image/svg+xml,${getSquigglySVGData(warningForeground)}") repeat-x bottom left; }`); } + const warningBackground = theme.getColor(editorWarningBackground); + if (warningBackground) { + collector.addRule(`.monaco-editor .${ClassName.EditorWarningDecoration}::before { display: block; content: ''; width: 100%; height: 100%; background: ${warningBackground}; }`); + } const infoBorderColor = theme.getColor(editorInfoBorder); if (infoBorderColor) { @@ -2003,6 +2011,10 @@ registerThemingParticipant((theme, collector) => { if (infoForeground) { collector.addRule(`.monaco-editor .${ClassName.EditorInfoDecoration} { background: url("data:image/svg+xml,${getSquigglySVGData(infoForeground)}") repeat-x bottom left; }`); } + const infoBackground = theme.getColor(editorInfoBackground); + if (infoBackground) { + collector.addRule(`.monaco-editor .${ClassName.EditorInfoDecoration}::before { display: block; content: ''; width: 100%; height: 100%; background: ${infoBackground}; }`); + } const hintBorderColor = theme.getColor(editorHintBorder); if (hintBorderColor) { diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index ada211784bf..0e39aa77037 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -296,6 +296,12 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedCodeLens = Boolean(options.modifiedCodeLens); } + if (typeof options.isInEmbeddedEditor !== 'undefined') { + this._contextKeyService.createKey('isInEmbeddedDiffEditor', options.isInEmbeddedEditor); + } else { + this._contextKeyService.createKey('isInEmbeddedDiffEditor', false); + } + this._updateDecorationsRunner = this._register(new RunOnceScheduler(() => this._updateDecorations(), 0)); this._containerDomElement = document.createElement('div'); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index c2c0141e294..1886fd56a09 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -495,6 +495,14 @@ export interface IEditorOptions { * Defaults to true. */ codeLens?: boolean; + /** + * Code lens font family. Defaults to editor font family. + */ + codeLensFontFamily?: string; + /** + * Code lens font size. Default to 90% of the editor font size + */ + codeLensFontSize?: number; /** * Control the behavior and rendering of the code action lightbulb. */ @@ -657,6 +665,11 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to false. */ modifiedCodeLens?: boolean; + /** + * Is the diff editor inside another editor + * Defaults to false + */ + isInEmbeddedEditor?: boolean; } //#endregion @@ -2920,6 +2933,11 @@ export interface IEditorScrollbarOptions { * Defaults to `horizontalScrollbarSize`. */ horizontalSliderSize?: number; + /** + * Scroll gutter clicks move by page vs jump to position. + * Defaults to false. + */ + scrollByPage?: boolean; } export interface InternalEditorScrollbarOptions { @@ -2935,6 +2953,7 @@ export interface InternalEditorScrollbarOptions { readonly horizontalSliderSize: number; readonly verticalScrollbarSize: number; readonly verticalSliderSize: number; + readonly scrollByPage: boolean; } function _scrollbarVisibilityFromString(visibility: string | undefined, defaultValue: ScrollbarVisibility): ScrollbarVisibility { @@ -2965,7 +2984,8 @@ class EditorScrollbar extends BaseEditorOption client.computeMoreMinimalEdits(resource, edits)); result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed())); - return result; + return Promise.race([result, timeout(1000).then(() => edits)]); } else { return Promise.resolve(undefined); diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index b20a0b7c58b..60aca88ad68 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -93,6 +93,6 @@ export function detectModeId(modelService: IModelService, modeService: IModeServ return modeService.getModeIdByFilepathOrFirstLine(resource); } -export function cssEscape(val: string): string { - return val.replace(/\s/g, '\\$&'); // make sure to not introduce CSS classes from files that contain whitespace +export function cssEscape(str: string): string { + return str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 91c07a481a7..c8a841bd212 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -179,114 +179,116 @@ export enum EditorOption { automaticLayout = 9, autoSurround = 10, codeLens = 11, - colorDecorators = 12, - columnSelection = 13, - comments = 14, - contextmenu = 15, - copyWithSyntaxHighlighting = 16, - cursorBlinking = 17, - cursorSmoothCaretAnimation = 18, - cursorStyle = 19, - cursorSurroundingLines = 20, - cursorSurroundingLinesStyle = 21, - cursorWidth = 22, - disableLayerHinting = 23, - disableMonospaceOptimizations = 24, - dragAndDrop = 25, - emptySelectionClipboard = 26, - extraEditorClassName = 27, - fastScrollSensitivity = 28, - find = 29, - fixedOverflowWidgets = 30, - folding = 31, - foldingStrategy = 32, - foldingHighlight = 33, - unfoldOnClickAfterEndOfLine = 34, - fontFamily = 35, - fontInfo = 36, - fontLigatures = 37, - fontSize = 38, - fontWeight = 39, - formatOnPaste = 40, - formatOnType = 41, - glyphMargin = 42, - gotoLocation = 43, - hideCursorInOverviewRuler = 44, - highlightActiveIndentGuide = 45, - hover = 46, - inDiffEditor = 47, - letterSpacing = 48, - lightbulb = 49, - lineDecorationsWidth = 50, - lineHeight = 51, - lineNumbers = 52, - lineNumbersMinChars = 53, - links = 54, - matchBrackets = 55, - minimap = 56, - mouseStyle = 57, - mouseWheelScrollSensitivity = 58, - mouseWheelZoom = 59, - multiCursorMergeOverlapping = 60, - multiCursorModifier = 61, - multiCursorPaste = 62, - occurrencesHighlight = 63, - overviewRulerBorder = 64, - overviewRulerLanes = 65, - padding = 66, - parameterHints = 67, - peekWidgetDefaultFocus = 68, - definitionLinkOpensInPeek = 69, - quickSuggestions = 70, - quickSuggestionsDelay = 71, - readOnly = 72, - renameOnType = 73, - renderControlCharacters = 74, - renderIndentGuides = 75, - renderFinalNewline = 76, - renderLineHighlight = 77, - renderLineHighlightOnlyWhenFocus = 78, - renderValidationDecorations = 79, - renderWhitespace = 80, - revealHorizontalRightPadding = 81, - roundedSelection = 82, - rulers = 83, - scrollbar = 84, - scrollBeyondLastColumn = 85, - scrollBeyondLastLine = 86, - scrollPredominantAxis = 87, - selectionClipboard = 88, - selectionHighlight = 89, - selectOnLineNumbers = 90, - showFoldingControls = 91, - showUnused = 92, - snippetSuggestions = 93, - smartSelect = 94, - smoothScrolling = 95, - stopRenderingLineAfter = 96, - suggest = 97, - suggestFontSize = 98, - suggestLineHeight = 99, - suggestOnTriggerCharacters = 100, - suggestSelection = 101, - tabCompletion = 102, - tabIndex = 103, - unusualLineTerminators = 104, - useTabStops = 105, - wordSeparators = 106, - wordWrap = 107, - wordWrapBreakAfterCharacters = 108, - wordWrapBreakBeforeCharacters = 109, - wordWrapColumn = 110, - wordWrapMinified = 111, - wrappingIndent = 112, - wrappingStrategy = 113, - showDeprecated = 114, - editorClassName = 115, - pixelRatio = 116, - tabFocusMode = 117, - layoutInfo = 118, - wrappingInfo = 119 + codeLensFontFamily = 12, + codeLensFontSize = 13, + colorDecorators = 14, + columnSelection = 15, + comments = 16, + contextmenu = 17, + copyWithSyntaxHighlighting = 18, + cursorBlinking = 19, + cursorSmoothCaretAnimation = 20, + cursorStyle = 21, + cursorSurroundingLines = 22, + cursorSurroundingLinesStyle = 23, + cursorWidth = 24, + disableLayerHinting = 25, + disableMonospaceOptimizations = 26, + dragAndDrop = 27, + emptySelectionClipboard = 28, + extraEditorClassName = 29, + fastScrollSensitivity = 30, + find = 31, + fixedOverflowWidgets = 32, + folding = 33, + foldingStrategy = 34, + foldingHighlight = 35, + unfoldOnClickAfterEndOfLine = 36, + fontFamily = 37, + fontInfo = 38, + fontLigatures = 39, + fontSize = 40, + fontWeight = 41, + formatOnPaste = 42, + formatOnType = 43, + glyphMargin = 44, + gotoLocation = 45, + hideCursorInOverviewRuler = 46, + highlightActiveIndentGuide = 47, + hover = 48, + inDiffEditor = 49, + letterSpacing = 50, + lightbulb = 51, + lineDecorationsWidth = 52, + lineHeight = 53, + lineNumbers = 54, + lineNumbersMinChars = 55, + links = 56, + matchBrackets = 57, + minimap = 58, + mouseStyle = 59, + mouseWheelScrollSensitivity = 60, + mouseWheelZoom = 61, + multiCursorMergeOverlapping = 62, + multiCursorModifier = 63, + multiCursorPaste = 64, + occurrencesHighlight = 65, + overviewRulerBorder = 66, + overviewRulerLanes = 67, + padding = 68, + parameterHints = 69, + peekWidgetDefaultFocus = 70, + definitionLinkOpensInPeek = 71, + quickSuggestions = 72, + quickSuggestionsDelay = 73, + readOnly = 74, + renameOnType = 75, + renderControlCharacters = 76, + renderIndentGuides = 77, + renderFinalNewline = 78, + renderLineHighlight = 79, + renderLineHighlightOnlyWhenFocus = 80, + renderValidationDecorations = 81, + renderWhitespace = 82, + revealHorizontalRightPadding = 83, + roundedSelection = 84, + rulers = 85, + scrollbar = 86, + scrollBeyondLastColumn = 87, + scrollBeyondLastLine = 88, + scrollPredominantAxis = 89, + selectionClipboard = 90, + selectionHighlight = 91, + selectOnLineNumbers = 92, + showFoldingControls = 93, + showUnused = 94, + snippetSuggestions = 95, + smartSelect = 96, + smoothScrolling = 97, + stopRenderingLineAfter = 98, + suggest = 99, + suggestFontSize = 100, + suggestLineHeight = 101, + suggestOnTriggerCharacters = 102, + suggestSelection = 103, + tabCompletion = 104, + tabIndex = 105, + unusualLineTerminators = 106, + useTabStops = 107, + wordSeparators = 108, + wordWrap = 109, + wordWrapBreakAfterCharacters = 110, + wordWrapBreakBeforeCharacters = 111, + wordWrapColumn = 112, + wordWrapMinified = 113, + wrappingIndent = 114, + wrappingStrategy = 115, + showDeprecated = 116, + editorClassName = 117, + pixelRatio = 118, + tabFocusMode = 119, + layoutInfo = 120, + wrappingInfo = 121 } /** diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 360889a81e2..77955b25ac9 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -53,7 +53,7 @@ export class CodeLensContribution implements IEditorContribution { this._disposables.add(this._editor.onDidChangeModel(() => this._onModelChange())); this._disposables.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange())); this._disposables.add(this._editor.onDidChangeConfiguration((e) => { - if (e.hasChanged(EditorOption.fontInfo)) { + if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.codeLensFontSize) || e.hasChanged(EditorOption.codeLensFontFamily)) { this._updateLensStyle(); } if (e.hasChanged(EditorOption.codeLens)) { @@ -77,21 +77,41 @@ export class CodeLensContribution implements IEditorContribution { this._disposables.dispose(); this._oldCodeLensModels.dispose(); this._currentCodeLensModel?.dispose(); + this._styleElement.remove(); + } + + private _getLayoutInfo() { + let fontSize = this._editor.getOption(EditorOption.codeLensFontSize); + let codeLensHeight: number; + if (!fontSize || fontSize < 5) { + fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0; + codeLensHeight = this._editor.getOption(EditorOption.lineHeight); + } else { + codeLensHeight = (fontSize * Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize))) | 0; + } + return { codeLensHeight, fontSize }; } private _updateLensStyle(): void { - const options = this._editor.getOptions(); - const fontInfo = options.get(EditorOption.fontInfo); - const lineHeight = options.get(EditorOption.lineHeight); + const { codeLensHeight, fontSize } = this._getLayoutInfo(); + const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily); - const height = Math.round(lineHeight * 1.1); - const fontSize = Math.round(fontInfo.fontSize * 0.9); - const newStyle = ` - .monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;} - .monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; } + let newStyle = ` + .monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px;} + .monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } `; + if (fontFamily) { + newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`; + } this._styleElement.textContent = newStyle; + + // + this._editor.changeViewZones(accessor => { + for (let lens of this._lenses) { + lens.updateHeight(codeLensHeight, accessor); + } + }); } private _localDispose(): void { @@ -204,6 +224,9 @@ export class CodeLensContribution implements IEditorContribution { // Ask for all references again scheduler.schedule(); })); + this._localToDispose.add(this._editor.onDidFocusEditorWidget(() => { + scheduler.schedule(); + })); this._localToDispose.add(this._editor.onDidScrollChange(e => { if (e.scrollTopChanged && this._lenses.length > 0) { this._resolveCodeLensesInViewportSoon(); @@ -283,6 +306,7 @@ export class CodeLensContribution implements IEditorContribution { } const scrollState = StableEditorScrollState.capture(this._editor); + const layoutInfo = this._getLayoutInfo(); this._editor.changeDecorations(decorationsAccessor => { this._editor.changeViewZones(viewZoneAccessor => { @@ -304,7 +328,7 @@ export class CodeLensContribution implements IEditorContribution { groupsIndex++; codeLensIndex++; } else { - this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._resolveCodeLensesInViewportSoon())); + this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon())); codeLensIndex++; groupsIndex++; } @@ -318,7 +342,7 @@ export class CodeLensContribution implements IEditorContribution { // Create extra symbols while (groupsIndex < groups.length) { - this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._resolveCodeLensesInViewportSoon())); + this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon())); groupsIndex++; } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 215e48f22de..cf6d754271f 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -18,20 +18,20 @@ import { renderCodicons } from 'vs/base/browser/codicons'; class CodeLensViewZone implements IViewZone { - readonly heightInLines: number; readonly suppressMouseDown: boolean; readonly domNode: HTMLElement; afterLineNumber: number; + heightInPx: number; private _lastHeight?: number; - private readonly _onHeight: Function; + private readonly _onHeight: () => void; - constructor(afterLineNumber: number, onHeight: Function) { + constructor(afterLineNumber: number, heightInPx: number, onHeight: () => void) { this.afterLineNumber = afterLineNumber; - this._onHeight = onHeight; + this.heightInPx = heightInPx; - this.heightInLines = 1; + this._onHeight = onHeight; this.suppressMouseDown = true; this.domNode = document.createElement('div'); } @@ -179,8 +179,8 @@ export class CodeLensWidget { private readonly _editor: IActiveCodeEditor; private readonly _className: string; - private readonly _viewZone!: CodeLensViewZone; - private readonly _viewZoneId!: string; + private readonly _viewZone: CodeLensViewZone; + private readonly _viewZoneId: string; private _contentWidget?: CodeLensContentWidget; private _decorationIds: string[]; @@ -193,7 +193,8 @@ export class CodeLensWidget { className: string, helper: CodeLensHelper, viewZoneChangeAccessor: IViewZoneChangeAccessor, - updateCallback: Function + heightInPx: number, + updateCallback: () => void ) { this._editor = editor; this._className = className; @@ -224,7 +225,7 @@ export class CodeLensWidget { } }); - this._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, updateCallback); + this._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, heightInPx, updateCallback); this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone); if (lenses.length > 0) { @@ -277,6 +278,14 @@ export class CodeLensWidget { }); } + updateHeight(height: number, viewZoneChangeAccessor: IViewZoneChangeAccessor): void { + this._viewZone.heightInPx = height; + viewZoneChangeAccessor.layoutZone(this._viewZoneId); + if (this._contentWidget) { + this._editor.layoutContentWidget(this._contentWidget); + } + } + computeIfNecessary(model: ITextModel): CodeLensItem[] | null { if (!this._viewZone.domNode.hasAttribute('monaco-visible-view-zone')) { return null; diff --git a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts index 4bcdaa3974d..454b3d33992 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts @@ -335,7 +335,7 @@ export class ColorPickerWidget extends Widget { body: ColorPickerBody; - constructor(container: Node, private readonly model: ColorPickerModel, private pixelRatio: number, themeService: IThemeService) { + constructor(container: Node, readonly model: ColorPickerModel, private pixelRatio: number, themeService: IThemeService) { super(); this._register(onDidChangeZoomLevel(() => this.layout())); diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index a6d26405df6..db45b86d8fc 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -83,6 +83,10 @@ export class CommonFindController extends Disposable implements IEditorContribut private readonly _clipboardService: IClipboardService; protected readonly _contextKeyService: IContextKeyService; + get editor() { + return this._editor; + } + public static get(editor: ICodeEditor): CommonFindController { return editor.getContribution(CommonFindController.ID); } @@ -583,7 +587,13 @@ export class NextMatchFindAction extends MatchFindAction { } protected _run(controller: CommonFindController): boolean { - return controller.moveToNextMatch(); + const result = controller.moveToNextMatch(); + if (result) { + controller.editor.pushUndoStop(); + return true; + } + + return false; } } @@ -604,7 +614,13 @@ export class NextMatchFindAction2 extends MatchFindAction { } protected _run(controller: CommonFindController): boolean { - return controller.moveToNextMatch(); + const result = controller.moveToNextMatch(); + if (result) { + controller.editor.pushUndoStop(); + return true; + } + + return false; } } diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index bedb20cdd97..cdfdac89bbb 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -215,13 +215,12 @@ class FormatDocumentAction extends EditorAction { alias: 'Format Document', precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor, EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider), kbOpts: { - kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentFormattingProvider), + kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, weight: KeybindingWeight.EditorContrib }, contextMenuOpts: { - when: EditorContextKeys.hasDocumentFormattingProvider, group: '1_modification', order: 1.3 } @@ -249,12 +248,12 @@ class FormatSelectionAction extends EditorAction { alias: 'Format Selection', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), kbOpts: { - kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentSelectionFormattingProvider), + kbExpr: EditorContextKeys.editorTextFocus, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F), weight: KeybindingWeight.EditorContrib }, contextMenuOpts: { - when: ContextKeyExpr.and(EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasNonEmptySelection), + when: EditorContextKeys.hasNonEmptySelection, group: '1_modification', order: 1.31 } diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index 232aab8d0f8..83503350528 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -162,6 +162,9 @@ abstract class SymbolNavigationAction extends EditorAction { if (!range) { range = reference.range; } + if (!range) { + return undefined; + } const targetEditor = await editorService.openCodeEditor({ resource: reference.uri, diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index a7aaa87c526..b0231c8fa34 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -367,7 +367,7 @@ KeybindingsRegistry.registerKeybindingRule({ }); KeybindingsRegistry.registerKeybindingRule({ id: 'closeReferenceSearch', - weight: KeybindingWeight.EditorContrib + 50, + weight: KeybindingWeight.WorkbenchContrib + 50, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(ctxReferenceSearchVisible, ContextKeyExpr.not('config.editor.stablePeek')) @@ -376,7 +376,7 @@ KeybindingsRegistry.registerKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'revealReference', - weight: KeybindingWeight.EditorContrib, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter, mac: { primary: KeyCode.Enter, diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 2cb7e7938c5..4a8c03b7135 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -251,6 +251,23 @@ export class ModesContentHoverWidget extends ContentHoverWidget { })); this._register(TokenizationRegistry.onDidChange((e) => { if (this.isVisible && this._lastRange && this._messages.length > 0) { + this._messages = this._messages.map(msg => { + // If a color hover is visible, we need to update the message that + // created it so that the color matches the last chosen color + if (msg instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.range) && this._colorPicker?.model.color) { + const color = this._colorPicker.model.color; + const newColor = { + red: color.rgba.r / 255, + green: color.rgba.g / 255, + blue: color.rgba.b / 255, + alpha: color.rgba.a + }; + return new ColorHover(msg.range, newColor, msg.provider); + } else { + return msg; + } + }); + this._hover.contentsDomNode.textContent = ''; this._renderMessages(this._lastRange, this._messages); } diff --git a/src/vs/editor/contrib/message/messageController.css b/src/vs/editor/contrib/message/messageController.css index a3910415dd4..924349d112e 100644 --- a/src/vs/editor/contrib/message/messageController.css +++ b/src/vs/editor/contrib/message/messageController.css @@ -8,6 +8,12 @@ z-index: 10000; } +.monaco-editor .monaco-editor-overlaymessage.below { + padding-bottom: 0; + padding-top: 8px; + z-index: 10000; +} + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } @@ -37,3 +43,13 @@ border-width: 8px; position: absolute; } + +.monaco-editor .monaco-editor-overlaymessage:not(.below) .anchor.top, +.monaco-editor .monaco-editor-overlaymessage.below .anchor.below { + display: none; +} + +.monaco-editor .monaco-editor-overlaymessage.below .anchor.top { + display: inherit; + top: -8px; +} diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index 6bec24fd00f..319f0d521d7 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -7,7 +7,7 @@ import 'vs/css!./messageController'; import * as nls from 'vs/nls'; import { TimeoutTimer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; @@ -20,7 +20,7 @@ import { inputValidationInfoBorder, inputValidationInfoBackground, inputValidati import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -export class MessageController extends Disposable implements IEditorContribution { +export class MessageController implements IEditorContribution { public static readonly ID = 'editor.contrib.messageController'; @@ -32,21 +32,24 @@ export class MessageController extends Disposable implements IEditorContribution private readonly _editor: ICodeEditor; private readonly _visible: IContextKey; - private readonly _messageWidget = this._register(new MutableDisposable()); - private readonly _messageListeners = this._register(new DisposableStore()); + private readonly _messageWidget = new MutableDisposable(); + private readonly _messageListeners = new DisposableStore(); + private readonly _editorListener: IDisposable; constructor( editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService ) { - super(); + this._editor = editor; this._visible = MessageController.MESSAGE_VISIBLE.bindTo(contextKeyService); - this._register(this._editor.onDidAttemptReadOnlyEdit(() => this._onDidAttemptReadOnlyEdit())); + this._editorListener = this._editor.onDidAttemptReadOnlyEdit(() => this._onDidAttemptReadOnlyEdit()); } dispose(): void { - super.dispose(); + this._editorListener.dispose(); + this._messageListeners.dispose(); + this._messageWidget.dispose(); this._visible.reset(); } @@ -150,14 +153,18 @@ class MessageWidget implements IContentWidget { this._domNode = document.createElement('div'); this._domNode.classList.add('monaco-editor-overlaymessage'); + const anchorTop = document.createElement('div'); + anchorTop.classList.add('anchor', 'top'); + this._domNode.appendChild(anchorTop); + const message = document.createElement('div'); message.classList.add('message'); message.textContent = text; this._domNode.appendChild(message); - const anchor = document.createElement('div'); - anchor.classList.add('anchor'); - this._domNode.appendChild(anchor); + const anchorBottom = document.createElement('div'); + anchorBottom.classList.add('anchor', 'below'); + this._domNode.appendChild(anchorBottom); this._editor.addContentWidget(this); this._domNode.classList.add('fadeIn'); @@ -178,6 +185,11 @@ class MessageWidget implements IContentWidget { getPosition(): IContentWidgetPosition { return { position: this._position, preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW] }; } + + afterRender(position: ContentWidgetPositionPreference | null): void { + this._domNode.classList.toggle('below', position === ContentWidgetPositionPreference.BELOW); + } + } registerEditorContribution(MessageController.ID, MessageController); @@ -186,7 +198,8 @@ registerThemingParticipant((theme, collector) => { const border = theme.getColor(inputValidationInfoBorder); if (border) { let borderWidth = theme.type === ColorScheme.HIGH_CONTRAST ? 2 : 1; - collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor { border-top-color: ${border}; }`); + collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.below { border-top-color: ${border}; }`); + collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.top { border-bottom-color: ${border}; }`); collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { border: ${borderWidth}px solid ${border}; }`); } const background = theme.getColor(inputValidationInfoBackground); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts index 72500145f14..642c9fddb0e 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts @@ -91,6 +91,7 @@ export class SuggestDetailsWidget { const lineHeightPx = `${lineHeight}px`; this.domNode.style.fontSize = fontSizePx; + this.domNode.style.lineHeight = lineHeightPx; this.domNode.style.fontWeight = fontWeight; this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; this._type.style.fontFamily = fontFamily; diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index c071f28aa7d..01e42685a92 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -214,7 +214,7 @@ export class ItemRenderer implements IListRenderer { horizontalSliderSize: EditorOptions.scrollbar.defaultValue.horizontalSliderSize, verticalScrollbarSize: input.verticalScrollbarWidth, verticalSliderSize: EditorOptions.scrollbar.defaultValue.verticalSliderSize, + scrollByPage: EditorOptions.scrollbar.defaultValue.scrollByPage, }; options._write(EditorOption.scrollbar, scrollbarOptions); const lineNumbersOptions: InternalEditorRenderLineNumbersOptions = { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a932e9f18de..a5a2fa1c36e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3034,6 +3034,14 @@ declare namespace monaco.editor { * Defaults to true. */ codeLens?: boolean; + /** + * Code lens font family. Defaults to editor font family. + */ + codeLensFontFamily?: string; + /** + * Code lens font size. Default to 90% of the editor font size + */ + codeLensFontSize?: number; /** * Control the behavior and rendering of the code action lightbulb. */ @@ -3190,6 +3198,11 @@ declare namespace monaco.editor { * Defaults to false. */ modifiedCodeLens?: boolean; + /** + * Is the diff editor inside another editor + * Defaults to false + */ + isInEmbeddedEditor?: boolean; } /** @@ -3660,6 +3673,11 @@ declare namespace monaco.editor { * Defaults to `horizontalScrollbarSize`. */ horizontalSliderSize?: number; + /** + * Scroll gutter clicks move by page vs jump to position. + * Defaults to false. + */ + scrollByPage?: boolean; } export interface InternalEditorScrollbarOptions { @@ -3675,6 +3693,7 @@ declare namespace monaco.editor { readonly horizontalSliderSize: number; readonly verticalScrollbarSize: number; readonly verticalSliderSize: number; + readonly scrollByPage: boolean; } /** @@ -3712,7 +3731,7 @@ declare namespace monaco.editor { /** * Show details inline with the label. Defaults to true. */ - showStatusDetailsInline?: boolean; + showInlineDetails?: boolean; /** * Show method-suggestions. */ @@ -3873,114 +3892,116 @@ declare namespace monaco.editor { automaticLayout = 9, autoSurround = 10, codeLens = 11, - colorDecorators = 12, - columnSelection = 13, - comments = 14, - contextmenu = 15, - copyWithSyntaxHighlighting = 16, - cursorBlinking = 17, - cursorSmoothCaretAnimation = 18, - cursorStyle = 19, - cursorSurroundingLines = 20, - cursorSurroundingLinesStyle = 21, - cursorWidth = 22, - disableLayerHinting = 23, - disableMonospaceOptimizations = 24, - dragAndDrop = 25, - emptySelectionClipboard = 26, - extraEditorClassName = 27, - fastScrollSensitivity = 28, - find = 29, - fixedOverflowWidgets = 30, - folding = 31, - foldingStrategy = 32, - foldingHighlight = 33, - unfoldOnClickAfterEndOfLine = 34, - fontFamily = 35, - fontInfo = 36, - fontLigatures = 37, - fontSize = 38, - fontWeight = 39, - formatOnPaste = 40, - formatOnType = 41, - glyphMargin = 42, - gotoLocation = 43, - hideCursorInOverviewRuler = 44, - highlightActiveIndentGuide = 45, - hover = 46, - inDiffEditor = 47, - letterSpacing = 48, - lightbulb = 49, - lineDecorationsWidth = 50, - lineHeight = 51, - lineNumbers = 52, - lineNumbersMinChars = 53, - links = 54, - matchBrackets = 55, - minimap = 56, - mouseStyle = 57, - mouseWheelScrollSensitivity = 58, - mouseWheelZoom = 59, - multiCursorMergeOverlapping = 60, - multiCursorModifier = 61, - multiCursorPaste = 62, - occurrencesHighlight = 63, - overviewRulerBorder = 64, - overviewRulerLanes = 65, - padding = 66, - parameterHints = 67, - peekWidgetDefaultFocus = 68, - definitionLinkOpensInPeek = 69, - quickSuggestions = 70, - quickSuggestionsDelay = 71, - readOnly = 72, - renameOnType = 73, - renderControlCharacters = 74, - renderIndentGuides = 75, - renderFinalNewline = 76, - renderLineHighlight = 77, - renderLineHighlightOnlyWhenFocus = 78, - renderValidationDecorations = 79, - renderWhitespace = 80, - revealHorizontalRightPadding = 81, - roundedSelection = 82, - rulers = 83, - scrollbar = 84, - scrollBeyondLastColumn = 85, - scrollBeyondLastLine = 86, - scrollPredominantAxis = 87, - selectionClipboard = 88, - selectionHighlight = 89, - selectOnLineNumbers = 90, - showFoldingControls = 91, - showUnused = 92, - snippetSuggestions = 93, - smartSelect = 94, - smoothScrolling = 95, - stopRenderingLineAfter = 96, - suggest = 97, - suggestFontSize = 98, - suggestLineHeight = 99, - suggestOnTriggerCharacters = 100, - suggestSelection = 101, - tabCompletion = 102, - tabIndex = 103, - unusualLineTerminators = 104, - useTabStops = 105, - wordSeparators = 106, - wordWrap = 107, - wordWrapBreakAfterCharacters = 108, - wordWrapBreakBeforeCharacters = 109, - wordWrapColumn = 110, - wordWrapMinified = 111, - wrappingIndent = 112, - wrappingStrategy = 113, - showDeprecated = 114, - editorClassName = 115, - pixelRatio = 116, - tabFocusMode = 117, - layoutInfo = 118, - wrappingInfo = 119 + codeLensFontFamily = 12, + codeLensFontSize = 13, + colorDecorators = 14, + columnSelection = 15, + comments = 16, + contextmenu = 17, + copyWithSyntaxHighlighting = 18, + cursorBlinking = 19, + cursorSmoothCaretAnimation = 20, + cursorStyle = 21, + cursorSurroundingLines = 22, + cursorSurroundingLinesStyle = 23, + cursorWidth = 24, + disableLayerHinting = 25, + disableMonospaceOptimizations = 26, + dragAndDrop = 27, + emptySelectionClipboard = 28, + extraEditorClassName = 29, + fastScrollSensitivity = 30, + find = 31, + fixedOverflowWidgets = 32, + folding = 33, + foldingStrategy = 34, + foldingHighlight = 35, + unfoldOnClickAfterEndOfLine = 36, + fontFamily = 37, + fontInfo = 38, + fontLigatures = 39, + fontSize = 40, + fontWeight = 41, + formatOnPaste = 42, + formatOnType = 43, + glyphMargin = 44, + gotoLocation = 45, + hideCursorInOverviewRuler = 46, + highlightActiveIndentGuide = 47, + hover = 48, + inDiffEditor = 49, + letterSpacing = 50, + lightbulb = 51, + lineDecorationsWidth = 52, + lineHeight = 53, + lineNumbers = 54, + lineNumbersMinChars = 55, + links = 56, + matchBrackets = 57, + minimap = 58, + mouseStyle = 59, + mouseWheelScrollSensitivity = 60, + mouseWheelZoom = 61, + multiCursorMergeOverlapping = 62, + multiCursorModifier = 63, + multiCursorPaste = 64, + occurrencesHighlight = 65, + overviewRulerBorder = 66, + overviewRulerLanes = 67, + padding = 68, + parameterHints = 69, + peekWidgetDefaultFocus = 70, + definitionLinkOpensInPeek = 71, + quickSuggestions = 72, + quickSuggestionsDelay = 73, + readOnly = 74, + renameOnType = 75, + renderControlCharacters = 76, + renderIndentGuides = 77, + renderFinalNewline = 78, + renderLineHighlight = 79, + renderLineHighlightOnlyWhenFocus = 80, + renderValidationDecorations = 81, + renderWhitespace = 82, + revealHorizontalRightPadding = 83, + roundedSelection = 84, + rulers = 85, + scrollbar = 86, + scrollBeyondLastColumn = 87, + scrollBeyondLastLine = 88, + scrollPredominantAxis = 89, + selectionClipboard = 90, + selectionHighlight = 91, + selectOnLineNumbers = 92, + showFoldingControls = 93, + showUnused = 94, + snippetSuggestions = 95, + smartSelect = 96, + smoothScrolling = 97, + stopRenderingLineAfter = 98, + suggest = 99, + suggestFontSize = 100, + suggestLineHeight = 101, + suggestOnTriggerCharacters = 102, + suggestSelection = 103, + tabCompletion = 104, + tabIndex = 105, + unusualLineTerminators = 106, + useTabStops = 107, + wordSeparators = 108, + wordWrap = 109, + wordWrapBreakAfterCharacters = 110, + wordWrapBreakBeforeCharacters = 111, + wordWrapColumn = 112, + wordWrapMinified = 113, + wrappingIndent = 114, + wrappingStrategy = 115, + showDeprecated = 116, + editorClassName = 117, + pixelRatio = 118, + tabFocusMode = 119, + layoutInfo = 120, + wrappingInfo = 121 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -3995,6 +4016,8 @@ declare namespace monaco.editor { automaticLayout: IEditorOption; autoSurround: IEditorOption; codeLens: IEditorOption; + codeLensFontFamily: IEditorOption; + codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; columnSelection: IEditorOption; comments: IEditorOption; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index d2d8ca659a8..e7b4a45e43d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -43,8 +43,6 @@ export const OPTIONS: OptionDescriptions> = { 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, - '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)") }, 'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") }, 'waitMarkerFilePath': { type: 'string' }, 'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") }, @@ -79,6 +77,9 @@ export const OPTIONS: OptionDescriptions> = { 'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, 'remote': { type: 'string' }, + 'folder-uri': { type: 'string[]', cat: 'o', args: 'uri' }, + 'file-uri': { type: 'string[]', cat: 'o', args: 'uri' }, + 'locate-extension': { type: 'string[]' }, 'extensionDevelopmentPath': { type: 'string[]' }, 'extensionTestsPath': { type: 'string' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 1e67add4612..76e81a13ce7 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -245,5 +245,5 @@ export function parsePathArg(arg: string | undefined, process: NodeJS.Process): } export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string { - return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath(process.platform)); + return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath()); } diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index b36a5aac681..8648504a3c5 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -180,6 +180,7 @@ export class FileService extends Disposable implements IFileService { private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise; private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise { const provider = await this.withProvider(resource); + const isPathCaseSensitive = this.isPathCaseSensitive(provider); const resolveTo = options?.resolveTo; const resolveSingleChildDescendants = options?.resolveSingleChildDescendants; @@ -193,7 +194,7 @@ export class FileService extends Disposable implements IFileService { // lazy trie to check for recursive resolving if (!trie) { - trie = TernarySearchTree.forUris(); + trie = TernarySearchTree.forUris2(() => !isPathCaseSensitive); trie.set(resource, true); if (isNonEmptyArray(resolveTo)) { resolveTo.forEach(uri => trie!.set(uri, true)); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ed480b1c068..4232528124a 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -521,19 +521,19 @@ export class FileChangesEvent { switch (change.type) { case FileChangeType.ADDED: if (!this.added) { - this.added = TernarySearchTree.forUris(this.ignorePathCasing); + this.added = TernarySearchTree.forUris2(() => this.ignorePathCasing); } this.added.set(change.resource, change); break; case FileChangeType.UPDATED: if (!this.updated) { - this.updated = TernarySearchTree.forUris(this.ignorePathCasing); + this.updated = TernarySearchTree.forUris2(() => this.ignorePathCasing); } this.updated.set(change.resource, change); break; case FileChangeType.DELETED: if (!this.deleted) { - this.deleted = TernarySearchTree.forUris(this.ignorePathCasing); + this.deleted = TernarySearchTree.forUris2(() => this.ignorePathCasing); } this.deleted.set(change.resource, change); break; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index c71a163ff96..94b55b1829d 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -833,7 +833,7 @@ function workbenchTreeDataPreamble(treeExpandOnFolderClick) + expandOnlyOnTwistieClick: options.expandOnlyOnTwistieClick ?? !configurationService.getValue(treeExpandOnFolderClick) } as TOptions }; } @@ -935,7 +935,7 @@ class WorkbenchTreeInternals { if (e.affectsConfiguration(openModeSettingKey)) { newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' }; } - if (e.affectsConfiguration(treeExpandOnFolderClick)) { + if (e.affectsConfiguration(treeExpandOnFolderClick) && options.expandOnlyOnTwistieClick === undefined) { newOptions = { ...newOptions, expandOnlyOnTwistieClick: !configurationService.getValue(treeExpandOnFolderClick) }; } if (Object.keys(newOptions).length > 0) { diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 977cb79b80d..d82651f93be 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; export const IOpenerService = createDecorator('openerService'); @@ -18,6 +19,11 @@ type OpenInternalOptions = { */ readonly openToSide?: boolean; + /** + * Extra editor options to apply in case an editor is used to open. + */ + readonly editorOptions?: IEditorOptions; + /** * Signals that the editor to open was triggered through a user * action, such as keyboard or mouse usage. diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 322c486f095..e682f79b8e4 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -243,12 +243,15 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); +export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hc: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F48771', light: '#E51400', hc: null }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hc: Color.fromHex('#E47777').transparent(0.8) }, nls.localize('errorBorder', 'Border color of error boxes in the editor.')); +export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hc: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#E9A700', hc: null }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hc: Color.fromHex('#FFCC00').transparent(0.8) }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.')); +export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hc: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#75BEFF', light: '#75BEFF', hc: null }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hc: Color.fromHex('#75BEFF').transparent(0.8) }, nls.localize('infoBorder', 'Border color of info boxes in the editor.')); diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 8963865f3b6..476201f8fc6 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -11,6 +11,14 @@ import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMappi export const IWebviewManagerService = createDecorator('webviewManagerService'); +export interface WebviewWebContentsId { + readonly webContentsId: number; +} + +export interface WebviewWindowId { + readonly windowId: number; +} + export interface IWebviewManagerService { _serviceBrand: unknown; @@ -20,7 +28,7 @@ export interface IWebviewManagerService { didLoadResource(requestId: number, content: VSBuffer | undefined): void; - setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise; + setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise; } export interface RegisterWebviewMetadata { diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index c3b49724aba..03a6e306cac 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { webContents } from 'electron'; +import { WebContents, webContents } from 'electron'; import { VSBuffer } from 'vs/base/common/buffer'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; -import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService'; +import { IWebviewManagerService, RegisterWebviewMetadata, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider'; import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -26,7 +26,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer @IFileService fileService: IFileService, @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, - @IWindowsMainService windowsMainService: IWindowsMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService)); @@ -70,11 +70,24 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer }); } - public async setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise { - const contents = webContents.fromId(webContentsId); - if (!contents) { - throw new Error(`Invalid webContentsId: ${webContentsId}`); + public async setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise { + let contents: WebContents | undefined; + + if (typeof (id as WebviewWindowId).windowId === 'number') { + const { windowId } = (id as WebviewWindowId); + const window = this.windowsMainService.getWindowById(windowId); + if (!window) { + throw new Error(`Invalid windowId: ${windowId}`); + } + contents = window.win.webContents; + } else { + const { webContentsId } = (id as WebviewWebContentsId); + contents = webContents.fromId(webContentsId); + if (!contents) { + throw new Error(`Invalid webContentsId: ${webContentsId}`); + } } + if (!contents.isDestroyed()) { contents.setIgnoreMenuShortcuts(enabled); } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index b87cc3744ea..6d622945d68 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -72,7 +72,7 @@ interface IOpenBrowserWindowOptions { initialStartup?: boolean; - fileInputs?: IFileInputs; + filesToOpen?: IFilesToOpen; forceNewWindow?: boolean; forceNewTabbedWindow?: boolean; @@ -87,7 +87,7 @@ interface IPathParseOptions { remoteAuthority?: string; } -interface IFileInputs { +interface IFilesToOpen { filesToOpenOrCreate: IPath[]; filesToDiff: IPath[]; filesToWait?: IPathsToWaitFor; @@ -385,17 +385,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.logService.trace('windowsManager#open'); openConfig = this.validateOpenConfig(openConfig); - const pathsToOpen = this.getPathsToOpen(openConfig); - this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); - const foldersToAdd: IFolderPathToOpen[] = []; const foldersToOpen: IFolderPathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; const workspacesToRestore: IWorkspacePathToOpen[] = []; - const emptyToRestore: IEmptyWindowBackupInfo[] = []; // empty windows with backupPath + const emptyToRestore: IEmptyWindowBackupInfo[] = []; + let filesToOpen: IFilesToOpen | undefined; + let emptyToOpen = 0; - let emptyToOpen: number = 0; - let fileInputs: IFileInputs | undefined; // collect all file inputs + // Identify things to open from open config + const pathsToOpen = this.getPathsToOpen(openConfig); + this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { if (isFolderPathToOpen(path)) { if (openConfig.addMode) { @@ -408,10 +408,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } else if (isWorkspacePathToOpen(path)) { workspacesToOpen.push(path); } else if (path.fileUri) { - if (!fileInputs) { - fileInputs = { filesToOpenOrCreate: [], filesToDiff: [], remoteAuthority: path.remoteAuthority }; + if (!filesToOpen) { + filesToOpen = { filesToOpenOrCreate: [], filesToDiff: [], remoteAuthority: path.remoteAuthority }; } - fileInputs.filesToOpenOrCreate.push(path); + filesToOpen.filesToOpenOrCreate.push(path); } else if (path.backupPath) { emptyToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority }); } else { @@ -421,14 +421,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // When run with --diff, take the files to open as files to diff // if there are exactly two files provided. - if (fileInputs && openConfig.diffMode && fileInputs.filesToOpenOrCreate.length === 2) { - fileInputs.filesToDiff = fileInputs.filesToOpenOrCreate; - fileInputs.filesToOpenOrCreate = []; + if (openConfig.diffMode && filesToOpen?.filesToOpenOrCreate.length === 2) { + filesToOpen.filesToDiff = filesToOpen.filesToOpenOrCreate; + filesToOpen.filesToOpenOrCreate = []; } // When run with --wait, make sure we keep the paths to wait for - if (fileInputs && openConfig.waitMarkerFileURI) { - fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; + if (filesToOpen && openConfig.waitMarkerFileURI) { + filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } // @@ -447,7 +447,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, fileInputs, foldersToAdd); + const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyToRestore.length}, emptyToOpen: ${emptyToOpen})`); @@ -492,7 +492,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Remember in recent document list (unless this opens for extension development) // Also do not add paths when files are opened for diffing, only if opened individually - const isDiff = fileInputs && fileInputs.filesToDiff.length > 0; + const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0; if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !openConfig.noRecentEntry) { const recents: IRecent[] = []; for (let pathToOpen of pathsToOpen) { @@ -535,7 +535,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic foldersToOpen: IFolderPathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], emptyToOpen: number, - fileInputs: IFileInputs | undefined, + filesToOpen: IFilesToOpen | undefined, foldersToAdd: IFolderPathToOpen[] ) { const usedWindows: ICodeWindow[] = []; @@ -554,13 +554,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Handle files to open/diff or to create when we dont open a folder and we do not restore any folder/untitled from hot-exit const potentialWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; - if (potentialWindowsCount === 0 && fileInputs) { + if (potentialWindowsCount === 0 && filesToOpen) { // Find suitable window or folder path to open files in - const fileToCheck = fileInputs.filesToOpenOrCreate[0] || fileInputs.filesToDiff[0]; + const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0]; // only look at the windows with correct authority - const windows = WindowsMainService.WINDOWS.filter(window => fileInputs && window.remoteAuthority === fileInputs.remoteAuthority); + const windows = WindowsMainService.WINDOWS.filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); const bestWindowOrFolder = findBestWindowOrFolderForFile({ windows, @@ -587,10 +587,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic else { // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, fileInputs)); + usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen)); // Reset these because we handled them - fileInputs = undefined; + filesToOpen = undefined; } } @@ -600,14 +600,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - fileInputs, + filesToOpen, forceNewWindow: true, - remoteAuthority: fileInputs.remoteAuthority, + remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow })); // Reset these because we handled them - fileInputs = undefined; + filesToOpen = undefined; } } @@ -619,14 +619,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowsOnWorkspace = arrays.coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; - const fileInputsForWindow = (fileInputs?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? fileInputs : undefined; + const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, fileInputsForWindow)); + usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow)); // Reset these because we handled them - if (fileInputsForWindow) { - fileInputs = undefined; + if (filesToOpenInWindow) { + filesToOpen = undefined; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -639,14 +639,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } const remoteAuthority = workspaceToOpen.remoteAuthority; - const fileInputsForWindow = (fileInputs?.remoteAuthority === remoteAuthority) ? fileInputs : undefined; + const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, fileInputsForWindow)); + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow)); // Reset these because we handled them - if (fileInputsForWindow) { - fileInputs = undefined; + if (filesToOpenInWindow) { + filesToOpen = undefined; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -661,14 +661,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowsOnFolderPath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; - const fileInputsForWindow = fileInputs?.remoteAuthority === windowOnFolderPath.remoteAuthority ? fileInputs : undefined; + const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, fileInputsForWindow)); + usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow)); // Reset these because we handled them - if (fileInputsForWindow) { - fileInputs = undefined; + if (filesToOpenInWindow) { + filesToOpen = undefined; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -682,14 +682,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } const remoteAuthority = folderToOpen.remoteAuthority; - const fileInputsForWindow = (fileInputs?.remoteAuthority === remoteAuthority) ? fileInputs : undefined; + const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, fileInputsForWindow)); + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow)); // Reset these because we handled them - if (fileInputsForWindow) { - fileInputs = undefined; + if (filesToOpenInWindow) { + filesToOpen = undefined; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -701,13 +701,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allEmptyToRestore.length > 0) { allEmptyToRestore.forEach(emptyWindowBackupInfo => { const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; - const fileInputsForWindow = (fileInputs?.remoteAuthority === remoteAuthority) ? fileInputs : undefined; + const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - fileInputs: fileInputsForWindow, + filesToOpen: filesToOpenInWindow, remoteAuthority, forceNewWindow: true, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, @@ -715,8 +715,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic })); // Reset these because we handled them - if (fileInputsForWindow) { - fileInputs = undefined; + if (filesToOpenInWindow) { + filesToOpen = undefined; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -724,18 +724,18 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Handle empty to open (only if no other window opened) - if (usedWindows.length === 0 || fileInputs) { - if (fileInputs && !emptyToOpen) { + if (usedWindows.length === 0 || filesToOpen) { + if (filesToOpen && !emptyToOpen) { emptyToOpen++; } - const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); + const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, fileInputs)); + usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen)); // Reset these because we handled them - fileInputs = undefined; + filesToOpen = undefined; openFolderInNewWindow = true; // any other window to open must open in new window then } } @@ -743,16 +743,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return arrays.distinct(usedWindows); } - private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, fileInputs?: IFileInputs): ICodeWindow { + private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, filesToOpen?: IFilesToOpen): ICodeWindow { this.logService.trace('windowsManager#doOpenFilesInExistingWindow'); window.focus(); // make sure window has focus const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; - if (fileInputs) { - params.filesToOpenOrCreate = fileInputs.filesToOpenOrCreate; - params.filesToDiff = fileInputs.filesToDiff; - params.filesToWait = fileInputs.filesToWait; + if (filesToOpen) { + params.filesToOpenOrCreate = filesToOpen.filesToOpenOrCreate; + params.filesToDiff = filesToOpen.filesToDiff; + params.filesToWait = filesToOpen.filesToWait; } if (configuration.userEnv) { @@ -773,7 +773,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return window; } - private doOpenEmpty(openConfig: IOpenConfiguration, forceNewWindow: boolean, remoteAuthority: string | undefined, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { + private doOpenEmpty(openConfig: IOpenConfiguration, forceNewWindow: boolean, remoteAuthority: string | undefined, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/97172 } @@ -785,12 +785,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, - fileInputs, + filesToOpen, windowToUse }); } - private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/49587 } @@ -801,7 +801,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic initialStartup: openConfig.initialStartup, workspace: folderOrWorkspace.workspace, folderUri: folderOrWorkspace.folderUri, - fileInputs, + filesToOpen, remoteAuthority: folderOrWorkspace.remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, @@ -837,7 +837,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Convert multiple folders into workspace (if opened via API or CLI) // This will ensure to open these folders in one window instead of multiple - // If we are in addMode, we should not do this because in that case all + // If we are in `addMode`, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); @@ -914,7 +914,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - // file uris const fileUris = cli['file-uri']; if (fileUris) { @@ -947,9 +946,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } private doGetWindowsFromLastSession(): IPathToOpen[] { - const restoreWindows = this.getRestoreWindowsSetting(); + const restoreWindowsSetting = this.getRestoreWindowsSetting(); - switch (restoreWindows) { + switch (restoreWindowsSetting) { // none: we always open an empty window case 'none': @@ -961,8 +960,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic case 'one': case 'all': case 'folders': + + // Collect previously opened windows const openedWindows: IWindowState[] = []; - if (restoreWindows !== 'one') { + if (restoreWindowsSetting !== 'one') { openedWindows.push(...this.windowsState.openedWindows); } if (this.windowsState.lastActiveWindow) { @@ -971,17 +972,25 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowsToOpen: IPathToOpen[] = []; for (const openedWindow of openedWindows) { - if (openedWindow.workspace) { // Workspaces + + // Workspaces + if (openedWindow.workspace) { const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority }); if (pathToOpen?.workspace) { windowsToOpen.push(pathToOpen); } - } else if (openedWindow.folderUri) { // Folders + } + + // Folders + else if (openedWindow.folderUri) { const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority }); if (pathToOpen?.folderUri) { windowsToOpen.push(pathToOpen); } - } else if (restoreWindows !== 'folders' && openedWindow.backupPath) { // Empty window, potentially editors open to be restored + } + + // Empty window, potentially editors open to be restored + else if (restoreWindowsSetting !== 'folders' && openedWindow.backupPath) { windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority }); } } @@ -1360,11 +1369,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration.folderUri = options.folderUri; configuration.remoteAuthority = options.remoteAuthority; - const fileInputs = options.fileInputs; - if (fileInputs) { - configuration.filesToOpenOrCreate = fileInputs.filesToOpenOrCreate; - configuration.filesToDiff = fileInputs.filesToDiff; - configuration.filesToWait = fileInputs.filesToWait; + const filesToOpen = options.filesToOpen; + if (filesToOpen) { + configuration.filesToOpenOrCreate = filesToOpen.filesToOpenOrCreate; + configuration.filesToDiff = filesToOpen.filesToDiff; + configuration.filesToWait = filesToOpen.filesToWait; } // if we know the backup folder upfront (for empty windows to restore), we can set it diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index eb7426c56be..c5534502c95 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -4325,6 +4325,12 @@ declare module 'vscode' { * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor. */ export interface FoldingRangeProvider { + + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChangeFoldingRanges?: Event; + /** * Returns a list of folding ranges or null and undefined if the provider * does not want to participate or was cancelled. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index e289a214eaa..f0596e4e745 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2134,15 +2134,4 @@ declare module 'vscode' { notebook: NotebookDocument | undefined; } //#endregion - - //#region https://github.com/microsoft/vscode/issues/108929 FoldingRangeProvider.onDidChangeFoldingRanges @aeschli - export interface FoldingRangeProvider2 extends FoldingRangeProvider { - - /** - * An optional event to signal that the folding ranges from this provider have changed. - */ - onDidChangeFoldingRanges?: Event; - - } - //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 64a3f19a3be..33824992142 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -165,16 +165,15 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerCodeLensSupport(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { const provider = { - provideCodeLenses: (model: ITextModel, token: CancellationToken): Promise => { - return this._proxy.$provideCodeLenses(handle, model.uri, token).then(listDto => { - if (!listDto) { - return undefined; - } - return { - lenses: listDto.lenses, - dispose: () => listDto.cacheId && this._proxy.$releaseCodeLenses(handle, listDto.cacheId) - }; - }); + provideCodeLenses: async (model: ITextModel, token: CancellationToken): Promise => { + const listDto = await this._proxy.$provideCodeLenses(handle, model.uri, token); + if (!listDto) { + return undefined; + } + return { + lenses: listDto.lenses, + dispose: () => listDto.cacheId && this._proxy.$releaseCodeLenses(handle, listDto.cacheId) + }; }, resolveCodeLens: (_model: ITextModel, codeLens: modes.CodeLens, token: CancellationToken): Promise => { return this._proxy.$resolveCodeLens(handle, codeLens, token); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b26a4ef0405..77c0b391594 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -32,7 +32,6 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; // --- adapter @@ -117,65 +116,57 @@ class CodeLensAdapter { private readonly _provider: vscode.CodeLensProvider ) { } - provideCodeLenses(resource: URI, token: CancellationToken): Promise { + async provideCodeLenses(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); - return asPromise(() => this._provider.provideCodeLenses(doc, token)).then(lenses => { - - if (!lenses || token.isCancellationRequested) { - return undefined; - } - - const cacheId = this._cache.add(lenses); - const disposables = new DisposableStore(); - this._disposables.set(cacheId, disposables); - - const result: extHostProtocol.ICodeLensListDto = { - cacheId, - lenses: [], - }; - - for (let i = 0; i < lenses.length; i++) { - result.lenses.push({ - cacheId: [cacheId, i], - range: typeConvert.Range.from(lenses[i].range), - command: this._commands.toInternal(lenses[i].command, disposables) - }); - } - - return result; - }); + const lenses = await this._provider.provideCodeLenses(doc, token); + if (!lenses || token.isCancellationRequested) { + return undefined; + } + const cacheId = this._cache.add(lenses); + const disposables = new DisposableStore(); + this._disposables.set(cacheId, disposables); + const result: extHostProtocol.ICodeLensListDto = { + cacheId, + lenses: [], + }; + for (let i = 0; i < lenses.length; i++) { + result.lenses.push({ + cacheId: [cacheId, i], + range: typeConvert.Range.from(lenses[i].range), + command: this._commands.toInternal(lenses[i].command, disposables) + }); + } + return result; } - resolveCodeLens(symbol: extHostProtocol.ICodeLensDto, token: CancellationToken): Promise { + async resolveCodeLens(symbol: extHostProtocol.ICodeLensDto, token: CancellationToken): Promise { const lens = symbol.cacheId && this._cache.get(...symbol.cacheId); if (!lens) { - return Promise.resolve(undefined); + return undefined; } - let resolve: Promise; + let resolvedLens: vscode.CodeLens | undefined | null; if (typeof this._provider.resolveCodeLens !== 'function' || lens.isResolved) { - resolve = Promise.resolve(lens); + resolvedLens = lens; } else { - resolve = asPromise(() => this._provider.resolveCodeLens!(lens, token)); + resolvedLens = await this._provider.resolveCodeLens(lens, token); + } + if (!resolvedLens) { + resolvedLens = lens; } - return resolve.then(newLens => { - if (token.isCancellationRequested) { - return undefined; - } - - const disposables = symbol.cacheId && this._disposables.get(symbol.cacheId[0]); - if (!disposables) { - // We've already been disposed of - return undefined; - } - - newLens = newLens || lens; - symbol.command = this._commands.toInternal(newLens.command || CodeLensAdapter._badCmd, disposables); - return symbol; - }); + if (token.isCancellationRequested) { + return undefined; + } + const disposables = symbol.cacheId && this._disposables.get(symbol.cacheId[0]); + if (!disposables) { + // disposed in the meantime + return undefined; + } + symbol.command = this._commands.toInternal(resolvedLens.command ?? CodeLensAdapter._badCmd, disposables); + return symbol; } releaseCodeLenses(cachedId: number): void { @@ -1814,7 +1805,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo, token), undefined); } - registerFoldingRangeProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider2): vscode.Disposable { + registerFoldingRangeProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider): vscode.Disposable { const handle = this._nextHandle(); const eventHandle = typeof provider.onDidChangeFoldingRanges === 'function' ? this._nextHandle() : undefined; @@ -1823,8 +1814,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF let result = this._createDisposable(handle); if (eventHandle !== undefined) { - checkProposedApiEnabled(extension); - const subscription = provider.onDidChangeFoldingRanges!(_ => this._proxy.$emitFoldingRangeEvent(eventHandle)); + const subscription = provider.onDidChangeFoldingRanges!(() => this._proxy.$emitFoldingRangeEvent(eventHandle)); result = Disposable.from(result, subscription); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index c0eb2a21f6a..ee870895848 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1183,6 +1183,7 @@ export namespace FoldingRangeKind { export interface TextEditorOpenOptions extends vscode.TextDocumentShowOptions { background?: boolean; + override?: boolean; } export namespace TextEditorOpenOptions { @@ -1194,6 +1195,7 @@ export namespace TextEditorOpenOptions { inactive: options.background, preserveFocus: options.preserveFocus, selection: typeof options.selection === 'object' ? Range.from(options.selection) : undefined, + override: typeof options.override === 'boolean' ? false : undefined }; } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 609ed7d5a8c..802966c808e 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -238,27 +238,27 @@ export abstract class AbstractSideBySideEditorInputFactory implements IEditorInp const secondaryInput = secondaryInputFactory.deserialize(instantiationService, deserialized.secondarySerialized); if (primaryInput && secondaryInput) { - return this.createEditorInput(deserialized.name, deserialized.description, secondaryInput, primaryInput); + return this.createEditorInput(instantiationService, deserialized.name, deserialized.description, secondaryInput, primaryInput); } } return undefined; } - protected abstract createEditorInput(name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; + protected abstract createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput; } class SideBySideEditorInputFactory extends AbstractSideBySideEditorInputFactory { - protected createEditorInput(name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { return new SideBySideEditorInput(name, description, secondaryInput, primaryInput); } } class DiffEditorInputFactory extends AbstractSideBySideEditorInputFactory { - protected createEditorInput(name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { - return new DiffEditorInput(name, description, secondaryInput, primaryInput); + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + return instantiationService.createInstance(DiffEditorInput, name, description, secondaryInput, primaryInput, undefined); } } diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 8c5f62153b8..c05ce6d79b2 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -38,7 +38,8 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { openSideBySideDirection: 'right', closeEmptyGroups: true, labelFormat: 'default', - splitSizing: 'distribute' + splitSizing: 'distribute', + splitOnDragAndDrop: true }; export function impactsEditorPartOptions(event: IConfigurationChangeEvent): boolean { diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index de74d85fd76..8eb81049a38 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -47,6 +47,9 @@ export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor'; export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange'; +export const DIFF_FOCUS_PRIMARY_SIDE = 'workbench.action.compareEditor.focusPrimarySide'; +export const DIFF_FOCUS_SECONDARY_SIDE = 'workbench.action.compareEditor.focusSecondarySide'; +export const DIFF_FOCUS_OTHER_SIDE = 'workbench.action.compareEditor.focusOtherSide'; export const TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE = 'toggle.diff.ignoreTrimWhitespace'; export const SPLIT_EDITOR_UP = 'workbench.action.splitEditorUp'; @@ -268,18 +271,56 @@ function registerDiffEditorCommands(): void { handler: accessor => navigateInDiffEditor(accessor, false) }); - function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { + function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { const editorService = accessor.get(IEditorService); - const candidates = [editorService.activeEditorPane, ...editorService.visibleEditorPanes].filter(editor => editor instanceof TextDiffEditor); - if (candidates.length > 0) { - const navigator = (candidates[0]).getDiffNavigator(); + for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { + if (editor instanceof TextDiffEditor) { + return editor; + } + } + + return undefined; + } + + function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + + if (activeTextDiffEditor) { + const navigator = activeTextDiffEditor.getDiffNavigator(); if (navigator) { next ? navigator.next() : navigator.previous(); } } } + enum FocusTextDiffEditorMode { + Original, + Modified, + Toggle + } + + function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + + if (activeTextDiffEditor) { + switch (mode) { + case FocusTextDiffEditorMode.Original: + activeTextDiffEditor.getControl()?.getOriginalEditor().focus(); + break; + case FocusTextDiffEditorMode.Modified: + activeTextDiffEditor.getControl()?.getModifiedEditor().focus(); + break; + case FocusTextDiffEditorMode.Toggle: + if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { + return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); + } else { + return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); + } + } + } + } + function toggleDiffSideBySide(accessor: ServicesAccessor): void { const configurationService = accessor.get(IConfigurationService); @@ -302,6 +343,30 @@ function registerDiffEditorCommands(): void { handler: accessor => toggleDiffSideBySide(accessor) }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_PRIMARY_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_SECONDARY_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_OTHER_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 7c63cf6e530..50f571563cd 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -12,7 +12,7 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, MergeGroupMode, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, IEditorGroupsService, MergeGroupMode, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -55,7 +55,8 @@ class DropOverlay extends Themable { @IInstantiationService private instantiationService: IInstantiationService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(themeService); @@ -144,8 +145,14 @@ class DropOverlay extends Themable { } } - // Position overlay - this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup); + // Position overlay and conditionally enable or disable + // editor group splitting support based on setting and + // keymodifiers used. + let splitOnDragAndDrop = !!this.editorGroupService.partOptions.splitOnDragAndDrop; + if (this.isToggleSplitOperation(e)) { + splitOnDragAndDrop = !splitOnDragAndDrop; + } + this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup, splitOnDragAndDrop); // Make sure to stop any running cleanup scheduler to remove the overlay if (this.cleanupOverlayScheduler.isScheduled()) { @@ -363,24 +370,33 @@ class DropOverlay extends Themable { return (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); } - private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean): void { + private isToggleSplitOperation(e: DragEvent): boolean { + return (e.altKey && !isMacintosh) || (e.shiftKey && isMacintosh); + } + + private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean, enableSplitting: boolean): void { const preferSplitVertically = this.accessor.partOptions.openSideBySideDirection === 'right'; const editorControlWidth = this.groupView.element.clientWidth; const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight(); let edgeWidthThresholdFactor: number; - if (isDraggingGroup) { - edgeWidthThresholdFactor = preferSplitVertically ? 0.3 : 0.1; // give larger threshold when dragging group depending on preferred split direction - } else { - edgeWidthThresholdFactor = 0.1; // 10% threshold to split if dragging editors - } - let edgeHeightThresholdFactor: number; - if (isDraggingGroup) { - edgeHeightThresholdFactor = preferSplitVertically ? 0.1 : 0.3; // give larger threshold when dragging group depending on preferred split direction + if (enableSplitting) { + if (isDraggingGroup) { + edgeWidthThresholdFactor = preferSplitVertically ? 0.3 : 0.1; // give larger threshold when dragging group depending on preferred split direction + } else { + edgeWidthThresholdFactor = 0.1; // 10% threshold to split if dragging editors + } + + if (isDraggingGroup) { + edgeHeightThresholdFactor = preferSplitVertically ? 0.1 : 0.3; // give larger threshold when dragging group depending on preferred split direction + } else { + edgeHeightThresholdFactor = 0.1; // 10% threshold to split if dragging editors + } } else { - edgeHeightThresholdFactor = 0.1; // 10% threshold to split if dragging editors + edgeWidthThresholdFactor = 0; + edgeHeightThresholdFactor = 0; } const edgeWidthThreshold = editorControlWidth * edgeWidthThresholdFactor; diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 115d6ee98d4..be8c620ce46 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -175,7 +175,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan const originalInput = input.originalInput; const modifiedInput = input.modifiedInput; - const binaryDiffInput = new DiffEditorInput(input.getName(), input.getDescription(), originalInput, modifiedInput, true); + const binaryDiffInput = this.instantiationService.createInstance(DiffEditorInput, input.getName(), input.getDescription(), originalInput, modifiedInput, true); // Forward binary flag to input if supported const fileEditorInputFactory = Registry.as(EditorInputExtensions.EditorInputFactories).getFileEditorInputFactory(); diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index d7e75f38c6e..46de06abcb9 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -122,7 +122,7 @@ export abstract class MenubarControl extends Disposable { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); // Listen to update service - this.updateService.onStateChange(() => this.updateMenubar()); + this.updateService.onStateChange(() => this.onUpdateStateChange()); // Listen for changes in recently opened menu this._register(this.workspacesService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); @@ -148,6 +148,14 @@ export abstract class MenubarControl extends Disposable { return label; } + protected onUpdateStateChange(): void { + this.updateMenubar(); + } + + protected onUpdateKeybindings(): void { + this.updateMenubar(); + } + protected getOpenRecentActions(): (Separator | IAction & { uri: URI })[] { if (!this.recentlyOpened) { return []; @@ -193,7 +201,7 @@ export abstract class MenubarControl extends Disposable { } } - private onRecentlyOpenedChange(): void { + protected onRecentlyOpenedChange(): void { this.workspacesService.getRecentlyOpened().then(recentlyOpened => { this.recentlyOpened = recentlyOpened; this.updateMenubar(); @@ -266,6 +274,7 @@ export class CustomMenubarControl extends MenubarControl { private container: HTMLElement | undefined; private alwaysOnMnemonics: boolean = false; private focusInsideMenubar: boolean = false; + private visible: boolean = true; private readonly _onVisibilityChange: Emitter; private readonly _onFocusStateChange: Emitter; @@ -530,6 +539,12 @@ export class CustomMenubarControl extends MenubarControl { return currentSidebarLocation === 'right' ? Direction.Left : Direction.Right; } + private onDidVisibilityChange(visible: boolean): void { + this.visible = visible; + this.onRecentlyOpenedChange(); + this._onVisibilityChange.fire(visible); + } + private setupCustomMenubar(firstTime: boolean): void { // If there is no container, we cannot setup the menubar if (!this.container) { @@ -554,7 +569,7 @@ export class CustomMenubarControl extends MenubarControl { } })); - this._register(this.menubar.onVisibilityChange(e => this._onVisibilityChange.fire(e))); + this._register(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e))); // Before we focus the menubar, stop updates to it so that focus-related context keys will work this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, () => { @@ -668,6 +683,10 @@ export class CustomMenubarControl extends MenubarControl { } protected onDidChangeWindowFocus(hasFocus: boolean): void { + if (!this.visible) { + return; + } + super.onDidChangeWindowFocus(hasFocus); if (this.container) { @@ -682,6 +701,30 @@ export class CustomMenubarControl extends MenubarControl { } } + protected onUpdateStateChange(): void { + if (!this.visible) { + return; + } + + super.onUpdateStateChange(); + } + + protected onRecentlyOpenedChange(): void { + if (!this.visible) { + return; + } + + super.onRecentlyOpenedChange(); + } + + protected onUpdateKeybindings(): void { + if (!this.visible) { + return; + } + + super.onUpdateKeybindings(); + } + protected registerListeners(): void { super.registerListeners(); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index e6c8f1dbfd8..fdc99470bb5 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -108,6 +108,11 @@ import { isStandalone } from 'vs/base/browser/browser'; ], 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'splitSizing' }, "Controls the sizing of editor groups when splitting them.") }, + 'workbench.editor.splitOnDragAndDrop': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('splitOnDragAndDrop', "Controls if editor groups can be split from drag and drop operations by dropping an editor or file on the edges of the editor area.") + }, 'workbench.editor.focusRecentEditorAfterClose': { 'type': 'boolean', 'description': nls.localize('focusRecentEditorAfterClose', "Controls whether tabs are closed in most recently used order or from left to right."), diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 30a375204cd..2dfe74eed7d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -734,7 +734,7 @@ export class SideBySideEditorInput extends EditorInput { constructor( protected readonly name: string | undefined, - private readonly description: string | undefined, + protected readonly description: string | undefined, private readonly _secondary: EditorInput, private readonly _primary: EditorInput ) { @@ -1266,6 +1266,7 @@ interface IEditorPartConfiguration { labelFormat?: 'default' | 'short' | 'medium' | 'long'; restoreViewState?: boolean; splitSizing?: 'split' | 'distribute'; + splitOnDragAndDrop?: boolean; limit?: { enabled?: boolean; value?: number; diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 4274f2eb876..e4ac4d07e3a 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -3,11 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; +import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, BINARY_DIFF_EDITOR_ID, Verbosity } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { localize } from 'vs/nls'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { dirname } from 'vs/base/common/resources'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -21,10 +26,12 @@ export class DiffEditorInput extends SideBySideEditorInput { constructor( protected name: string | undefined, - description: string | undefined, + protected description: string | undefined, public readonly originalInput: EditorInput, public readonly modifiedInput: EditorInput, - private readonly forceOpenAsBinary?: boolean + private readonly forceOpenAsBinary: boolean | undefined, + @ILabelService private readonly labelService: ILabelService, + @IFileService private readonly fileService: IFileService ) { super(name, description, originalInput, modifiedInput); } @@ -35,12 +42,53 @@ export class DiffEditorInput extends SideBySideEditorInput { getName(): string { if (!this.name) { + + // Craft a name from original and modified input that includes the + // relative path in case both sides have different parents and we + // compare file resources. + const fileResources = this.asFileResources(); + if (fileResources && dirname(fileResources.original).path !== dirname(fileResources.modified).path + ) { + return `${this.labelService.getUriLabel(fileResources.original, { relative: true })} ↔ ${this.labelService.getUriLabel(fileResources.modified, { relative: true })}`; + } + return localize('sideBySideLabels', "{0} ↔ {1}", this.originalInput.getName(), this.modifiedInput.getName()); } return this.name; } + getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { + if (typeof this.description !== 'string') { + + // Pass the description of the modified side in case both original + // and modified input have the same parent and we compare file resources. + const fileResources = this.asFileResources(); + if (fileResources && dirname(fileResources.original).path === dirname(fileResources.modified).path + ) { + return this.modifiedInput.getDescription(verbosity); + } + } + + return this.description; + } + + private asFileResources(): { original: URI, modified: URI } | undefined { + if ( + this.originalInput instanceof AbstractTextResourceEditorInput && + this.modifiedInput instanceof AbstractTextResourceEditorInput && + this.fileService.canHandleResource(this.originalInput.preferredResource) && + this.fileService.canHandleResource(this.modifiedInput.preferredResource) + ) { + return { + original: this.originalInput.preferredResource, + modified: this.modifiedInput.preferredResource + }; + } + + return undefined; + } + async resolve(): Promise { // Create Model - we never reuse our cached model if refresh is true because we cannot diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 1f196dd3f0f..e9e3d55b8c5 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -26,7 +26,7 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; -import { basename } from 'vs/base/common/resources'; +import { basename, dirname } from 'vs/base/common/resources'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -356,6 +356,7 @@ export class BulkEditPane extends ViewPane { leftResource, rightResource: previewUri, label, + description: this._labelService.getUriLabel(dirname(leftResource), { relative: true }), options }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } diff --git a/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/src/vs/workbench/contrib/cli/node/cli.contribution.ts index 2000693fdf3..30972a41158 100644 --- a/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ b/src/vs/workbench/contrib/cli/node/cli.contribution.ts @@ -8,12 +8,8 @@ import * as path from 'vs/base/common/path'; import * as cp from 'child_process'; import * as pfs from 'vs/base/node/pfs'; import * as extpath from 'vs/base/node/extpath'; -import * as platform from 'vs/base/common/platform'; import { promisify } from 'util'; -import { Action } from 'vs/base/common/actions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import product from 'vs/platform/product/common/product'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -21,6 +17,9 @@ import Severity from 'vs/base/common/severity'; import { ILogService } from 'vs/platform/log/common/log'; import { FileAccess } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; function ignore(code: string, value: T): (err: any) => Promise { return err => err.code === code ? Promise.resolve(value) : Promise.reject(err); @@ -39,45 +38,65 @@ function isAvailable(): Promise { return Promise.resolve(pfs.exists(getSource())); } -class InstallAction extends Action { +const category = nls.localize('shellCommand', "Shell Command"); - static readonly ID = 'workbench.action.installCommandLine'; - static readonly LABEL = nls.localize('install', "Install '{0}' command in PATH", product.applicationName); +class InstallAction extends Action2 { - constructor( - id: string, - label: string, - @INotificationService private readonly notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, - @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.installCommandLine', + title: { + value: nls.localize('install', "Install '{0}' command in PATH", product.applicationName), + original: `Shell Command: Install \'${product.applicationName}\' command in PATH` + }, + category, + f1: true, + precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) + }); } - private get target(): string { - return `/usr/local/bin/${this.productService.applicationName}`; - } + run(accessor: ServicesAccessor): Promise { + const productService = accessor.get(IProductService); + const notificationService = accessor.get(INotificationService); + const logService = accessor.get(ILogService); + const dialogService = accessor.get(IDialogService); + const target = `/usr/local/bin/${productService.applicationName}`; - run(): Promise { return isAvailable().then(isAvailable => { if (!isAvailable) { const message = nls.localize('not available', "This command is not available"); - this.notificationService.info(message); + notificationService.info(message); return undefined; } - return this.isInstalled() + return this.isInstalled(target) .then(isInstalled => { if (!isAvailable || isInstalled) { return Promise.resolve(null); } else { - return pfs.unlink(this.target) + return pfs.unlink(target) .then(undefined, ignore('ENOENT', null)) - .then(() => pfs.symlink(getSource(), this.target)) + .then(() => pfs.symlink(getSource(), target)) .then(undefined, err => { if (err.code === 'EACCES' || err.code === 'ENOENT') { - return this.createBinFolderAndSymlinkAsAdmin(); + return new Promise((resolve, reject) => { + const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; + + dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(result => { + switch (result.choice) { + case 0 /* OK */: + const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + target + '\'\\" with administrator privileges"'; + + promisify(cp.exec)(command, {}) + .then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) + .then(() => resolve(), reject); + break; + case 1 /* Cancel */: + reject(new Error(nls.localize('aborted', "Aborted"))); + break; + } + }); + }); } return Promise.reject(err); @@ -85,113 +104,84 @@ class InstallAction extends Action { } }) .then(() => { - this.logService.trace('cli#install', this.target); - this.notificationService.info(nls.localize('successIn', "Shell command '{0}' successfully installed in PATH.", this.productService.applicationName)); + logService.trace('cli#install', target); + notificationService.info(nls.localize('successIn', "Shell command '{0}' successfully installed in PATH.", productService.applicationName)); }); }); } - private isInstalled(): Promise { - return pfs.lstat(this.target) + private isInstalled(target: string): Promise { + return pfs.lstat(target) .then(stat => stat.isSymbolicLink()) - .then(() => extpath.realpath(this.target)) + .then(() => extpath.realpath(target)) .then(link => link === getSource()) .then(undefined, ignore('ENOENT', false)); } - - private createBinFolderAndSymlinkAsAdmin(): Promise { - return new Promise((resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - this.dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(result => { - switch (result.choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + this.target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - }); - } } -class UninstallAction extends Action { +class UninstallAction extends Action2 { - static readonly ID = 'workbench.action.uninstallCommandLine'; - static readonly LABEL = nls.localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName); - - constructor( - id: string, - label: string, - @INotificationService private readonly notificationService: INotificationService, - @ILogService private readonly logService: ILogService, - @IDialogService private readonly dialogService: IDialogService, - @IProductService private readonly productService: IProductService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.uninstallCommandLine', + title: { + value: nls.localize('uninstall', "Uninstall '{0}' command from PATH", product.applicationName), + original: `Shell Command: Uninstall \'${product.applicationName}\' command from PATH` + }, + category, + f1: true, + precondition: ContextKeyExpr.and(IsMacNativeContext, ContextKeyExpr.equals('remoteName', '')) + }); } - private get target(): string { - return `/usr/local/bin/${this.productService.applicationName}`; - } + run(accessor: ServicesAccessor): Promise { + const productService = accessor.get(IProductService); + const notificationService = accessor.get(INotificationService); + const logService = accessor.get(ILogService); + const dialogService = accessor.get(IDialogService); + const target = `/usr/local/bin/${productService.applicationName}`; - run(): Promise { return isAvailable().then(isAvailable => { if (!isAvailable) { const message = nls.localize('not available', "This command is not available"); - this.notificationService.info(message); + notificationService.info(message); return undefined; } const uninstall = () => { - return pfs.unlink(this.target) + return pfs.unlink(target) .then(undefined, ignore('ENOENT', null)); }; return uninstall().then(undefined, err => { if (err.code === 'EACCES') { - return this.deleteSymlinkAsAdmin(); + return new Promise(async (resolve, reject) => { + const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; + + const { choice } = await dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }); + switch (choice) { + case 0 /* OK */: + const command = 'osascript -e "do shell script \\"rm \'' + target + '\'\\" with administrator privileges"'; + + promisify(cp.exec)(command, {}) + .then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", target)))) + .then(() => resolve(), reject); + break; + case 1 /* Cancel */: + reject(new Error(nls.localize('aborted', "Aborted"))); + break; + } + }); } return Promise.reject(err); }).then(() => { - this.logService.trace('cli#uninstall', this.target); - this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", this.productService.applicationName)); + logService.trace('cli#uninstall', target); + notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", productService.applicationName)); }); }); } - - private deleteSymlinkAsAdmin(): Promise { - return new Promise(async (resolve, reject) => { - const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; - - const { choice } = await this.dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }); - switch (choice) { - case 0 /* OK */: - const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"'; - - promisify(cp.exec)(command, {}) - .then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", this.target)))) - .then(() => resolve(), reject); - break; - case 1 /* Cancel */: - reject(new Error(nls.localize('aborted', "Aborted"))); - break; - } - }); - } } -if (platform.isMacintosh) { - const category = nls.localize('shellCommand', "Shell Command"); - - const workbenchActionsRegistry = Registry.as(ActionExtensions.WorkbenchActions); - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(InstallAction), `Shell Command: Install \'${product.applicationName}\' command in PATH`, category); - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(UninstallAction), `Shell Command: Uninstall \'${product.applicationName}\' command from PATH`, category); -} +registerAction2(InstallAction); +registerAction2(UninstallAction); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 3bcaaacdba0..c67d70f1a95 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -447,6 +447,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo constructor( @IEditorService private readonly editorService: IEditorService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -651,7 +652,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo if (modifiedOverride || originalOverride) { return { override: (async () => { - const input = new DiffEditorInput(editor.getName(), editor.getDescription(), originalOverride || editor.originalInput, modifiedOverride || editor.modifiedInput, true); + const input = this.instantiationService.createInstance(DiffEditorInput, editor.getName(), editor.getDescription(), originalOverride || editor.originalInput, modifiedOverride || editor.modifiedInput, true); return this.editorService.openEditor(input, { ...options, override: false }, group); })(), }; diff --git a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts index 960c3335d1b..ac834f0fabb 100644 --- a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts +++ b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts @@ -8,12 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; import { CustomEditorDescriptor, CustomEditorInfo, CustomEditorPriority } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { customEditorsExtensionPoint, ICustomEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/common/extensionPoint'; -import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { DEFAULT_EDITOR_ID } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in"); @@ -40,7 +40,7 @@ export class ContributedCustomEditors extends Disposable { this._memento = new Memento(ContributedCustomEditors.CUSTOM_EDITORS_STORAGE_ID, storageService); - const mementoObject = this._memento.legacygetMemento(StorageScope.GLOBAL); + const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); for (const info of (mementoObject[ContributedCustomEditors.CUSTOM_EDITORS_ENTRY_ID] || []) as CustomEditorDescriptor[]) { this.add(new CustomEditorInfo(info)); } diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index ee883ca582e..ba3b3147a83 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -5,7 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -222,7 +222,7 @@ export class ExperimentService extends Disposable implements IExperimentService const storageKey = 'experiments.' + experimentId; const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); experimentState.state = ExperimentState.Complete; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + this.storageService.store2(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL, StorageTarget.MACHINE); } protected async getExperiments(): Promise { diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts index b0e18778a2e..df2c5a34a0b 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -68,6 +68,11 @@ suite('Experimental Prompts', () => { if (a === 'experiments.experiment1') { storageData = JSON.parse(b + ''); } + }, + store2: (a, b, c, d) => { + if (a === 'experiments.experiment1') { + storageData = JSON.parse(b + ''); + } } }); instantiationService.stub(INotificationService, new TestNotificationService()); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index f9bef7343d0..285311deafc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -27,7 +27,12 @@ import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { + UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, + RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, + ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, + InstallAnotherVersionAction, ExtensionEditorManageExtensionAction +} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; @@ -407,7 +412,6 @@ export class ExtensionEditor extends EditorPane { const combinedInstallAction = this.instantiationService.createInstance(InstallDropdownAction); const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction); const actions = [ - this.instantiationService.createInstance(ToggleSyncExtensionAction), reloadAction, this.instantiationService.createInstance(StatusLabelAction), this.instantiationService.createInstance(UpdateAction), @@ -421,8 +425,13 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(LocalInstallAction), combinedInstallAction, this.instantiationService.createInstance(InstallingLabelAction), - this.instantiationService.createInstance(UninstallAction), + this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ + this.instantiationService.createInstance(UninstallAction), + this.instantiationService.createInstance(InstallAnotherVersionAction), + ]), + this.instantiationService.createInstance(ToggleSyncExtensionAction), systemDisabledWarningAction, + this.instantiationService.createInstance(ExtensionEditorManageExtensionAction), this.instantiationService.createInstance(ExtensionToolTipAction, systemDisabledWarningAction, reloadAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, true), ]; @@ -435,7 +444,7 @@ export class ExtensionEditor extends EditorPane { this.transientDisposables.add(disposable); } - this.setSubText(extension, reloadAction, template); + this.setSubText(extension, template); template.content.innerText = ''; // Clear content before setting navbar actions. template.navbar.clear(); @@ -466,56 +475,24 @@ export class ExtensionEditor extends EditorPane { this.editorLoadComplete = true; } - private setSubText(extension: IExtension, reloadAction: ReloadAction, template: IExtensionEditorTemplate): void { + private setSubText(extension: IExtension, template: IExtensionEditorTemplate): void { hide(template.subtextContainer); - const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction, extension); - const undoIgnoreAction = this.instantiationService.createInstance(UndoIgnoreExtensionRecommendationAction, extension); - ignoreAction.enabled = false; - undoIgnoreAction.enabled = false; - - template.ignoreActionbar.clear(); - template.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); - this.transientDisposables.add(ignoreAction); - this.transientDisposables.add(undoIgnoreAction); - const updateRecommendationFn = () => { const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason(); if (extRecommendations[extension.identifier.id.toLowerCase()]) { - ignoreAction.enabled = true; - undoIgnoreAction.enabled = false; template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; show(template.subtextContainer); } else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(extension.identifier.id.toLowerCase()) !== -1) { - ignoreAction.enabled = false; - undoIgnoreAction.enabled = true; template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); show(template.subtextContainer); } else { - ignoreAction.enabled = false; - undoIgnoreAction.enabled = false; template.subtext.textContent = ''; hide(template.subtextContainer); } }; updateRecommendationFn(); this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationFn())); - - this.transientDisposables.add(reloadAction.onDidChange(e => { - if (e.tooltip) { - template.subtext.textContent = reloadAction.tooltip; - show(template.subtextContainer); - ignoreAction.enabled = false; - undoIgnoreAction.enabled = false; - } - if (e.enabled === true) { - show(template.subtextContainer); - } - if (e.enabled === false) { - hide(template.subtextContainer); - } - this.layout(); - })); } clearInput(): void { @@ -903,6 +880,7 @@ export class ExtensionEditor extends EditorPane { this.renderLocalizations(content, manifest, layout), this.renderCustomEditors(content, manifest, layout), this.renderAuthentication(content, manifest, layout), + this.renderActivationEvents(content, manifest, layout), ]; scrollableContent.scanDomNode(); @@ -1417,6 +1395,21 @@ export class ExtensionEditor extends EditorPane { return true; } + private renderActivationEvents(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { + const activationEvents = manifest.activationEvents || []; + if (!activationEvents.length) { + return false; + } + + const details = $('details', { open: true, ontoggle: onDetailsToggle }, + $('summary', { tabindex: '0' }, localize('activation events', "Activation Events ({0})", activationEvents.length)), + $('ul', undefined, ...activationEvents.map(activationEvent => $('li', undefined, activationEvent))) + ); + + append(container, details); + return true; + } + private resolveKeybinding(rawKeyBinding: IKeyBinding): ResolvedKeybinding | null { let key: string | undefined; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 7df6cf1b99e..fa782cae308 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -100,7 +100,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte }) ]); - this._register(this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations(() => this._onDidChangeRecommendations.fire())); + this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); this._register(this.extensionRecommendationsManagementService.onDidChangeGlobalIgnoredRecommendation(({ extensionId, isRecommended }) => { if (!isRecommended) { const reason = this.getAllRecommendationsWithReason()[extensionId]; @@ -111,7 +111,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte })); await this.promptWorkspaceRecommendations(); - this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations())); } private isEnabled(): boolean { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index f4b33985ab1..450d7bf5ec6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -10,7 +10,7 @@ import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/acti import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -18,7 +18,7 @@ import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction + EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -26,7 +26,7 @@ import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContrib 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'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -62,6 +62,8 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IAction } from 'vs/base/common/actions'; +import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; +import { Schemas } from 'vs/base/common/network'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -234,8 +236,8 @@ CommandsRegistry.registerCommand({ .then(async (extensions) => { for (const extension of extensions) { const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); const actions = requireReload ? [{ label: localize('InstallVSIXAction.reloadNow', "Reload Now"), run: () => hostService.reload() @@ -916,7 +918,7 @@ class ExtensionsContributions implements IWorkbenchContribution { menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: CONTEXT_SYNC_ENABLEMENT + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) }, }); } @@ -929,6 +931,220 @@ class ExtensionsContributions implements IWorkbenchContribution { } } }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.ignoreRecommendation', + title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isExtensionRecommended'), + order: 1 + }, + }); + } + + async run(accessor: ServicesAccessor, id: string): Promise { + accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.undoIgnoredRecommendation', + title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isUserIgnoredRecommendation'), + order: 1 + }, + }); + } + + async run(accessor: ServicesAccessor, id: string): Promise { + accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + order: 2 + }, + }); + } + + run(accessor: ServicesAccessor, id: string): Promise { + return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), + order: 2 + }, + }); + } + + run(accessor: ServicesAccessor, id: string): Promise { + return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.addToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); + if (!(editorService.activeEditor instanceof ExtensionsInput)) { + return; + } + const extensionId = editorService.activeEditor.extension.identifier.id.toLowerCase(); + const recommendations = await workpsaceExtensionsConfigService.getRecommendations(); + if (recommendations.includes(extensionId)) { + return; + } + await workpsaceExtensionsConfigService.toggleRecommendation(extensionId); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + }); + } + + async run(accessor: ServicesAccessor): Promise { + return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceRecommendations'); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); + if (!(editorService.activeEditor instanceof ExtensionsInput)) { + return; + } + const extensionId = editorService.activeEditor.extension.identifier.id.toLowerCase(); + const unwatedRecommendations = await workpsaceExtensionsConfigService.getUnwantedRecommendations(); + if (unwatedRecommendations.includes(extensionId)) { + return; + } + await workpsaceExtensionsConfigService.toggleUnwantedRecommendation(extensionId); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + }); + } + + run(accessor: ServicesAccessor): Promise { + return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations'); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: ConfigureWorkspaceRecommendedExtensionsAction.ID, + title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.isEqualTo('workspace'), + }, + }); + } + + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); + } + }); + + registerAction2(class extends Action2 { + + constructor() { + super({ + id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, + title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.notEqualsTo('empty'), + }, + }); + } + + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); + } + }); } } @@ -936,7 +1152,6 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(ConfigureRecommendedExtensionsCommandsContributor, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 91765c8c58e..4663cc13565 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -11,12 +11,12 @@ import * as DOM from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { dispose, Disposable } from 'vs/base/common/lifecycle'; +import { dispose } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionIgnoredRecommendationsService, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -36,16 +36,14 @@ import { Color } from 'vs/base/common/color'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -61,7 +59,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { EXTENSIONS_CONFIG } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; +import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; @@ -169,7 +167,7 @@ export abstract class ExtensionAction extends Action implements IExtensionContai abstract update(): void; } -export abstract class ActionWithDropDownAction extends ExtensionAction { +export class ActionWithDropDownAction extends ExtensionAction { private action: IAction | undefined; @@ -579,7 +577,7 @@ export class LocalInstallAction extends InstallInOtherServerAction { export class UninstallAction extends ExtensionAction { - private static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); + static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); private static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; @@ -834,29 +832,38 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem { } } -export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, instantiationService: IInstantiationService, extension: IExtension | undefined | null): IAction[][] { - const scopedContextKeyService = contextKeyService.createScoped(); - if (extension) { - scopedContextKeyService.createKey('extension', extension.identifier.id); - scopedContextKeyService.createKey('isBuiltinExtension', extension.isBuiltin); - scopedContextKeyService.createKey('extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration); - if (extension.state === ExtensionState.Installed) { - scopedContextKeyService.createKey('extensionStatus', 'installed'); +export function getContextMenuActions(extension: IExtension | undefined | null, inExtensionEditor: boolean, instantiationService: IInstantiationService): IAction[][] { + return instantiationService.invokeFunction(accessor => { + const scopedContextKeyService = accessor.get(IContextKeyService).createScoped(); + const menuService = accessor.get(IMenuService); + const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService); + const extensionIgnoredRecommendationsService = accessor.get(IExtensionIgnoredRecommendationsService); + if (extension) { + scopedContextKeyService.createKey('extension', extension.identifier.id); + scopedContextKeyService.createKey('isBuiltinExtension', extension.isBuiltin); + scopedContextKeyService.createKey('extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration); + scopedContextKeyService.createKey('isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]); + scopedContextKeyService.createKey('isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace); + scopedContextKeyService.createKey('isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())); + scopedContextKeyService.createKey('inExtensionEditor', inExtensionEditor); + if (extension.state === ExtensionState.Installed) { + scopedContextKeyService.createKey('extensionStatus', 'installed'); + } } - } - const groups: IAction[][] = []; - const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService); - menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => { - if (action instanceof SubmenuAction) { - return action; - } - return instantiationService.createInstance(MenuItemExtensionAction, action); - }))); - menu.dispose(); - scopedContextKeyService.dispose(); + const groups: IAction[][] = []; + const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService); + menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => { + if (action instanceof SubmenuAction) { + return action; + } + return instantiationService.createInstance(MenuItemExtensionAction, action); + }))); + menu.dispose(); + scopedContextKeyService.dispose(); - return groups; + return groups; + }); } export class ManageExtensionAction extends ExtensionDropDownAction { @@ -870,8 +877,6 @@ export class ManageExtensionAction extends ExtensionDropDownAction { @IInstantiationService instantiationService: IInstantiationService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(ManageExtensionAction.ID, '', '', true, true, instantiationService); @@ -908,10 +913,12 @@ export class ManageExtensionAction extends ExtensionDropDownAction { this.instantiationService.createInstance(DisableGloballyAction, runningExtensions), this.instantiationService.createInstance(DisableForWorkspaceAction, runningExtensions) ]); - groups.push([this.instantiationService.createInstance(UninstallAction)]); - groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]); + groups.push([ + this.instantiationService.createInstance(UninstallAction), + this.instantiationService.createInstance(InstallAnotherVersionAction) + ]); - getContextMenuActions(this.menuService, this.contextKeyService, this.instantiationService, this.extension).forEach(actions => groups.push(actions)); + getContextMenuActions(this.extension, false, this.instantiationService).forEach(actions => groups.push(actions)); groups.forEach(group => group.forEach(extensionAction => { if (extensionAction instanceof ExtensionAction) { @@ -939,6 +946,25 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } } +export class ExtensionEditorManageExtensionAction extends ExtensionDropDownAction { + + constructor( + @IInstantiationService instantiationService: IInstantiationService + ) { + super('extensionEditor.manageExtension', '', `${ExtensionAction.ICON_ACTION_CLASS} manage codicon-gear`, true, true, instantiationService); + this.tooltip = localize('manage', "Manage"); + } + + update(): void { } + + run(): Promise { + const actionGroups: IAction[][] = []; + getContextMenuActions(this.extension, true, this.instantiationService).forEach(actions => actionGroups.push(actions)); + return super.run({ actionGroups, disposeActionsOnHide: true }); + } + +} + export class MenuItemExtensionAction extends ExtensionAction { constructor( @@ -975,12 +1001,12 @@ export class InstallAnotherVersionAction extends ExtensionAction { @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { - super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL); + super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.update(); } update(): void { - this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery; + this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery && this.extension.state === ExtensionState.Installed; } run(): Promise { @@ -2115,13 +2141,15 @@ export class ChangeSortAction extends Action { this.query = Query.parse(''); this.enabled = false; + this.checked = false; this._register(onSearchChange(this.onSearchChange, this)); } private onSearchChange(value: string): void { const query = Query.parse(value); this.query = new Query(query.value, this.sortBy || query.sortBy, query.groupBy); - this.enabled = !!value && this.query.isValid() && !this.query.equals(query); + this.enabled = !!value && this.query.isValid(); + this.checked = this.enabled && this.query.equals(query); } run(): Promise { @@ -2134,124 +2162,6 @@ export class ChangeSortAction extends Action { } } -export class ConfigureRecommendedExtensionsCommandsContributor extends Disposable implements IWorkbenchContribution { - - private workspaceContextKey = new RawContextKey('workspaceRecommendations', true); - private workspaceFolderContextKey = new RawContextKey('workspaceFolderRecommendations', true); - private addToWorkspaceRecommendationsContextKey = new RawContextKey('addToWorkspaceRecommendations', false); - private addToWorkspaceFolderRecommendationsContextKey = new RawContextKey('addToWorkspaceFolderRecommendations', false); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IEditorService editorService: IEditorService - ) { - super(); - const boundWorkspaceContextKey = this.workspaceContextKey.bindTo(contextKeyService); - boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE); - this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); - - const boundWorkspaceFolderContextKey = this.workspaceFolderContextKey.bindTo(contextKeyService); - boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0); - this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0))); - - const boundAddToWorkspaceRecommendationsContextKey = this.addToWorkspaceRecommendationsContextKey.bindTo(contextKeyService); - boundAddToWorkspaceRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE); - this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceRecommendationsContextKey.set( - editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); - this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundAddToWorkspaceRecommendationsContextKey.set( - editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); - - const boundAddToWorkspaceFolderRecommendationsContextKey = this.addToWorkspaceFolderRecommendationsContextKey.bindTo(contextKeyService); - boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput); - this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput))); - - this.registerCommands(); - } - - private registerCommands(): void { - CommandsRegistry.registerCommand(ConfigureWorkspaceRecommendedExtensionsAction.ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions") - }, - when: this.workspaceContextKey - }); - - CommandsRegistry.registerCommand(ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, - category: localize('extensions', "Extensions") - }, - when: this.workspaceFolderContextKey - }); - - CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.ADD_ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService) - .createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.ADD_ID, AddToWorkspaceRecommendationsAction.ADD_LABEL) - .run(AddToWorkspaceRecommendationsAction.ADD); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: AddToWorkspaceRecommendationsAction.ADD_ID, - title: { value: AddToWorkspaceRecommendationsAction.ADD_LABEL, original: 'Add to Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions") - }, - when: this.addToWorkspaceRecommendationsContextKey - }); - - CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.ADD_ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService) - .createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.ADD_ID, AddToWorkspaceFolderRecommendationsAction.ADD_LABEL) - .run(AddToWorkspaceRecommendationsAction.ADD); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: AddToWorkspaceFolderRecommendationsAction.ADD_ID, - title: { value: AddToWorkspaceFolderRecommendationsAction.ADD_LABEL, original: 'Extensions: Add to Recommended Extensions (Workspace Folder)' }, - category: localize('extensions', "Extensions") - }, - when: this.addToWorkspaceFolderRecommendationsContextKey - }); - - CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.IGNORE_ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService) - .createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.IGNORE_ID, AddToWorkspaceRecommendationsAction.IGNORE_LABEL) - .run(AddToWorkspaceRecommendationsAction.IGNORE); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: AddToWorkspaceRecommendationsAction.IGNORE_ID, - title: { value: AddToWorkspaceRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace)' }, - category: localize('extensions', "Extensions") - }, - when: this.addToWorkspaceRecommendationsContextKey - }); - - CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService) - .createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL) - .run(AddToWorkspaceRecommendationsAction.IGNORE); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, - title: { value: AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace Folder)' }, - category: localize('extensions', "Extensions") - }, - when: this.addToWorkspaceFolderRecommendationsContextKey - }); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2293,83 +2203,6 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio })); } - protected addExtensionToWorkspaceConfig(workspaceConfigurationFile: URI, extensionId: string, shouldRecommend: boolean) { - return this.getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile) - .then(content => { - const extensionIdLowerCase = extensionId.toLowerCase(); - const workspaceExtensionsConfigContent: IExtensionsConfigContent = (json.parse(content.value.toString()) || {})['extensions'] || {}; - let insertInto = shouldRecommend ? workspaceExtensionsConfigContent.recommendations || [] : workspaceExtensionsConfigContent.unwantedRecommendations || []; - let removeFrom = shouldRecommend ? workspaceExtensionsConfigContent.unwantedRecommendations || [] : workspaceExtensionsConfigContent.recommendations || []; - - if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) { - return Promise.resolve(null); - } - - insertInto.push(extensionId); - removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase); - - return this.jsonEditingService.write(workspaceConfigurationFile, - [{ - path: ['extensions'], - value: { - recommendations: shouldRecommend ? insertInto : removeFrom, - unwantedRecommendations: shouldRecommend ? removeFrom : insertInto - } - }], - true); - }); - } - - protected addExtensionToWorkspaceFolderConfig(extensionsFileResource: URI, extensionId: string, shouldRecommend: boolean): Promise { - return this.getOrCreateExtensionsFile(extensionsFileResource) - .then(({ content }) => { - const extensionIdLowerCase = extensionId.toLowerCase(); - const extensionsConfigContent: IExtensionsConfigContent = json.parse(content) || {}; - let insertInto = shouldRecommend ? extensionsConfigContent.recommendations || [] : extensionsConfigContent.unwantedRecommendations || []; - let removeFrom = shouldRecommend ? extensionsConfigContent.unwantedRecommendations || [] : extensionsConfigContent.recommendations || []; - - if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) { - return Promise.resolve(null); - } - - insertInto.push(extensionId); - - let removeFromPromise: Promise = Promise.resolve(); - if (removeFrom.some(e => e.toLowerCase() === extensionIdLowerCase)) { - removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase); - removeFromPromise = this.jsonEditingService.write(extensionsFileResource, - [{ - path: shouldRecommend ? ['unwantedRecommendations'] : ['recommendations'], - value: removeFrom - }], - true); - } - - return removeFromPromise.then(() => - this.jsonEditingService.write(extensionsFileResource, - [{ - path: shouldRecommend ? ['recommendations'] : ['unwantedRecommendations'], - value: insertInto - }], - true) - ); - }); - } - - protected getWorkspaceExtensionsConfigContent(extensionsFileResource: URI): Promise { - return Promise.resolve(this.fileService.readFile(extensionsFileResource)) - .then(content => { - return (json.parse(content.value.toString()) || {})['extensions'] || {}; - }, err => ({ recommendations: [], unwantedRecommendations: [] })); - } - - protected getWorkspaceFolderExtensionsConfigContent(extensionsFileResource: URI): Promise { - return Promise.resolve(this.fileService.readFile(extensionsFileResource)) - .then(content => { - return (json.parse(content.value.toString()) || {}); - }, err => ({ recommendations: [], unwantedRecommendations: [] })); - } - private getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile: URI): Promise { return Promise.resolve(this.fileService.readFile(workspaceConfigurationFile)) .then(content => { @@ -2488,161 +2321,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac } } -export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction { - static readonly ADD = true; - static readonly IGNORE = false; - static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceFolderRecommendations'; - static readonly ADD_LABEL = localize('addToWorkspaceFolderRecommendations', "Add to Recommended Extensions (Workspace Folder)"); - static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations'; - static readonly IGNORE_LABEL = localize('addToWorkspaceFolderIgnoredRecommendations', "Ignore Recommended Extension (Workspace Folder)"); - - constructor( - id: string, - label: string, - @IFileService fileService: IFileService, - @ITextFileService textFileService: ITextFileService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IEditorService editorService: IEditorService, - @IJSONEditingService jsonEditingService: IJSONEditingService, - @ITextModelService textModelResolverService: ITextModelService, - @ICommandService private readonly commandService: ICommandService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); - } - - run(shouldRecommend: boolean): Promise { - if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension) { - return Promise.resolve(); - } - const folders = this.contextService.getWorkspace().folders; - if (!folders || !folders.length) { - this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.noWorkspace', 'There are no workspace folders open to add recommendations.')); - return Promise.resolve(); - } - - const extensionId = this.editorService.activeEditor.extension.identifier; - const pickFolderPromise = folders.length === 1 - ? Promise.resolve(folders[0]) - : this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); - return Promise.resolve(pickFolderPromise) - .then(workspaceFolder => { - if (!workspaceFolder) { - return Promise.resolve(); - } - const configurationFile = workspaceFolder.toResource(EXTENSIONS_CONFIG); - return this.getWorkspaceFolderExtensionsConfigContent(configurationFile).then(content => { - const extensionIdLowerCase = extensionId.id.toLowerCase(); - if (shouldRecommend) { - if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { - this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s recommendations.')); - return Promise.resolve(); - } - - return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId.id, shouldRecommend).then(() => { - this.notificationService.prompt(Severity.Info, - localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.'), - [{ - label: localize('viewChanges', "View Changes"), - run: () => this.openExtensionsFile(configurationFile) - }]); - }, err => { - this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); - }); - } - else { - if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { - this.notificationService.info(localize('AddToWorkspaceFolderIgnoredRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s unwanted recommendations.')); - return Promise.resolve(); - } - - return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId.id, shouldRecommend).then(() => { - this.notificationService.prompt(Severity.Info, - localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.'), - [{ - label: localize('viewChanges', "View Changes"), - run: () => this.openExtensionsFile(configurationFile) - }]); - }, err => { - this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); - }); - } - }); - }); - } -} - -export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction { - static readonly ADD = true; - static readonly IGNORE = false; - static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceRecommendations'; - static readonly ADD_LABEL = localize('addToWorkspaceRecommendations', "Add to Recommended Extensions (Workspace)"); - static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations'; - static readonly IGNORE_LABEL = localize('addToWorkspaceIgnoredRecommendations', "Ignore Recommended Extension (Workspace)"); - - constructor( - id: string, - label: string, - @IFileService fileService: IFileService, - @ITextFileService textFileService: ITextFileService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IEditorService editorService: IEditorService, - @IJSONEditingService jsonEditingService: IJSONEditingService, - @ITextModelService textModelResolverService: ITextModelService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); - } - - run(shouldRecommend: boolean): Promise { - const workspaceConfig = this.contextService.getWorkspace().configuration; - - if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension || !workspaceConfig) { - return Promise.resolve(); - } - - const extensionId = this.editorService.activeEditor.extension.identifier; - - return this.getWorkspaceExtensionsConfigContent(workspaceConfig).then(content => { - const extensionIdLowerCase = extensionId.id.toLowerCase(); - if (shouldRecommend) { - if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { - this.notificationService.info(localize('AddToWorkspaceRecommendations.alreadyExists', 'This extension is already present in workspace recommendations.')); - return Promise.resolve(); - } - - return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId.id, shouldRecommend).then(() => { - this.notificationService.prompt(Severity.Info, - localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.'), - [{ - label: localize('viewChanges', "View Changes"), - run: () => this.openWorkspaceConfigurationFile(workspaceConfig) - }]); - - }, err => { - this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); - }); - } else { - if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { - this.notificationService.info(localize('AddToWorkspaceUnwantedRecommendations.alreadyExists', 'This extension is already present in workspace unwanted recommendations.')); - return Promise.resolve(); - } - - return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId.id, shouldRecommend).then(() => { - this.notificationService.prompt(Severity.Info, - localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.'), - [{ - label: localize('viewChanges', "View Changes"), - run: () => this.openWorkspaceConfigurationFile(workspaceConfig) - }]); - }, err => { - this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); - }); - } - }); - } -} - export class StatusLabelAction extends Action implements IExtensionContainer { private static readonly ENABLED_CLASS = `${ExtensionAction.TEXT_ACTION_CLASS} extension-status-label`; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8dc0c106914..c5fc5e44a5c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -44,7 +44,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IMenuService } from 'vs/platform/actions/common/actions'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -112,7 +111,6 @@ export class ExtensionsListView extends ViewPane { @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IMenuService private readonly menuService: IMenuService, @IOpenerService openerService: IOpenerService, @IPreferencesService private readonly preferencesService: IPreferencesService, ) { @@ -251,7 +249,7 @@ export class ExtensionsListView extends ViewPane { getActions: () => actions.slice(0, actions.length - 1) }); } else if (e.element) { - const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), this.instantiationService, e.element); + const groups = getContextMenuActions(e.element, false, this.instantiationService); groups.forEach(group => group.forEach(extensionAction => { if (extensionAction instanceof ExtensionAction) { extensionAction.extension = e.element!; @@ -890,7 +888,6 @@ export class ServerExtensionsView extends ExtensionsListView { @IWorkbenchExtensioManagementService extensionManagementService: IWorkbenchExtensioManagementService, @IProductService productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService menuService: IMenuService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @IPreferencesService preferencesService: IPreferencesService, @@ -898,7 +895,7 @@ export class ServerExtensionsView extends ExtensionsListView { options.server = server; super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService, - contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); + contextKeyService, viewDescriptorService, openerService, preferencesService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 3a49d109175..b79ec641beb 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -76,7 +76,6 @@ .extension-editor > .header > .details > .title > .identifier { margin-left: 10px; font-size: 14px; - opacity: 0.6; background: rgba(173, 173, 173, 0.31); padding: 0px 4px; border-radius: 4px; diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 231aa8f4b47..013e981bf18 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -7,12 +7,12 @@ import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platf import { distinct, flatten } from 'vs/base/common/arrays'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IExtensionsConfigContent, ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; +import { IExtensionsConfigContent, IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; export class WorkspaceRecommendations extends ExtensionRecommendations { @@ -51,23 +51,28 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`); } + this._recommendations = []; this._ignoredRecommendations = []; for (const extensionsConfig of extensionsConfigs) { - for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) { - if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) { - this._ignoredRecommendations.push(unwantedRecommendation); + if (extensionsConfig.unwantedRecommendations) { + for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) { + if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) { + this._ignoredRecommendations.push(unwantedRecommendation); + } } } - for (const extensionId of extensionsConfig.recommendations) { - if (invalidRecommendations.indexOf(extensionId) === -1) { - this._recommendations.push({ - extensionId, - reason: { - reasonId: ExtensionRecommendationReason.Workspace, - reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") - } - }); + if (extensionsConfig.recommendations) { + for (const extensionId of extensionsConfig.recommendations) { + if (invalidRecommendations.indexOf(extensionId) === -1) { + this._recommendations.push({ + extensionId, + reason: { + reasonId: ExtensionRecommendationReason.Workspace, + reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") + } + }); + } } } } @@ -114,12 +119,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } private async onDidChangeExtensionsConfigs(): Promise { - const oldWorkspaceRecommended = this._recommendations; await this.fetch(); - // Suggest only if at least one of the newly added recommendations was not suggested before - if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) { - this._onDidChangeRecommendations.fire(); - } + this._onDidChangeRecommendations.fire(); } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index d4803919342..0e4312af5ea 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -538,9 +538,7 @@ export class ReportExtensionIssueAction extends Action { @INativeHostService private readonly nativeHostService: INativeHostService ) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); - this.enabled = extension.marketplaceInfo - && extension.marketplaceInfo.type === ExtensionType.User - && !!extension.description.repository && !!extension.description.repository.url; + this.enabled = !!extension.description.repository && !!extension.description.repository.url; } async run(): Promise { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index 2f44fabf0ae..311037fc22c 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -304,14 +304,14 @@ suite('ExtensionRecommendationsService Test', () => { }, null, '\t')); const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); + const fileService = new FileService(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + instantiationService.stub(IFileService, fileService); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IWorkpsaceExtensionsConfigService, instantiationService.createInstance(WorkspaceExtensionsConfigService)); instantiationService.stub(IExtensionIgnoredRecommendationsService, instantiationService.createInstance(ExtensionIgnoredRecommendationsService)); instantiationService.stub(IExtensionRecommendationNotificationService, instantiationService.createInstance(ExtensionRecommendationNotificationService)); - const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - instantiationService.stub(IFileService, fileService); } function testNoPromptForValidRecommendations(recommendations: string[]) { diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 086d9c9dd57..6dd0c4da584 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -23,9 +23,11 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; +import { ContextKeyOrExpr } from 'vs/platform/contextkey/common/contextkey'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); +const REVEAL_IN_OS_WHEN_CONTEXT = ContextKeyOrExpr.create([ResourceContextKey.Scheme.isEqualTo(Schemas.file), ResourceContextKey.Scheme.isEqualTo(Schemas.userData)]); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REVEAL_IN_OS_COMMAND_ID, @@ -57,19 +59,19 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, ResourceContextKey.Scheme.isEqualTo(Schemas.file)); +appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, REVEAL_IN_OS_WHEN_CONTEXT); // Menu registration - open editors const revealInOsCommand = { id: REVEAL_IN_OS_COMMAND_ID, - title: isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder") + title: REVEAL_IN_OS_LABEL }; MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 20, command: revealInOsCommand, - when: ResourceContextKey.IsFileSystemResource + when: REVEAL_IN_OS_WHEN_CONTEXT }); // Menu registration - explorer @@ -78,10 +80,10 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: 'navigation', order: 20, command: revealInOsCommand, - when: ResourceContextKey.Scheme.isEqualTo(Schemas.file) + when: REVEAL_IN_OS_WHEN_CONTEXT }); // Command Palette const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; -appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in File Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category, ResourceContextKey.Scheme.isEqualTo(Schemas.file)); +appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in File Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category, REVEAL_IN_OS_WHEN_CONTEXT); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 9355b4e6dc1..1efff633b1b 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -32,7 +32,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { alias: 'Format Document', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider.toNegated()), kbOpts: { - kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentFormattingProvider.toNegated()), + kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, weight: KeybindingWeight.EditorContrib, @@ -55,6 +55,8 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { return commandService.executeCommand('editor.action.formatDocument.multiple'); } else if (formatterCount === 1) { return commandService.executeCommand('editor.action.formatDocument'); + } else if (model.isTooLargeForSyncing()) { + notificationService.prompt(Severity.Info, nls.localize('too.large', "This file cannot be formatted because it is too large"), []); } else { const langName = model.getLanguageIdentifier().language; const message = nls.localize('no.provider', "There is no formatter for '{0}' files installed.", langName); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts index a53d3536553..3af3a0c7832 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -62,7 +62,8 @@ const fixedDiffEditorOptions: IDiffEditorOptions = { glyphMargin: true, enableSplitViewResizing: false, renderIndicators: false, - readOnly: false + readOnly: false, + isInEmbeddedEditor: true }; @@ -674,7 +675,7 @@ abstract class AbstractCellRenderer extends Disposable { const mode = this.modeService.create('json'); const originaloutputSource = this._getFormatedOutputJSON( - this.notebookEditor.textModel!.transientOptions + this.notebookEditor.textModel!.transientOptions.transientOutputs ? [] : this.cell.type === 'insert' ? this.cell.modified!.outputs || [] diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 8c59d791150..5f5e5c47554 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -95,7 +95,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( // Register Preferences Editor Input Factory class PreferencesEditorInputFactory extends AbstractSideBySideEditorInputFactory { - protected createEditorInput(name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { + protected createEditorInput(instantiationService: IInstantiationService, name: string, description: string | undefined, secondaryInput: EditorInput, primaryInput: EditorInput): EditorInput { return new PreferencesEditorInput(name, description, secondaryInput, primaryInput); } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index b2eb1cb2909..bc258efa43e 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -17,7 +17,7 @@ export interface ITOCEntry { export const commonlyUsedData: ITOCEntry = { id: 'commonlyUsed', label: localize('commonlyUsed', "Commonly Used"), - settings: ['files.autoSave', 'editor.fontSize', 'editor.fontFamily', 'editor.tabSize', 'editor.renderWhitespace', 'editor.cursorStyle', 'editor.multiCursorModifier', 'editor.insertSpaces', 'editor.wordWrap', 'files.exclude', 'files.associations'] + settings: ['files.autoSave', 'editor.fontSize', 'editor.fontFamily', 'editor.tabSize', 'editor.renderWhitespace', 'editor.cursorStyle', 'editor.multiCursorModifier', 'editor.insertSpaces', 'editor.wordWrap', 'files.exclude', 'files.associations', 'workbench.editor.enablePreview'] }; export const tocData: ITOCEntry = { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index c4ecfc2d53b..0c7d7dca4b2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1038,7 +1038,11 @@ class ViewModel { } for (const repository of this.scmViewService.visibleRepositories) { - const item = this.items.get(repository)!; + const item = this.items.get(repository); + + if (!item) { + continue; + } // go backwards from last group for (let j = item.groupItems.length - 1; j >= 0; j--) { @@ -1482,7 +1486,8 @@ class SCMInputWidget extends Disposable { padding: { top: 3, bottom: 3 }, quickSuggestions: false, scrollbar: { alwaysConsumeMouseWheel: false }, - overflowWidgetsDomNode + overflowWidgetsDomNode, + renderWhitespace: 'none' }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 83a6db8d711..c85764d0724 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -24,6 +24,8 @@ import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/b import { Range } from 'vs/editor/common/core/range'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { mergeSort } from 'vs/base/common/arrays'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { dirname } from 'vs/base/common/resources'; const REPLACE_PREVIEW = 'replacePreview'; @@ -93,7 +95,8 @@ export class ReplaceService implements IReplaceService { @ITextFileService private readonly textFileService: ITextFileService, @IEditorService private readonly editorService: IEditorService, @ITextModelService private readonly textModelResolverService: ITextModelService, - @IBulkEditService private readonly bulkEditorService: IBulkEditService + @IBulkEditService private readonly bulkEditorService: IBulkEditService, + @ILabelService private readonly labelService: ILabelService ) { } replace(match: Match): Promise; @@ -113,6 +116,7 @@ export class ReplaceService implements IReplaceService { leftResource: fileMatch.resource, rightResource: toReplaceResource(fileMatch.resource), label: nls.localize('fileReplaceChanges', "{0} ↔ {1} (Replace Preview)", fileMatch.name(), fileMatch.name()), + description: this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true }), options: { preserveFocus, pinned, diff --git a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts index dc2cdaeaf94..76c12fa6a45 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -102,7 +102,7 @@ class PartsSplash { layoutInfo, baseTheme }), - { encoding: 'utf8', overwriteEncoding: true } + { encoding: 'utf8' } ); if (baseTheme !== this._lastBaseTheme || colorInfo.editorBackground !== this._lastBackground) { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 9cbba05bec8..895c5d54164 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -40,7 +40,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IOutputService, IOutputChannel } from 'vs/workbench/contrib/output/common/output'; @@ -1702,7 +1702,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } - if (!contributed) { + if (contributed.length === 0) { result.add(key, ...folderTasks.set.tasks); } else { let configurations = folderTasks.configurations; @@ -1867,31 +1867,35 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract updateWorkspaceTasks(runSource: TaskRunSource | void): void; protected computeWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise> { - if (this.workspaceFolders.length === 0) { - return Promise.resolve(new Map()); - } else { - let promises: Promise[] = []; - for (let folder of this.workspaceFolders) { - promises.push(this.computeWorkspaceFolderTasks(folder, runSource).then((value) => value, () => undefined)); + let promises: Promise[] = []; + for (let folder of this.workspaceFolders) { + promises.push(this.computeWorkspaceFolderTasks(folder, runSource).then((value) => value, () => undefined)); + } + return Promise.all(promises).then(async (values) => { + let result = new Map(); + for (let value of values) { + if (value) { + result.set(value.workspaceFolder.uri.toString(), value); + } } - return Promise.all(promises).then(async (values) => { - let result = new Map(); - for (let value of values) { - if (value) { - result.set(value.workspaceFolder.uri.toString(), value); - } - } - const userTasks = await this.computeUserTasks(this.workspaceFolders[0], runSource).then((value) => value, () => undefined); - if (userTasks) { - result.set(USER_TASKS_GROUP_KEY, userTasks); - } - const workspaceFileTasks = await this.computeWorkspaceFileTasks(this.workspaceFolders[0], runSource).then((value) => value, () => undefined); + let folder = this.workspaceFolders.length > 0 ? this.workspaceFolders[0] : undefined; + if (!folder) { + const userhome = await this.pathService.userHome(); + folder = new WorkspaceFolder({ uri: userhome, name: resources.basename(userhome), index: 0 }); + } + const userTasks = await this.computeUserTasks(folder, runSource).then((value) => value, () => undefined); + if (userTasks) { + result.set(USER_TASKS_GROUP_KEY, userTasks); + } + + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + const workspaceFileTasks = await this.computeWorkspaceFileTasks(folder, runSource).then((value) => value, () => undefined); if (workspaceFileTasks && this._workspace && this._workspace.configuration) { result.set(this._workspace.configuration.toString(), workspaceFileTasks); } - return result; - }); - } + } + return result; + }); } private get jsonTasksSupported(): boolean { @@ -2167,17 +2171,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private canRunCommand(): boolean { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.prompt( - Severity.Info, - nls.localize('TaskService.noWorkspace', "Tasks are only available on a workspace folder."), - [{ - label: nls.localize('TaskService.learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://code.visualstudio.com/docs/editor/tasks')) - }] - ); - return false; - } return true; } @@ -2871,8 +2864,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (this.isTaskEntry(selection)) { this.configureTask(selection.task); - } else { + } else if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { this.openTaskFile(selection.folder.toResource('.vscode/tasks.json'), TaskSourceKind.Workspace); + } else { + const resource = this.getResourceForKind(TaskSourceKind.User); + if (resource) { + this.openTaskFile(resource, TaskSourceKind.User); + } } } @@ -2914,46 +2912,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return taskPromise.then((taskMap) => { let entries: QuickPickInput[] = []; let needsCreateOrOpen: boolean = true; - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { - let tasks = taskMap.all(); - if (tasks.length > 0) { - tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); - for (let task of tasks) { - entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this.showDetail() ? task.configurationProperties.detail : undefined }); - if (!ContributedTask.is(task)) { - needsCreateOrOpen = false; - } + let tasks = taskMap.all(); + if (tasks.length > 0) { + tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); + for (let task of tasks) { + entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this.showDetail() ? task.configurationProperties.detail : undefined }); + if (!ContributedTask.is(task)) { + needsCreateOrOpen = false; } } - if (needsCreateOrOpen) { - let label = stats[0] !== undefined ? openLabel : createLabel; - if (entries.length) { - entries.push({ type: 'separator' }); - } - entries.push({ label, folder: this.contextService.getWorkspace().folders[0] }); - } - } else { - let folders = this.contextService.getWorkspace().folders; - let index = 0; - for (let folder of folders) { - let tasks = taskMap.get(folder); - if (tasks.length > 0) { - tasks = tasks.slice().sort((a, b) => a._label.localeCompare(b._label)); - for (let i = 0; i < tasks.length; i++) { - let entry: TaskQuickPickEntryType = { label: tasks[i]._label, task: tasks[i], description: this.getTaskDescription(tasks[i]) }; - if (i === 0) { - entries.push({ type: 'separator', label: folder.name }); - } - entries.push(entry); - } - } else { - let label = stats[index] !== undefined ? openLabel : createLabel; - let entry: TaskQuickPickEntryType = { label, folder: folder }; - entries.push({ type: 'separator', label: folder.name }); - entries.push(entry); - } - index++; + } + if (needsCreateOrOpen) { + let label = stats[0] !== undefined ? openLabel : createLabel; + if (entries.length) { + entries.push({ type: 'separator' }); } + entries.push({ label, folder: this.contextService.getWorkspace().folders[0] }); } if ((entries.length === 1) && !needsCreateOrOpen) { tokenSource.cancel(); diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 004f9217c13..511c8296399 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -31,7 +31,7 @@ type TelemetryData = { ext: string; path: number; reason?: number; - whitelistedjson?: string; + allowlistedjson?: string; }; type FileTelemetryDataFragment = { @@ -39,13 +39,13 @@ type FileTelemetryDataFragment = { ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; path: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; reason?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - whitelistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + allowlistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; export class TelemetryContribution extends Disposable implements IWorkbenchContribution { - private static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json']; - private static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json']; + private static ALLOWLIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json']; + private static ALLOWLIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json']; constructor( @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -184,7 +184,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr for (const folder of folders) { if (isEqualOrParent(resource, folder.toResource('.vscode'))) { const filename = basename(resource); - if (TelemetryContribution.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) { + if (TelemetryContribution.ALLOWLIST_WORKSPACE_JSON.indexOf(filename) > -1) { return `.vscode/${filename}`; } } @@ -202,11 +202,11 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr ext, path: hash(path), reason, - whitelistedjson: undefined as string | undefined + allowlistedjson: undefined as string | undefined }; - if (ext === '.json' && TelemetryContribution.WHITELIST_JSON.indexOf(fileName) > -1) { - telemetryData['whitelistedjson'] = fileName; + if (ext === '.json' && TelemetryContribution.ALLOWLIST_JSON.indexOf(fileName) > -1) { + telemetryData['allowlistedjson'] = fileName; } return telemetryData; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index 5c0e8243fd4..fb420ef52ae 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -49,6 +49,8 @@ export const unixLineAndColumnMatchIndex = 11; // Each line and column clause have 6 groups (ie no. of expressions in round brackets) export const lineAndColumnClauseGroupCount = 6; +const MAX_LENGTH = 2000; + export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider { constructor( private readonly _xterm: Terminal, @@ -85,6 +87,9 @@ export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider } const text = getXtermLineContent(this._xterm.buffer.active, startLine, endLine, this._xterm.cols); + if (text.length > MAX_LENGTH) { + return []; + } // clone regex to do a global search on text const rex = new RegExp(this._localLinkRegex, 'g'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index 793b5ae848c..2de671a97e7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -8,10 +8,11 @@ import { Color } from 'vs/base/common/color'; import { debounce } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { XTermAttributes, XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; -import { IBeforeProcessDataEvent, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DEFAULT_LOCAL_ECHO_EXCLUDE, IBeforeProcessDataEvent, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { IBuffer, IBufferCell, IDisposable, ITerminalAddon, Terminal } from 'xterm'; const ESC = '\x1b'; @@ -1178,11 +1179,16 @@ class TypeAheadStyle implements IDisposable { } } +const compileExcludeRegexp = (programs = DEFAULT_LOCAL_ECHO_EXCLUDE) => + new RegExp(`\\b(${programs.map(escapeRegExpCharacters).join('|')})\\b`, 'i'); + export class TypeAheadAddon extends Disposable implements ITerminalAddon { private typeaheadStyle?: TypeAheadStyle; private typeaheadThreshold = this.config.config.localEchoLatencyThreshold; + private excludeProgramRe = compileExcludeRegexp(this.config.config.localEchoExcludePrograms); protected lastRow?: { y: number; startingX: number }; - private timeline?: PredictionTimeline; + protected timeline?: PredictionTimeline; + private terminalTitle = ''; public stats?: PredictionStats; /** @@ -1206,6 +1212,10 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { timeline.setShowPredictions(this.typeaheadThreshold === 0); this._register(terminal.onData(e => this.onUserData(e))); + this._register(terminal.onTitleChange(title => { + this.terminalTitle = title; + this.reevaluatePredictorState(stats, timeline); + })); this._register(terminal.onResize(() => { timeline.setShowPredictions(false); timeline.clearCursor(); @@ -1214,6 +1224,7 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { this._register(this.config.onConfigChanged(() => { style.onUpdate(this.config.config.localEchoStyle); this.typeaheadThreshold = this.config.config.localEchoLatencyThreshold; + this.excludeProgramRe = compileExcludeRegexp(this.config.config.localEchoExcludePrograms); this.reevaluatePredictorState(stats, timeline); })); this._register(this.processManager.onBeforeProcessData(e => this.onBeforeProcessData(e))); @@ -1260,8 +1271,14 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { * terminal cursor is not updated, causes issues. */ @debounce(100) - private reevaluatePredictorState(stats: PredictionStats, timeline: PredictionTimeline) { - if (this.typeaheadThreshold < 0) { + protected reevaluatePredictorState(stats: PredictionStats, timeline: PredictionTimeline) { + this.reevaluatePredictorStateNow(stats, timeline); + } + + protected reevaluatePredictorStateNow(stats: PredictionStats, timeline: PredictionTimeline) { + if (this.excludeProgramRe.test(this.terminalTitle)) { + timeline.setShowPredictions(false); + } else if (this.typeaheadThreshold < 0) { timeline.setShowPredictions(false); } else if (this.typeaheadThreshold === 0) { timeline.setShowPredictions(true); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 3b32d9e9fcb..73a390bdf8f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -136,11 +136,14 @@ export interface ITerminalConfiguration { unicodeVersion: '6' | '11'; experimentalLinkProvider: boolean; localEchoLatencyThreshold: number; + localEchoExcludePrograms: ReadonlyArray; localEchoStyle: 'bold' | 'dim' | 'italic' | 'underlined' | 'inverted' | string; serverSpawn: boolean; enablePersistentSessions: boolean; } +export const DEFAULT_LOCAL_ECHO_EXCLUDE: ReadonlyArray = ['vim', 'vi', 'nano', 'tmux']; + export interface ITerminalConfigHelper { config: ITerminalConfiguration; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 9b1a6b4af4d..61cfd0b0bac 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -6,7 +6,7 @@ import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_LOCAL_ECHO_EXCLUDE } from 'vs/workbench/contrib/terminal/common/terminal'; import { isMacintosh, isWindows, Platform } from 'vs/base/common/platform'; export const terminalConfiguration: IConfigurationNode = { @@ -358,6 +358,15 @@ export const terminalConfiguration: IConfigurationNode = { minimum: -1, default: 30, }, + 'terminal.integrated.localEchoExcludePrograms': { + description: localize('terminal.integrated.localEchoExcludePrograms', "Experimental: local echo will be disabled when any of these program names are found in the terminal title."), + type: 'array', + items: { + type: 'string', + uniqueItems: true + }, + default: DEFAULT_LOCAL_ECHO_EXCLUDE, + }, 'terminal.integrated.localEchoStyle': { description: localize('terminal.integrated.localEchoStyle', "Experimental: terminal style of locally echoed text; either a font style or an RGB color."), default: 'dim', diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts index 3434cd4211a..a0a66dfe4a7 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts @@ -8,9 +8,10 @@ import { Terminal } from 'xterm'; import { SinonStub, stub, useFakeTimers } from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { IPrediction, PredictionStats, TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; -import { IBeforeProcessDataEvent, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DEFAULT_LOCAL_ECHO_EXCLUDE, IBeforeProcessDataEvent, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { timeout } from 'vs/base/common/async'; const CSI = `\x1b[`; @@ -92,7 +93,8 @@ suite('Workbench - Terminal Typeahead', () => { setup(() => { config = upcastPartial({ localEchoStyle: 'italic', - localEchoLatencyThreshold: 0 + localEchoLatencyThreshold: 0, + localEchoExcludePrograms: DEFAULT_LOCAL_ECHO_EXCLUDE, }); publicLog = stub(); addon = new TestTypeAheadAddon( @@ -260,6 +262,24 @@ suite('Workbench - Terminal Typeahead', () => { onBeforeProcessData.fire({ data: 'o' }); } }); + + test('disables on title change', async () => { + const t = createMockTerminal({ lines: ['hello|'] }); + addon.activate(t.terminal); + + await timeout(1000); + + addon.reevaluateNow(); + assert.strictEqual(addon.isShowing, true, 'expected to show initially'); + + t.onTitleChange.fire('foo - VIM.exe'); + addon.reevaluateNow(); + assert.strictEqual(addon.isShowing, false, 'expected to hide when vim is open'); + + t.onTitleChange.fire('foo - git.exe'); + addon.reevaluateNow(); + assert.strictEqual(addon.isShowing, true, 'expected to show again after vim closed'); + }); }); }); @@ -267,6 +287,14 @@ class TestTypeAheadAddon extends TypeAheadAddon { public unlockLeftNavigating() { this.lastRow = { y: 1, startingX: 1 }; } + + public reevaluateNow() { + this.reevaluatePredictorStateNow(this.stats!, this.timeline!); + } + + public get isShowing() { + return !!this.timeline?.isShowingPredictions; + } } function upcastPartial(v: Partial): T { @@ -292,6 +320,7 @@ function createMockTerminal({ lines, cursorAttrs }: { }) { const written: string[] = []; const cursor = { y: 1, x: 1 }; + const onTitleChange = new Emitter(); const onData = new Emitter(); const csiEmitter = new Emitter(); @@ -315,11 +344,13 @@ function createMockTerminal({ lines, cursorAttrs }: { clearWritten: () => written.splice(0, written.length), onData: (s: string) => onData.fire(s), csiEmitter, + onTitleChange, terminal: { cols: 80, rows: 5, onResize: new Emitter().event, onData: onData.event, + onTitleChange: onTitleChange.event, parser: { registerCsiHandler(_: unknown, callback: () => void) { csiEmitter.event(callback); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 660bbeb865f..f48dcb3b246 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -661,6 +661,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo leftResource: conflict.remoteResource, rightResource: conflict.previewResource, label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), + description: localize('sideBySideDescription', "Settings Sync"), options: { preserveFocus: false, pinned: true, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts index 69e0cf9e60a..3610b1ca5ac 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts @@ -318,6 +318,7 @@ export class UserDataSyncMergesViewPane extends TreeViewPane { leftResource, rightResource, label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), + description: localize('sideBySideDescription', "Settings Sync"), options: { preserveFocus: true, revealIfVisible: true, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 74bac346ea3..a29cb807d85 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -279,6 +279,7 @@ export class UserDataSyncDataViews extends Disposable { leftResource, rightResource, label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), + description: localize('sideBySideDescription', "Settings Sync"), options: { preserveFocus: true, revealIfVisible: true, diff --git a/src/vs/workbench/contrib/views/browser/media/views.css b/src/vs/workbench/contrib/views/browser/media/views.css index 8b30fccc585..7ff3318d576 100644 --- a/src/vs/workbench/contrib/views/browser/media/views.css +++ b/src/vs/workbench/contrib/views/browser/media/views.css @@ -64,7 +64,6 @@ } .monaco-workbench .pane > .pane-body > .welcome-view .monaco-button { - max-width: 260px; margin-left: auto; margin-right: auto; } diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 0f0773fce5a..c088c40d384 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -439,10 +439,18 @@ // propagate focus host.onMessage('focus', () => { - const target = getActiveFrame(); - if (target) { - target.contentWindow.focus(); + const activeFrame = getActiveFrame(); + if (!activeFrame || !activeFrame.contentWindow) { + return; } + + if (document.activeElement === activeFrame) { + // We are already focused on the iframe (or one of its children) so no need + // to refocus. + return; + } + + activeFrame.contentWindow.focus(); }); // update iframe-contents @@ -573,6 +581,8 @@ newFrame.contentWindow.focus(); } + + contentWindow.addEventListener('scroll', handleInnerScroll); contentWindow.addEventListener('wheel', handleWheel); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 6ac6a86d729..d11fa227334 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -8,96 +8,35 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { ThrottledDelayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; -import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget'; -import { WebviewResourceRequestManager, rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; - -class WebviewKeyboardHandler { - - private readonly _webviews = new Set(); - private readonly _isUsingNativeTitleBars: boolean; - - private readonly webviewMainService: IWebviewManagerService; - - constructor( - configurationService: IConfigurationService, - mainProcessService: IMainProcessService, - ) { - this._isUsingNativeTitleBars = configurationService.getValue('window.titleBarStyle') === 'native'; - - this.webviewMainService = createChannelSender(mainProcessService.getChannel('webview')); - } - - public add(webview: WebviewTag): IDisposable { - this._webviews.add(webview); - - const disposables = new DisposableStore(); - - if (this.shouldToggleMenuShortcutsEnablement) { - this.setIgnoreMenuShortcutsForWebview(webview, true); - } - - disposables.add(addDisposableListener(webview, 'ipc-message', (event) => { - switch (event.channel) { - case 'did-focus': - this.setIgnoreMenuShortcuts(true); - break; - - case 'did-blur': - this.setIgnoreMenuShortcuts(false); - return; - } - })); - - return toDisposable(() => { - disposables.dispose(); - this._webviews.delete(webview); - }); - } - - private get shouldToggleMenuShortcutsEnablement() { - return isMacintosh || this._isUsingNativeTitleBars; - } - - private setIgnoreMenuShortcuts(value: boolean) { - for (const webview of this._webviews) { - this.setIgnoreMenuShortcutsForWebview(webview, value); - } - } - - private setIgnoreMenuShortcutsForWebview(webview: WebviewTag, value: boolean) { - if (this.shouldToggleMenuShortcutsEnablement) { - this.webviewMainService.setIgnoreMenuShortcuts(webview.getWebContentsId(), value); - } - } -} +import { WebviewIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager'; +import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class ElectronWebviewBasedWebview extends BaseWebview implements Webview, WebviewFindDelegate { - private static _webviewKeyboardHandler: WebviewKeyboardHandler | undefined; + private static _webviewKeyboardHandler: WebviewIgnoreMenuShortcutsManager | undefined; private static getWebviewKeyboardHandler( configService: IConfigurationService, mainProcessService: IMainProcessService, ) { if (!this._webviewKeyboardHandler) { - this._webviewKeyboardHandler = new WebviewKeyboardHandler(configService, mainProcessService); + this._webviewKeyboardHandler = new WebviewIgnoreMenuShortcutsManager(configService, mainProcessService); } return this._webviewKeyboardHandler; } @@ -124,6 +63,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme @IConfigurationService configurationService: IConfigurationService, @IMainProcessService mainProcessService: IMainProcessService, @INotificationService noficationService: INotificationService, + @INativeHostService nativeHostService: INativeHostService, ) { super(id, options, contentOptions, extension, _webviewThemeDataProvider, noficationService, _myLogService, telemetryService, environmentService); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager.ts new file mode 100644 index 00000000000..5b5e4dd75ac --- /dev/null +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WebviewTag } from 'electron'; +import { addDisposableListener } from 'vs/base/browser/dom'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; +import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; + +export class WebviewIgnoreMenuShortcutsManager { + + private readonly _webviews = new Set(); + private readonly _isUsingNativeTitleBars: boolean; + + private readonly webviewMainService: IWebviewManagerService; + + constructor( + configurationService: IConfigurationService, + mainProcessService: IMainProcessService, + ) { + this._isUsingNativeTitleBars = configurationService.getValue('window.titleBarStyle') === 'native'; + + this.webviewMainService = createChannelSender(mainProcessService.getChannel('webview')); + } + + public add(webview: WebviewTag): IDisposable { + this._webviews.add(webview); + + const disposables = new DisposableStore(); + + if (this.shouldToggleMenuShortcutsEnablement) { + this.setIgnoreMenuShortcutsForWebview(webview, true); + } + + disposables.add(addDisposableListener(webview, 'ipc-message', (event) => { + switch (event.channel) { + case WebviewMessageChannels.didFocus: + this.setIgnoreMenuShortcuts(true); + break; + + case WebviewMessageChannels.didBlur: + this.setIgnoreMenuShortcuts(false); + return; + } + })); + + return toDisposable(() => { + disposables.dispose(); + this._webviews.delete(webview); + }); + } + + private get shouldToggleMenuShortcutsEnablement() { + return isMacintosh || this._isUsingNativeTitleBars; + } + + private setIgnoreMenuShortcuts(value: boolean) { + for (const webview of this._webviews) { + this.setIgnoreMenuShortcutsForWebview(webview, value); + } + } + + private setIgnoreMenuShortcutsForWebview(webview: WebviewTag, value: boolean) { + if (this.shouldToggleMenuShortcutsEnablement) { + this.webviewMainService.setIgnoreMenuShortcuts({ webContentsId: webview.getWebContentsId() }, value); + } + } +} diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts index 3b9c5e75a9c..da47d47f957 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts @@ -6,18 +6,23 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; +import { WindowIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; /** @@ -31,6 +36,8 @@ export class ElectronIframeWebview extends IFrameWebview { private readonly _focusDelayer = this._register(new ThrottledDelayer(10)); private _elementFocusImpl!: (options?: FocusOptions | undefined) => void; + private readonly _webviewKeyboardHandler: WindowIgnoreMenuShortcutsManager; + constructor( id: string, options: WebviewOptions, @@ -45,12 +52,25 @@ export class ElectronIframeWebview extends IFrameWebview { @IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IMainProcessService mainProcessService: IMainProcessService, @INotificationService noficationService: INotificationService, + @INativeHostService nativeHostService: INativeHostService, ) { super(id, options, contentOptions, extension, webviewThemeDataProvider, noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, _remoteAuthorityResolverService, logService); this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); + + this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, nativeHostService); + + this._register(this.on(WebviewMessageChannels.didFocus, () => { + this._webviewKeyboardHandler.didFocus(); + })); + + this._register(this.on(WebviewMessageChannels.didBlur, () => { + this._webviewKeyboardHandler.didBlur(); + })); } protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) { diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager.ts b/src/vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager.ts new file mode 100644 index 00000000000..00da271c3e8 --- /dev/null +++ b/src/vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isMacintosh } from 'vs/base/common/platform'; +import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; + +export class WindowIgnoreMenuShortcutsManager { + + private readonly _isUsingNativeTitleBars: boolean; + + private readonly webviewMainService: IWebviewManagerService; + + constructor( + configurationService: IConfigurationService, + mainProcessService: IMainProcessService, + private readonly nativeHostService: INativeHostService + ) { + this._isUsingNativeTitleBars = configurationService.getValue('window.titleBarStyle') === 'native'; + + this.webviewMainService = createChannelSender(mainProcessService.getChannel('webview')); + } + + public didFocus(): void { + this.setIgnoreMenuShortcuts(true); + } + + public didBlur(): void { + this.setIgnoreMenuShortcuts(false); + } + + private get shouldToggleMenuShortcutsEnablement() { + return isMacintosh || this._isUsingNativeTitleBars; + } + + protected setIgnoreMenuShortcuts(value: boolean) { + if (this.shouldToggleMenuShortcutsEnablement) { + this.webviewMainService.setIgnoreMenuShortcuts({ windowId: this.nativeHostService.windowId }, value); + } + } +} diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 049b3a0dd0d..df90ede801c 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -70,7 +70,7 @@ export class WebviewViewPane extends ViewPane { this.defaultTitle = this.title; this.memento = new Memento(`webviewView.${this.id}`, storageService); - this.viewState = this.memento.legacygetMemento(StorageScope.WORKSPACE); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 4d632ea03b4..2236fa47163 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -308,6 +308,12 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe const basename = paths.basename(getFilePath()); return (basename.slice(0, basename.length - paths.extname(basename).length)); + case 'fileDirnameBasename': + if (this._ignoreEditorVariables) { + return match; + } + return paths.basename(paths.dirname(getFilePath())); + case 'execPath': const ep = this._context.getExecPath(); if (ep) { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index fda885169d6..842849af346 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -18,6 +18,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { hash } from 'vs/base/common/hash'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; class DecorationRule { @@ -168,10 +169,10 @@ class DecorationStyles { class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { - private readonly _data = TernarySearchTree.forUris(); + private readonly _data = TernarySearchTree.forUris2(_uri => true); // events ignore all path casings affectsResource(uri: URI): boolean { - return this._data.get(uri) || this._data.findSuperstr(uri) !== undefined; + return this._data.get(uri) ?? this._data.findSuperstr(uri) !== undefined; } static debouncer(last: FileDecorationChangeEvent | undefined, current: URI | URI[]) { @@ -201,14 +202,18 @@ class DecorationDataRequest { class DecorationProviderWrapper { - readonly data = TernarySearchTree.forUris(); + readonly data: TernarySearchTree; private readonly _dispoable: IDisposable; constructor( readonly provider: IDecorationsProvider, + uriIdentityService: IUriIdentityService, private readonly _uriEmitter: Emitter, private readonly _flushEmitter: Emitter ) { + + this.data = TernarySearchTree.forUris2(uri => uriIdentityService.extUri.ignorePathCasing(uri)); + this._dispoable = this.provider.onDidChange(uris => { if (!uris) { // flush event -> drop all data, can affect everything @@ -326,6 +331,7 @@ export class DecorationsService implements IDecorationsService { constructor( @IThemeService themeService: IThemeService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, ) { this._decorationStyles = new DecorationStyles(themeService); } @@ -340,6 +346,7 @@ export class DecorationsService implements IDecorationsService { const wrapper = new DecorationProviderWrapper( provider, + this._uriIdentityService, this._onDidChangeDecorationsDelayed, this._onDidChangeDecorations ); diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index 9cbdbb2a632..5a4b76eb6e3 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -8,8 +8,11 @@ import { DecorationsService } from 'vs/workbench/services/decorations/browser/de import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; +import * as resources from 'vs/base/common/resources'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { mock } from 'vs/base/test/common/mock'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; suite('DecorationsService', function () { @@ -19,7 +22,12 @@ suite('DecorationsService', function () { if (service) { service.dispose(); } - service = new DecorationsService(new TestThemeService()); + service = new DecorationsService( + new TestThemeService(), + new class extends mock() { + extUri = resources.extUri; + } + ); }); test('Async provider, async/evented result', function () { diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index bdb558b4244..508de8442bf 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -164,7 +164,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (stat.isDirectory || options.forceNewWindow || preferNewWindow) { return this.hostService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow }); } else { - return this.openerService.open(uri, { fromUserGesture: true }); + return this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); } } } @@ -181,7 +181,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (options.forceNewWindow || preferNewWindow) { return this.hostService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow }); } else { - return this.openerService.open(uri, { fromUserGesture: true }); + return this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); } } } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 19876953254..ff3fe29fd60 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -924,7 +924,8 @@ export class SimpleFileDialog { const ext = resources.extname(file); for (let i = 0; i < this.options.filters.length; i++) { for (let j = 0; j < this.options.filters[i].extensions.length; j++) { - if (ext === ('.' + this.options.filters[i].extensions[j])) { + const testExt = this.options.filters[i].extensions[j]; + if ((testExt === '*') || (ext === ('.' + testExt))) { return true; } } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index a50d37b302f..0bffc4cb803 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -24,7 +24,6 @@ import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from import { coalesce, distinct, insert } from 'vs/base/common/arrays'; import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; -import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; @@ -73,7 +72,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -817,11 +815,12 @@ export class EditorService extends Disposable implements EditorServiceImpl { const leftInput = this.createEditorInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); const rightInput = this.createEditorInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile }); - return new DiffEditorInput( - resourceDiffInput.label || this.toSideBySideLabel(leftInput, rightInput), + return this.instantiationService.createInstance(DiffEditorInput, + resourceDiffInput.label, resourceDiffInput.description, leftInput, - rightInput + rightInput, + undefined ); } @@ -968,19 +967,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { return input; } - private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput): string | undefined { - - // If both editors are file inputs, we produce an optimized label - // by adding the relative path of both inputs to the label. This - // makes it easier to understand a file-based comparison. - if (this.fileEditorInputFactory.isFileEditorInput(leftInput) && this.fileEditorInputFactory.isFileEditorInput(rightInput)) { - return `${this.labelService.getUriLabel(leftInput.preferredResource, { relative: true })} ↔ ${this.labelService.getUriLabel(rightInput.preferredResource, { relative: true })}`; - } - - // Signal back that the label should be computed from within the editor - return undefined; - } - //#endregion //#region save/revert diff --git a/src/vs/workbench/services/experiment/common/experimentService.ts b/src/vs/workbench/services/experiment/common/experimentService.ts index 29ac3b835a3..7671335308c 100644 --- a/src/vs/workbench/services/experiment/common/experimentService.ts +++ b/src/vs/workbench/services/experiment/common/experimentService.ts @@ -8,7 +8,7 @@ import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilte import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryData } from 'vs/base/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -207,7 +207,7 @@ export class ExperimentService implements ITASExperimentService { ); const memento = new Memento(ExperimentService.MEMENTO_ID, this.storageService); - const keyValueStorage = new MementoKeyValueStorage(memento.legacygetMemento(StorageScope.GLOBAL)); + const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE)); const telemetry = new ExperimentServiceTelemetry(this.telemetryService); diff --git a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts index 21fb2dcf206..0d76094d021 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts @@ -7,11 +7,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IStringDictionary } from 'vs/base/common/collections'; import { Event } from 'vs/base/common/event'; -export interface IExtensionsConfigContent { - recommendations: string[]; - unwantedRecommendations: string[]; -} - export type DynamicRecommendation = 'dynamic'; export type ConfigRecommendation = 'config'; export type ExecutableRecommendation = 'executable'; diff --git a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts index dfcdaf50147..dc16b357514 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts @@ -3,20 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; +import { distinct, flatten } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IFileService } from 'vs/platform/files/common/files'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { FileKind, IFileService } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { localize } from 'vs/nls'; +import { URI } from 'vs/base/common/uri'; +import { IJSONEditingService, IJSONValue } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { ResourceMap } from 'vs/base/common/map'; export const EXTENSIONS_CONFIG = '.vscode/extensions.json'; export interface IExtensionsConfigContent { - recommendations: string[]; - unwantedRecommendations: string[]; + recommendations?: string[]; + unwantedRecommendations?: string[]; } export const IWorkpsaceExtensionsConfigService = createDecorator('IWorkpsaceExtensionsConfigService'); @@ -26,8 +34,11 @@ export interface IWorkpsaceExtensionsConfigService { onDidChangeExtensionsConfigs: Event; getExtensionsConfigs(): Promise; + getRecommendations(): Promise; getUnwantedRecommendations(): Promise; + toggleRecommendation(extensionId: string): Promise; + toggleUnwantedRecommendation(extensionId: string): Promise; } export class WorkspaceExtensionsConfigService extends Disposable implements IWorkpsaceExtensionsConfigService { @@ -40,53 +51,217 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IFileService private readonly fileService: IFileService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService, + @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, ) { super(); - this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire())); + this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire())); + this._register(fileService.onDidFilesChange(e => { + const workspace = workspaceContextService.getWorkspace(); + if ((workspace.configuration && e.affects(workspace.configuration)) + || workspace.folders.some(folder => e.affects(folder.toResource(EXTENSIONS_CONFIG))) + ) { + this._onDidChangeExtensionsConfigs.fire(); + } + })); } async getExtensionsConfigs(): Promise { const workspace = this.workspaceContextService.getWorkspace(); - const result = await Promise.all([ - this.resolveWorkspaceExtensionConfig(workspace), - ...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder)) - ]); - return coalesce(result); + const result: IExtensionsConfigContent[] = []; + const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined; + if (workspaceExtensionsConfigContent) { + result.push(workspaceExtensionsConfigContent); + } + result.push(...await Promise.all(workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder)))); + return result; + } + + async getRecommendations(): Promise { + const configs = await this.getExtensionsConfigs(); + return distinct(flatten(configs.map(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : []))); } async getUnwantedRecommendations(): Promise { const configs = await this.getExtensionsConfigs(); - return distinct(flatten(configs.map(c => c.unwantedRecommendations.map(c => c.toLowerCase())))); + return distinct(flatten(configs.map(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : []))); } - private async resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise { - try { - if (workspace.configuration) { - const content = await this.fileService.readFile(workspace.configuration); - const extensionsConfigContent = parse(content.value.toString())['extensions']; - return this.parseExtensionConfig(extensionsConfigContent); + async toggleRecommendation(extensionId: string): Promise { + const workspace = this.workspaceContextService.getWorkspace(); + const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined; + const workspaceFolderExtensionsConfigContents = new ResourceMap(); + await Promise.all(workspace.folders.map(async workspaceFolder => { + const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder); + workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent); + })); + + const isWorkspaceRecommended = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.recommendations?.some(r => r === extensionId); + const recommendedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.recommendations?.some(r => r === extensionId)); + const isRecommended = isWorkspaceRecommended || recommendedWorksapceFolders.length > 0; + + const workspaceOrFolders = isRecommended + ? await this.pickWorkspaceOrFolders(recommendedWorksapceFolders, isWorkspaceRecommended ? workspace : undefined, localize('select for remove', "Remove extension recommendation from")) + : await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to")); + + for (const workspaceOrWorkspaceFolder of workspaceOrFolders) { + if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) { + await this.addOrRemoveWorkspaceRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isRecommended); + } else { + await this.addOrRemoveWorkspaceFolderRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isRecommended); } - } catch (e) { /* Ignore */ } - return null; + } } - private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise { + async toggleUnwantedRecommendation(extensionId: string): Promise { + const workspace = this.workspaceContextService.getWorkspace(); + const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined; + const workspaceFolderExtensionsConfigContents = new ResourceMap(); + await Promise.all(workspace.folders.map(async workspaceFolder => { + const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder); + workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent); + })); + + const isWorkspaceUnwanted = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.unwantedRecommendations?.some(r => r === extensionId); + const unWantedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.unwantedRecommendations?.some(r => r === extensionId)); + const isUnwanted = isWorkspaceUnwanted || unWantedWorksapceFolders.length > 0; + + const workspaceOrFolders = isUnwanted + ? await this.pickWorkspaceOrFolders(unWantedWorksapceFolders, isWorkspaceUnwanted ? workspace : undefined, localize('select for remove', "Remove extension recommendation from")) + : await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to")); + + for (const workspaceOrWorkspaceFolder of workspaceOrFolders) { + if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) { + await this.addOrRemoveWorkspaceUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isUnwanted); + } else { + await this.addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isUnwanted); + } + } + } + + private async addOrRemoveWorkspaceFolderRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise { + const values: IJSONValue[] = []; + if (add) { + values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] }); + if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) { + values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) }); + } + } else if (extensionsConfigContent.recommendations) { + values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) }); + } + + if (values.length) { + return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true); + } + } + + private async addOrRemoveWorkspaceRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise { + const values: IJSONValue[] = []; + if (extensionsConfigContent) { + if (add) { + values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] }); + if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) { + values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) }); + } + } else if (extensionsConfigContent.recommendations) { + values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) }); + } + } else if (add) { + values.push({ path: ['extensions'], value: { recommendations: [extensionId] } }); + } + + if (values.length) { + return this.jsonEditingService.write(workspace.configuration!, values, true); + } + } + + private async addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise { + const values: IJSONValue[] = []; + if (add) { + values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] }); + if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) { + values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) }); + } + } else if (extensionsConfigContent.unwantedRecommendations) { + values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) }); + } + if (values.length) { + return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true); + } + } + + private async addOrRemoveWorkspaceUnwantedRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise { + const values: IJSONValue[] = []; + if (extensionsConfigContent) { + if (add) { + values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] }); + if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) { + values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) }); + } + } else if (extensionsConfigContent.unwantedRecommendations) { + values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) }); + } + } else if (add) { + values.push({ path: ['extensions'], value: { unwantedRecommendations: [extensionId] } }); + } + + if (values.length) { + return this.jsonEditingService.write(workspace.configuration!, values, true); + } + } + + private async pickWorkspaceOrFolders(workspaceFolders: IWorkspaceFolder[], workspace: IWorkspace | undefined, placeHolder: string): Promise<(IWorkspace | IWorkspaceFolder)[]> { + const workspaceOrFolders = workspace ? [...workspaceFolders, workspace] : [...workspaceFolders]; + if (workspaceOrFolders.length === 1) { + return workspaceOrFolders; + } + + const folderPicks: (IQuickPickItem & { workspaceOrFolder: IWorkspace | IWorkspaceFolder } | IQuickPickSeparator)[] = workspaceFolders.map(workspaceFolder => { + return { + label: workspaceFolder.name, + description: localize('workspace folder', "Workspace Folder"), + workspaceOrFolder: workspaceFolder, + iconClasses: getIconClasses(this.modelService, this.modeService, workspaceFolder.uri, FileKind.ROOT_FOLDER) + }; + }); + + if (workspace) { + folderPicks.push({ type: 'separator' }); + folderPicks.push({ + label: localize('workspace', "Workspace"), + workspaceOrFolder: workspace, + }); + } + + const result = await this.quickInputService.pick(folderPicks, { placeHolder, canPickMany: true }) || []; + return result.map(r => r.workspaceOrFolder!); + } + + private async resolveWorkspaceExtensionConfig(workspaceConfigurationResource: URI): Promise { + try { + const content = await this.fileService.readFile(workspaceConfigurationResource); + const extensionsConfigContent = parse(content.value.toString())['extensions']; + return extensionsConfigContent ? this.parseExtensionConfig(extensionsConfigContent) : undefined; + } catch (e) { /* Ignore */ } + return undefined; + } + + private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise { try { const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG)); const extensionsConfigContent = parse(content.value.toString()); return this.parseExtensionConfig(extensionsConfigContent); } catch (e) { /* ignore */ } - return null; + return {}; } - private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent | undefined): IExtensionsConfigContent | null { - if (extensionsConfigContent) { - return { - recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())), - unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase())) - }; - } - return null; + private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent { + return { + recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())), + unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase())) + }; } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts index 342ec2dc402..b8171e2f3b4 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts @@ -30,7 +30,7 @@ export class ExtensionHostProfiler { } private distill(profile: Profile, extensions: IExtensionDescription[]): IExtensionHostProfile { - let searchTree = TernarySearchTree.forUris(); + let searchTree = TernarySearchTree.forUris2(); for (let extension of extensions) { if (extension.extensionLocation.scheme === Schemas.file) { searchTree.set(URI.file(realpathSync(extension.extensionLocation.fsPath)), extension); diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 87fee781fb1..3050bc6e2d6 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -468,9 +468,12 @@ let _caCertificates: ReturnType | Promise; async function getCaCertificates(extHostLogService: ILogService) { if (!_caCertificates) { _caCertificates = readCaCertificates() - .then(res => res && res.certs.length ? res : undefined) + .then(res => { + extHostLogService.debug('ProxyResolver#getCaCertificates count', res && res.certs.length); + return res && res.certs.length ? res : undefined; + }) .catch(err => { - extHostLogService.error('ProxyResolver#getCertificates', toErrorMessage(err)); + extHostLogService.error('ProxyResolver#getCaCertificates error', toErrorMessage(err)); return undefined; }); } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 24d95740cb3..82f8a198074 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -578,7 +578,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic 'editor.insertSpaces', 'editor.wordWrap', 'files.exclude', - 'files.associations' + 'files.associations', + 'workbench.editor.enablePreview' ]; } diff --git a/src/vs/workbench/services/preferences/common/preferencesValidation.ts b/src/vs/workbench/services/preferences/common/preferencesValidation.ts index 40b916d16ae..80d97ac9190 100644 --- a/src/vs/workbench/services/preferences/common/preferencesValidation.ts +++ b/src/vs/workbench/services/preferences/common/preferencesValidation.ts @@ -92,10 +92,12 @@ function valueValidatesAsType(value: any, type: string): boolean { } function getStringValidators(prop: IConfigurationPropertySchema) { + const uriRegex = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; let patternRegex: RegExp | undefined; if (typeof prop.pattern === 'string') { patternRegex = new RegExp(prop.pattern); } + return [ { enabled: prop.maxLength !== undefined, @@ -116,7 +118,25 @@ function getStringValidators(prop: IConfigurationPropertySchema) { enabled: prop.format === 'color-hex', isValid: ((value: string) => Color.Format.CSS.parseHex(value)), message: nls.localize('validations.colorFormat', "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.") - } + }, + { + enabled: prop.format === 'uri' || prop.format === 'uri-reference', + isValid: ((value: string) => !!value.length), + message: nls.localize('validations.uriEmpty', "URI expected.") + }, + { + enabled: prop.format === 'uri' || prop.format === 'uri-reference', + isValid: ((value: string) => uriRegex.test(value)), + message: nls.localize('validations.uriMissing', "URI is expected.") + }, + { + enabled: prop.format === 'uri', + isValid: ((value: string) => { + const matches = value.match(uriRegex); + return !!(matches && matches[2]); + }), + message: nls.localize('validations.uriSchemeMissing', "URI with a scheme is expected.") + }, ].filter(validation => validation.enabled); } @@ -249,5 +269,3 @@ function getArrayOfStringValidator(prop: IConfigurationPropertySchema): ((value: return null; } - - diff --git a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts index 9fcbd932494..7a8e0c4872a 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts @@ -373,4 +373,20 @@ suite('Preferences Validation', () => { testInvalidTypeError([null], 'null', false); testInvalidTypeError('null', 'null', false); }); + + test('uri checks work', () => { + const tester = new Tester({ type: 'string', format: 'uri' }); + tester.rejects('example.com'); + tester.rejects('example.com/example'); + tester.rejects('example/example.html'); + tester.rejects('www.example.com'); + tester.rejects(''); + tester.rejects(' '); + tester.rejects('example'); + + tester.accepts('https:'); + tester.accepts('https://'); + tester.accepts('https://example.com'); + tester.accepts('https://www.example.com'); + }); }); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 2ef157dadcc..4ecca900e14 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -34,7 +34,7 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; -import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, UTF8_BOM, detectEncodingByBOMFromBuffer, toEncodeReadable, toDecodeStream, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding'; +import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, toEncodeReadable, toDecodeStream, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding'; import { consumeStream } from 'vs/base/common/stream'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -532,7 +532,6 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { @ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService, @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IFileService private fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(); @@ -569,26 +568,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> { const { encoding, hasBOM } = await this.getPreferredWriteEncoding(resource, options ? options.encoding : undefined); - // Some encodings come with a BOM automatically - if (hasBOM) { - return { encoding, addBOM: true }; - } - - // Ensure that we preserve an existing BOM if found for UTF8 - // unless we are instructed to overwrite the encoding - const overwriteEncoding = options?.overwriteEncoding; - if (!overwriteEncoding && encoding === UTF8) { - try { - const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value; - if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8_with_bom) { - return { encoding, addBOM: true }; - } - } catch (error) { - // ignore - file might not exist - } - } - - return { encoding, addBOM: false }; + return { encoding, addBOM: hasBOM }; } async getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index ffcc91f57fa..40827e31f2d 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -810,7 +810,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil try { const stat = await this.textFileService.write(lastResolvedFileStat.resource, textFileEditorModel.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, - overwriteEncoding: options.overwriteEncoding, mtime: lastResolvedFileStat.mtime, encoding: this.getEncoding(), etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, textFileEditorModel.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag, @@ -978,7 +977,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (!this.inConflictMode) { - this.save({ overwriteEncoding: true }); + this.save(); } } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 692976da0af..73618d52304 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -124,11 +124,6 @@ export interface IWriteTextFileOptions extends IWriteFileOptions { */ encoding?: string; - /** - * If set to true, will enforce the selected encoding and not perform any detection using BOMs. - */ - overwriteEncoding?: boolean; - /** * Whether to overwrite a file even if it is readonly. */ @@ -370,11 +365,6 @@ export interface ITextFileSaveOptions extends ISaveOptions { */ overwriteReadonly?: boolean; - /** - * Overwrite the encoding of the file on disk as configured. - */ - overwriteEncoding?: boolean; - /** * Save the file with elevated privileges. * diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index 9aeeb40b349..71f8242e67b 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -310,13 +310,13 @@ export default function createSuite(params: Params) { detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); - // ensure BOM preserved - await service.write(resource, content, { encoding: UTF8 }); + // ensure BOM preserved if enforced + await service.write(resource, content, { encoding: UTF8_with_bom }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); // allow to remove BOM - await service.write(resource, content, { encoding: UTF8, overwriteEncoding: true }); + await service.write(resource, content, { encoding: UTF8 }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, null); @@ -338,13 +338,13 @@ export default function createSuite(params: Params) { detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); - // ensure BOM preserved - await service.write(resource, model.createSnapshot(), { encoding: UTF8 }); + // ensure BOM preserved if enforced + await service.write(resource, model.createSnapshot(), { encoding: UTF8_with_bom }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); // allow to remove BOM - await service.write(resource, model.createSnapshot(), { encoding: UTF8, overwriteEncoding: true }); + await service.write(resource, model.createSnapshot(), { encoding: UTF8 }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, null); @@ -360,7 +360,7 @@ export default function createSuite(params: Params) { let detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); - await service.write(resource, 'Hello World'); + await service.write(resource, 'Hello World', { encoding: detectedEncoding! }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); }); diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index c23d2a8a8f7..b6af6ccb3f3 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -377,5 +377,6 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i return result; } function escapeCSS(str: string) { + str = str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. return window.CSS.escape(str); } 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 aad14e56a3c..8ff03965ebd 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -75,7 +75,7 @@ suite('Workbench editor', () => { assert.equal(EditorResourceAccessor.getOriginalUri(file, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); assert.ok(!EditorResourceAccessor.getOriginalUri(file, { filterByScheme: Schemas.untitled })); - const diffEditorInput = new DiffEditorInput('name', 'description', untitled, file); + const diffEditorInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', untitled, file, undefined); assert.ok(!EditorResourceAccessor.getCanonicalUri(diffEditorInput)); assert.ok(!EditorResourceAccessor.getCanonicalUri(diffEditorInput, { filterByScheme: Schemas.file })); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index 6333219069c..cf541542d35 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -37,7 +37,7 @@ suite('Workbench editor model', () => { let input = instantiationService.createInstance(ResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined); let otherInput = instantiationService.createInstance(ResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined); - let diffInput = new DiffEditorInput('name', 'description', input, otherInput); + let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); let model = await diffInput.resolve() as TextDiffEditorModel; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 2059826812e..4d2fbf4ec6e 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { EditorGroup, ISerializedEditorGroup, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroup'; import { Extensions as EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputFactory, CloseDirection, EditorsOrder } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -269,12 +269,13 @@ suite('Workbench editor groups', () => { test('contains()', function () { const group = createGroup(); + const instantiationService = workbenchInstantiationService(); const input1 = input(); const input2 = input(); - const diffInput1 = new DiffEditorInput('name', 'description', input1, input2); - const diffInput2 = new DiffEditorInput('name', 'description', input2, input1); + const diffInput1 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input1, input2, undefined); + const diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input2, input1, undefined); group.openEditor(input1, { pinned: true, active: true }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 8e00169fc0c..21c86ec8fda 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { EditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; class MyEditorInput extends EditorInput { readonly resource = undefined; @@ -36,6 +37,8 @@ suite('Workbench editor input', () => { }); test('DiffEditorInput', () => { + const instantiationService = workbenchInstantiationService(); + let counter = 0; let input = new MyEditorInput(); input.onDispose(() => { @@ -49,7 +52,7 @@ suite('Workbench editor input', () => { counter++; }); - let diffInput = new DiffEditorInput('name', 'description', input, otherInput); + let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); assert.equal(diffInput.originalInput, input); assert.equal(diffInput.modifiedInput, otherInput); @@ -62,11 +65,13 @@ suite('Workbench editor input', () => { }); test('DiffEditorInput disposes when input inside disposes', function () { + const instantiationService = workbenchInstantiationService(); + let counter = 0; let input = new MyEditorInput(); let otherInput = new MyEditorInput(); - let diffInput = new DiffEditorInput('name', 'description', input, otherInput); + let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); diffInput.onDispose(() => { counter++; assert(true); @@ -77,7 +82,7 @@ suite('Workbench editor input', () => { input = new MyEditorInput(); otherInput = new MyEditorInput(); - let diffInput2 = new DiffEditorInput('name', 'description', input, otherInput); + let diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); diffInput2.onDispose(() => { counter++; assert(true);