diff --git a/.github/classifier.json b/.github/classifier.json index e483a011577..e4acc63c170 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -20,8 +20,8 @@ "context-keys": {"assign": []}, "css-less-scss": {"assign": ["aeschli"]}, "custom-editors": {"assign": ["mjbvz"]}, - "debug": {"assign": ["connor4312 "]}, - "debug-console": {"assign": ["connor4312 "]}, + "debug": {"assign": ["weinand"]}, + "debug-console": {"assign": ["weinand"]}, "dialogs": {"assign": ["sbatten"]}, "diff-editor": {"assign": []}, "dropdown": {"assign": []}, diff --git a/.vscode/launch.json b/.vscode/launch.json index 9a5c41b3d8f..6153d7a995c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -269,8 +269,10 @@ } }, { - "type": "chrome", + "type": "pwa-chrome", "request": "launch", + "outFiles": [], + "perScriptSourcemaps": "yes", "name": "VS Code (Web, Chrome)", "url": "http://localhost:8080", "preLaunchTask": "Run web", @@ -282,6 +284,8 @@ { "type": "pwa-msedge", "request": "launch", + "outFiles": [], + "perScriptSourcemaps": "yes", "name": "VS Code (Web, Edge)", "url": "http://localhost:8080", "pauseForSourceMap": false, diff --git a/.vscode/settings.json b/.vscode/settings.json index ec4b9ec2be4..4b2a9059553 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,6 +67,9 @@ }, "gulp.autoDetect": "off", "files.insertFinalNewline": true, + "[plaintext]": { + "files.insertFinalNewline": false, + }, "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index e99c3baa71f..02dec40d741 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -27,7 +27,10 @@ function main() { account: process.env.AZURE_STORAGE_ACCOUNT, key: process.env.AZURE_STORAGE_ACCESS_KEY, container: process.env.VSCODE_QUALITY, - prefix: commit + '/' + prefix: commit + '/', + contentSettings: { + cacheControl: 'max-age=31536000, public' + } })); } diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 0d34d1cea63..47f0155d8a5 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -210,6 +210,10 @@ "name": "vs/workbench/contrib/webviewPanel", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/workspaces", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/customEditor", "project": "vscode-workbench" diff --git a/build/package.json b/build/package.json index 7561ebc958c..9d86dbaf79e 100644 --- a/build/package.json +++ b/build/package.json @@ -45,7 +45,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.1.0-dev.20200924", + "typescript": "^4.1.0-dev.20201018", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index 3cc284f20dc..09186eb00b0 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2535,10 +2535,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.1.0-dev.20200924: - version "4.1.0-dev.20200924" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200924.tgz#d8b2aaa6f94ec22725eafcadf0b9a17aae9c32b9" - integrity sha512-AXwqVrp2AeVZ3jaZ/gcvxb0nnvqEbDFuFFjvV5/9wfcyz7KZx5KvyJENUgGoJHywCvl1PHKasQKYjzjk1QixnQ== +typescript@^4.1.0-dev.20201018: + version "4.1.0-dev.20201018" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20201018.tgz#1a4b8e3f9b640218a44299773371354d75bcfa34" + integrity sha512-cOFYP1I+IrMWa6ZfefxcacZha1pQMxrq8DGMBLkvrl8k3CqIdD8APq9LXaMj/PWrB8IPgDprY6jHwqiHg0/oGA== typical@^4.0.0: version "4.0.0" diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 2b7952446f0..b408c46f42e 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -21,7 +21,7 @@ }, "settings": { "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container." + "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time." }, "remoteEnv": { "type": "object", diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index b37e07fa65c..4719d53e6db 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -23,7 +23,7 @@ }, "settings": { "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container." + "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, "forwardPorts": { "type": "array", @@ -298,7 +298,7 @@ }, "workspaceFolder": { "type": "string", - "description": "The path of the workspace folder inside the container." + "description": "The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml." }, "shutdownAction": { "type": "string", diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 6a2734cb9fa..16dc7b61d6d 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -66,7 +66,7 @@ function doWrapping(individualLines: boolean, args: any) { const helper = getEmmetHelper(); // Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents - let rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { + const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) { const previousLine = rangeToReplace.end.line - 1; @@ -88,7 +88,7 @@ function doWrapping(individualLines: boolean, args: any) { rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character); let textToWrapInPreview: string[]; - let textToReplace = editor.document.getText(rangeToReplace); + const textToReplace = editor.document.getText(rangeToReplace); if (individualLines) { textToWrapInPreview = textToReplace.split('\n').map(x => x.trim()); } else { @@ -144,7 +144,7 @@ function doWrapping(individualLines: boolean, args: any) { const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1; const newLinesInserted = expandedTextLines.length - oldPreviewLines; - let newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted; + const newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted; let newPreviewStart = oldPreviewRange.start.character; const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted; let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length; @@ -177,19 +177,19 @@ function doWrapping(individualLines: boolean, args: any) { return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview); } - let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation); + const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation); if (!extractedResults) { return Promise.resolve(inPreview); } else if (extractedResults.abbreviation !== inputAbbreviation) { // Not clear what should we do in this case. Warn the user? How? } - let { abbreviation, filter } = extractedResults; + const { abbreviation, filter } = extractedResults; if (definitive) { const revertPromise = inPreview ? revertPreview() : Promise.resolve(); return revertPromise.then(() => { const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => { - let rangeToReplace = rangesAndContent.originalRange; + const rangeToReplace = rangesAndContent.originalRange; let textToWrap: string[]; if (individualLines) { textToWrap = rangesAndContent.textToWrapInPreview; @@ -270,17 +270,17 @@ export function expandEmmetAbbreviation(args: any): Thenable { + const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => { position = document.validatePosition(position); let rangeToReplace: vscode.Range = selection; let abbr = document.getText(rangeToReplace); if (!rangeToReplace.isEmpty) { - let extractedResults = helper.extractAbbreviationFromText(abbr); + const extractedResults = helper.extractAbbreviationFromText(abbr); if (extractedResults) { return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter]; } @@ -293,23 +293,23 @@ export function expandEmmetAbbreviation(args: any): Thenable explicitly // else we will end up with <
if (syntax === 'html') { - let matches = textTillPosition.match(/<(\w+)$/); + const matches = textTillPosition.match(/<(\w+)$/); if (matches) { abbr = matches[1]; rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position); return [rangeToReplace, abbr, '']; } } - let extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, false); + const extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, { lookAhead: false }); if (!extractedResults) { return [null, '', '']; } - let { abbreviationRange, abbreviation, filter } = extractedResults; + const { abbreviationRange, abbreviation, filter } = extractedResults; return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter]; }; - let selectionsInReverseOrder = editor.selections.slice(0); + const selectionsInReverseOrder = editor.selections.slice(0); selectionsInReverseOrder.sort((a, b) => { const posA = a.isReversed ? a.anchor : a.active; const posB = b.isReversed ? b.anchor : b.active; @@ -322,7 +322,7 @@ export function expandEmmetAbbreviation(args: any): Thenable 1000) { rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active); } else { @@ -333,8 +333,8 @@ export function expandEmmetAbbreviation(args: any): Thenable { - let position = selection.isReversed ? selection.anchor : selection.active; - let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax); + const position = selection.isReversed ? selection.anchor : selection.active; + const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax); if (!rangeToReplace) { return; } @@ -578,7 +578,7 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex // Snippet to replace at multiple cursors are not the same // `editor.insertSnippet` will have to be called for each instance separately // We will not be able to maintain multiple cursors after snippet insertion - let insertPromises: Thenable[] = []; + const insertPromises: Thenable[] = []; if (!insertSameSnippet) { expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => { let expandedText = expandAbbr(expandAbbrInput); @@ -596,8 +596,8 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex // We can pass all ranges to `editor.insertSnippet` in a single call so that // all cursors are maintained after snippet insertion const anyExpandAbbrInput = expandAbbrList[0]; - let expandedText = expandAbbr(anyExpandAbbrInput); - let allRanges = expandAbbrList.map(value => { + const expandedText = expandAbbr(anyExpandAbbrInput); + const allRanges = expandAbbrList.map(value => { return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character); }); if (expandedText) { @@ -614,7 +614,7 @@ function walk(root: any, fn: ((node: any) => boolean)): boolean { let ctx = root; while (ctx) { - let next = ctx.next; + const next = ctx.next; if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) { return false; } @@ -653,7 +653,7 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { // Expand the abbreviation if (input.textToWrap) { - let parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); + const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) { // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text). diff --git a/extensions/emmet/src/defaultCompletionProvider.ts b/extensions/emmet/src/defaultCompletionProvider.ts index 705c347d6dc..9ac469af615 100644 --- a/extensions/emmet/src/defaultCompletionProvider.ts +++ b/extensions/emmet/src/defaultCompletionProvider.ts @@ -137,7 +137,10 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi } } - const extractAbbreviationResults = helper.extractAbbreviation(lsDoc, position, !isStyleSheet(syntax)); + const expandOptions = isStyleSheet(syntax) ? + { lookAhead: false, syntax: 'stylesheet' } : + { lookAhead: true, syntax: 'markup' }; + const extractAbbreviationResults = helper.extractAbbreviation(lsDoc, position, expandOptions); if (!extractAbbreviationResults || !helper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)) { return; } diff --git a/extensions/emmet/src/test/abbreviationAction.test.ts b/extensions/emmet/src/test/abbreviationAction.test.ts index cedf3244903..7577cd6a718 100644 --- a/extensions/emmet/src/test/abbreviationAction.test.ts +++ b/extensions/emmet/src/test/abbreviationAction.test.ts @@ -61,7 +61,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor('img', 'html', async (editor, _doc) => { editor.selection = new Selection(0, 3, 0, 3); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), '\"\"'); + assert.strictEqual(editor.document.getText(), '\"\"'); return Promise.resolve(); }); }); @@ -72,14 +72,14 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(!completionPromise, false, `Got unexpected undefined instead of a completion promise`); + assert.strictEqual(!completionPromise, false, `Got unexpected undefined instead of a completion promise`); return Promise.resolve(); } const completionList = await completionPromise; - assert.equal(completionList && completionList.items && completionList.items.length > 0, true); + assert.strictEqual(completionList && completionList.items && completionList.items.length > 0, true); if (completionList) { - assert.equal(completionList.items[0].label, 'img'); - assert.equal(((completionList.items[0].documentation) || '').replace(/\|/g, ''), '\"\"'); + assert.strictEqual(completionList.items[0].label, 'img'); + assert.strictEqual(((completionList.items[0].documentation) || '').replace(/\|/g, ''), '\"\"'); } return Promise.resolve(); }); @@ -161,7 +161,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(2, 4, 2, 4); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -171,7 +171,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(2, 4, 2, 4); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -180,7 +180,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(9, 8, 9, 8); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -190,7 +190,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(9, 8, 9, 8); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -200,7 +200,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(fileContents, 'html', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), fileContents); + assert.strictEqual(editor.document.getText(), fileContents); return Promise.resolve(); }); }); @@ -211,7 +211,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(0, 6, 0, 6); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -219,12 +219,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { test('Expand css when inside style tag (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(13, 16, 13, 19); - let expandPromise = expandEmmetAbbreviation({ language: 'css' }); + const expandPromise = expandEmmetAbbreviation({ language: 'css' }); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('m10', 'margin: 10px;')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('m10', 'margin: 10px;')); return Promise.resolve(); }); }); @@ -238,19 +238,19 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); - assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -259,7 +259,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(13, 14, 13, 14); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -268,12 +268,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const styleAttributeContent = '
'; return withRandomFileEditor(styleAttributeContent, 'html', async (editor, _doc) => { editor.selection = new Selection(0, 15, 0, 15); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;')); + assert.strictEqual(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;')); return Promise.resolve(); }); }); @@ -287,19 +287,19 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); - assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -307,12 +307,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { test('Expand html when inside script tag with html type (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(21, 12, 21, 12); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('span.hello', '')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('span.hello', '')); return Promise.resolve(); }); }); @@ -326,18 +326,18 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding span.hello`); + assert.strictEqual(1, 2, `Problem with expanding span.hello`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding span.hello`); + assert.strictEqual(1, 2, `Problem with expanding span.hello`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -346,7 +346,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(24, 12, 24, 12); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -356,7 +356,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(24, 12, 24, 12); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -365,12 +365,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': 'html' }, ConfigurationTarget.Global); await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(24, 10, 24, 10); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('span.bye', '')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('span.bye', '')); }); return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global); }); @@ -384,17 +384,17 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding span.bye`); + assert.strictEqual(1, 2, `Problem with expanding span.bye`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding span.bye`); + assert.strictEqual(1, 2, `Problem with expanding span.bye`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global); @@ -433,7 +433,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('ul.nav', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), '
    '); + assert.strictEqual(editor.document.getText(), '
      '); return Promise.resolve(); }); }); @@ -442,7 +442,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -452,7 +452,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), '\'\'/'); + assert.strictEqual(editor.document.getText(), '\'\'/'); return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global); }); }); @@ -461,7 +461,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'xml', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'xml' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -470,7 +470,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'html', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'html' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -479,7 +479,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('if (foo < 10) { span.bar', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 27, 0, 27); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), 'if (foo < 10) { '); + assert.strictEqual(editor.document.getText(), 'if (foo < 10) { '); return Promise.resolve(); }); }); @@ -505,15 +505,15 @@ suite('Tests for jsx, xml and xsl', () => { function testExpandAbbreviation(syntax: string, selection: Selection, abbreviation: string, expandedText: string, shouldFail?: boolean): Thenable { return withRandomFileEditor(htmlContents, syntax, async (editor, _doc) => { editor.selection = selection; - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace(abbreviation, expandedText)); + assert.strictEqual(editor.document.getText(), htmlContents.replace(abbreviation, expandedText)); return Promise.resolve(); }); } @@ -525,7 +525,7 @@ function testHtmlCompletionProvider(selection: Selection, abbreviation: string, const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } @@ -533,13 +533,13 @@ function testHtmlCompletionProvider(selection: Selection, abbreviation: string, const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); } @@ -549,7 +549,7 @@ function testNoCompletion(syntax: string, fileContents: string, selection: Selec editor.selection = selection; const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); } diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index f2ab2e5275f..b2df08d6929 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -2,6 +2,20 @@ # yarn lockfile v1 +"@emmetio/abbreviation@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.0.2.tgz#e26d55d78c00cdeb2ef983e902c7ad55ed0b648d" + integrity sha512-kpWg6jyR1YEj/yWceruvDj/fe1BhXqA0tGH3Z2ZiPFo8SDMH4JHg6FChqon5x0CCfLf4zVswrQa0gcZ4XtdRBQ== + dependencies: + "@emmetio/scanner" "^1.0.0" + +"@emmetio/css-abbreviation@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@emmetio/css-abbreviation/-/css-abbreviation-2.1.2.tgz#4a5d96f2576dd827a2c1a060374ffa8a5408cc1c" + integrity sha512-CvYTzJltVpLqJaCZ1Qn97LVAKsl2Uwl2fzir1EX/WuMY3xWxgc3BWRCheL6k65km6GyDrLVl6RhrrNb/pxOiAQ== + dependencies: + "@emmetio/scanner" "^1.0.0" + "@emmetio/css-parser@ramya-rao-a/css-parser#vscode": version "0.4.0" resolved "https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660" @@ -9,11 +23,6 @@ "@emmetio/stream-reader" "^2.2.0" "@emmetio/stream-reader-utils" "^0.1.0" -"@emmetio/extract-abbreviation@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@emmetio/extract-abbreviation/-/extract-abbreviation-0.2.0.tgz#0afc2b40c060549b98ea7b18f426e8317df5829e" - integrity sha512-eWIRoybKwQ0LkZw7aSULPFS+r2kp0+HdJlnw0HaE6g3AKbMNL4Ogwm2OTA9gNWZ5zdp6daOAOHFqjDqqhE5y/g== - "@emmetio/html-matcher@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@emmetio/html-matcher/-/html-matcher-0.3.3.tgz#0bbdadc0882e185950f03737dc6dbf8f7bd90728" @@ -30,6 +39,11 @@ "@emmetio/stream-reader" "^2.0.1" "@emmetio/stream-reader-utils" "^0.1.0" +"@emmetio/scanner@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f" + integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA== + "@emmetio/stream-reader-utils@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz#244cb02c77ec2e74f78a9bd318218abc9c500a61" @@ -437,6 +451,14 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +emmet@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.1.5.tgz#160c454d827e29db543a447b7673488f11485c8d" + integrity sha512-u0RR8qb067EELZ8t+LtxbhLXvfJ4nklbxcoFrHcvs61r7rk8SgJwgcVSM/Xa/4/tlq2jKdunGbVp5Nqz8MZYOg== + dependencies: + "@emmetio/abbreviation" "^2.0.2" + "@emmetio/css-abbreviation" "^2.1.2" + end-of-stream@^1.0.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2383,12 +2405,13 @@ vinyl@~2.0.1: replace-ext "^1.0.0" vscode-emmet-helper@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.5.tgz#72058cdb62b6d86e77c8f42de6c05491a700a88f" - integrity sha512-lDP+soFnJgEkUrdAWqdUYRFfXRFnmXhjzyzca+fy9vCUorr3lp32IKIys8mYwnlAUencmyXmF5JwN0VikUXj/Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.6.tgz#c3d0eb249da922a87229603fd4fc09dfdfeea830" + integrity sha512-cWF1xSBJ38OJ6q3i961g4Gjs7pwoxf8kTIXfpx7qgnmkclYjxsLZqh4K1HWrzTELJknoZm5PJoLBCkJ3m+7Pzw== dependencies: - "@emmetio/extract-abbreviation" "^0.2.0" + emmet "^2.1.5" jsonc-parser "^2.3.0" + vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" vscode-uri "^2.1.2" diff --git a/extensions/git/package.json b/extensions/git/package.json index 7540ba94050..460c3a7c1d0 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -38,6 +38,11 @@ "title": "%command.clone%", "category": "Git" }, + { + "command": "git.cloneRecursive", + "title": "%command.cloneRecursive%", + "category": "Git" + }, { "command": "git.init", "title": "%command.init%", @@ -500,6 +505,10 @@ "command": "git.clone", "when": "config.git.enabled && !git.missing" }, + { + "command": "git.cloneRecursive", + "when": "config.git.enabled && !git.missing" + }, { "command": "git.init", "when": "config.git.enabled && !git.missing" @@ -628,6 +637,10 @@ "command": "git.commitAllAmend", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.rebaseAbort", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && gitRebaseInProgress" + }, { "command": "git.commitNoVerify", "when": "config.git.enabled && !git.missing && config.git.allowNoVerifyCommit && gitOpenRepositoryCount != 0" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index e7a8fbe0d5c..dfcdc3c8f15 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -3,6 +3,7 @@ "description": "Git SCM Integration", "command.setLogLevel": "Set Log Level...", "command.clone": "Clone", + "command.cloneRecursive": "Clone (Recursive)", "command.init": "Initialize Repository", "command.openRepository": "Open Repository", "command.close": "Close Repository", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index c03f292a441..0e6ed0feba2 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -472,8 +472,7 @@ export class CommandCenter { } } - @command('git.clone') - async clone(url?: string, parentPath?: string): Promise { + async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise { if (!url || typeof url !== 'string') { url = await pickRemoteSource(this.model, { providerLabel: provider => localize('clonefrom', "Clone from {0}", provider.name), @@ -529,7 +528,7 @@ export class CommandCenter { const repositoryPath = await window.withProgress( opts, - (progress, token) => this.git.clone(url!, parentPath!, progress, token) + (progress, token) => this.git.clone(url!, { parentPath: parentPath!, progress, recursive: options.recursive }, token) ); let message = localize('proposeopen', "Would you like to open the cloned repository?"); @@ -586,6 +585,16 @@ export class CommandCenter { } } + @command('git.clone') + async clone(url?: string, parentPath?: string): Promise { + this.cloneRepository(url, parentPath); + } + + @command('git.cloneRecursive') + async cloneRecursive(url?: string, parentPath?: string): Promise { + this.cloneRepository(url, parentPath, { recursive: true }); + } + @command('git.init') async init(skipFolderPrompt = false): Promise { let repositoryPath: string | undefined = undefined; @@ -2539,7 +2548,11 @@ export class CommandCenter { @command('git.rebaseAbort', { repository: true }) async rebaseAbort(repository: Repository): Promise { - await repository.rebaseAbort(); + if (repository.rebaseCommit) { + await repository.rebaseAbort(); + } else { + await window.showInformationMessage(localize('no rebase', "No rebase in progress.")); + } } private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 8725d85a5be..e9592dd58ee 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -351,6 +351,12 @@ function sanitizePath(path: string): string { const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B'; +export interface ICloneOptions { + readonly parentPath: string; + readonly progress: Progress<{ increment: number }>; + readonly recursive?: boolean; +} + export class Git { readonly path: string; @@ -373,18 +379,18 @@ export class Git { return; } - async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise { + async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise { let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository'; let folderName = baseFolderName; - let folderPath = path.join(parentPath, folderName); + let folderPath = path.join(options.parentPath, folderName); let count = 1; while (count < 20 && await new Promise(c => exists(folderPath, c))) { folderName = `${baseFolderName}-${count++}`; - folderPath = path.join(parentPath, folderName); + folderPath = path.join(options.parentPath, folderName); } - await mkdirp(parentPath); + await mkdirp(options.parentPath); const onSpawn = (child: cp.ChildProcess) => { const decoder = new StringDecoder('utf8'); @@ -408,14 +414,18 @@ export class Git { } if (totalProgress !== previousProgress) { - progress.report({ increment: totalProgress - previousProgress }); + options.progress.report({ increment: totalProgress - previousProgress }); previousProgress = totalProgress; } }); }; try { - await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn }); + let command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress']; + if (options.recursive) { + command.push('--recursive'); + } + await this.exec(options.parentPath, command, { cancellationToken, onSpawn }); } catch (err) { if (err.stderr) { err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim(); diff --git a/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index 33f71b6aa8e..2b1a204c603 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -34,4 +34,4 @@ export class GitProtocolHandler implements UriHandler { dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 6fc49c12592..6b4fd018815 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode'; import * as nls from 'vscode-nls'; import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git'; import { AutoFetcher } from './autofetch'; @@ -643,6 +643,7 @@ export class Repository implements Disposable { } this._rebaseCommit = rebaseCommit; + commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit); } get rebaseCommit(): Commit | undefined { @@ -1791,6 +1792,28 @@ export class Repository implements Disposable { return `${this.HEAD.behind}↓ ${this.HEAD.ahead}↑`; } + get syncTooltip(): string { + if (!this.HEAD + || !this.HEAD.name + || !this.HEAD.commit + || !this.HEAD.upstream + || !(this.HEAD.ahead || this.HEAD.behind) + ) { + return localize('sync changes', "Synchronize Changes"); + } + + const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote; + const remote = this.remotes.find(r => r.name === remoteName); + + if ((remote && remote.isReadOnly) || !this.HEAD.ahead) { + return localize('pull n', "Pull {0} commits from {1}/{2}", this.HEAD.behind, this.HEAD.upstream.remote, this.HEAD.upstream.name); + } else if (!this.HEAD.behind) { + return localize('push n', "Push {0} commits to {1}/{2}", this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name); + } else { + return localize('pull push n', "Pull {0} and push {1} commits between {2}/{3}", this.HEAD.behind, this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name); + } + } + private updateInputBoxPlaceholder(): void { const branchName = this.headShortName; diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index 8a108ae1b45..fd556024f83 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -28,7 +28,7 @@ class CheckoutStatusBar { return { command: 'git.checkout', - tooltip: `${this.repository.headLabel}`, + tooltip: localize('checkout', "Checkout branch/tag..."), title, arguments: [this.repository.sourceControl] }; @@ -150,7 +150,7 @@ class SyncStatusBar { const rebaseWhenSync = config.get('rebaseWhenSync'); command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync'; - tooltip = localize('sync changes', "Synchronize Changes"); + tooltip = this.repository.syncTooltip; } else { icon = '$(cloud-upload)'; command = 'git.publish'; diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 85d7d9b0b64..0c363cb2935 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -95,6 +95,10 @@ { "command": "npm.runScriptFromFolder", "title": "%command.runScriptFromFolder%" + }, + { + "command": "npm.packageManager", + "title": "%command.packageManager" } ], "menus": { @@ -126,6 +130,10 @@ { "command": "npm.runScriptFromFolder", "when": "false" + }, + { + "command": "npm.packageManager", + "when": "false" } ], "editor/context": [ diff --git a/extensions/npm/package.nls.json b/extensions/npm/package.nls.json index 51756d241f1..8ecf3746281 100644 --- a/extensions/npm/package.nls.json +++ b/extensions/npm/package.nls.json @@ -19,5 +19,6 @@ "command.openScript": "Open", "command.runInstall": "Run Install", "command.runSelectedScript": "Run Script", - "command.runScriptFromFolder": "Run NPM Script in Folder..." + "command.runScriptFromFolder": "Run NPM Script in Folder...", + "command.packageManager": "Get Configured Package Manager" } diff --git a/extensions/npm/src/npmMain.ts b/extensions/npm/src/npmMain.ts index fc0f8a5c557..f5c4b419e53 100644 --- a/extensions/npm/src/npmMain.ts +++ b/extensions/npm/src/npmMain.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { addJSONProviders } from './features/jsonContributions'; import { runSelectedScript, selectAndRunScriptFromFolder } from './commands'; import { NpmScriptsTreeDataProvider } from './npmView'; -import { invalidateTasksCache, NpmTaskProvider } from './tasks'; +import { getPackageManager, invalidateTasksCache, NpmTaskProvider } from './tasks'; import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover'; let treeDataProvider: NpmScriptsTreeDataProvider | undefined; @@ -56,7 +56,12 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(vscode.commands.registerCommand('npm.refresh', () => { invalidateScriptCaches(); })); - + context.subscriptions.push(vscode.commands.registerCommand('npm.packageManager', (args) => { + if (args instanceof vscode.Uri) { + return getPackageManager(args, true); + } + return ''; + })); } function canRunNpmInCurrentWorkspace() { diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index f0681b489c6..4b51fd395de 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -108,15 +108,15 @@ export function isWorkspaceFolder(value: any): value is WorkspaceFolder { return value && typeof value !== 'number'; } -export async function getPackageManager(folder: WorkspaceFolder): Promise { - let packageManagerName = workspace.getConfiguration('npm', folder.uri).get('packageManager', 'npm'); +export async function getPackageManager(folder: Uri, silent: boolean = false): Promise { + let packageManagerName = workspace.getConfiguration('npm', folder).get('packageManager', 'npm'); if (packageManagerName === 'auto') { - const { name, multiplePMDetected } = await findPreferredPM(folder.uri.fsPath); + const { name, multiplePMDetected } = await findPreferredPM(folder.fsPath); packageManagerName = name; - if (multiplePMDetected) { - const multiplePMWarning = localize('npm.multiplePMWarning', 'Found multiple lockfiles for {0}. Using {1} as the preferred package manager.', folder.uri.fsPath, packageManagerName); + if (multiplePMDetected && !silent) { + const multiplePMWarning = localize('npm.multiplePMWarning', 'Found multiple lockfiles for {0}. Using {1} as the preferred package manager.', folder.fsPath, packageManagerName); window.showWarningMessage(multiplePMWarning); } } @@ -291,7 +291,7 @@ export async function createTask(script: NpmTaskDefinition | string, cmd: string kind = script; } - const packageManager = await getPackageManager(folder); + const packageManager = await getPackageManager(folder.uri); async function getCommandLine(cmd: string): Promise { if (workspace.getConfiguration('npm', folder.uri).get('runSilent')) { return `${packageManager} --silent ${cmd}`; @@ -378,7 +378,7 @@ export async function startDebugging(scriptName: string, cwd: string, folder: Wo request: 'launch', name: `Debug ${scriptName}`, cwd, - runtimeExecutable: await getPackageManager(folder), + runtimeExecutable: await getPackageManager(folder.uri), runtimeArgs: [ 'run', scriptName, diff --git a/extensions/scss/test/colorize-results/test_scss.json b/extensions/scss/test/colorize-results/test_scss.json index 5841c3b8e3f..66f3e1f376d 100644 --- a/extensions/scss/test/colorize-results/test_scss.json +++ b/extensions/scss/test/colorize-results/test_scss.json @@ -16206,11 +16206,11 @@ "c": "rel", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.attribute-selector.scss entity.other.attribute-name.attribute.scss", "r": { - "dark_plus": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_plus": "entity.other.attribute-name.attribute.scss: #800000", - "dark_vs": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_vs": "entity.other.attribute-name.attribute.scss: #800000", - "hc_black": "entity.other.attribute-name.attribute.scss: #D7BA7D" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" } }, { @@ -20606,11 +20606,11 @@ "c": "data-icon", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.attribute-selector.scss entity.other.attribute-name.attribute.scss", "r": { - "dark_plus": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_plus": "entity.other.attribute-name.attribute.scss: #800000", - "dark_vs": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_vs": "entity.other.attribute-name.attribute.scss: #800000", - "hc_black": "entity.other.attribute-name.attribute.scss: #D7BA7D" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" } }, { diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 1b4cf8b967e..3a5008aef7a 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -86,7 +86,6 @@ "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 495a15238dc..d0382cec294 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -105,10 +105,7 @@ "entity.other.attribute-name.parent-selector.css", "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", - "source.css.less entity.other.attribute-name.id", - - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 3410551898b..23881ae8dc7 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -87,7 +87,6 @@ "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 70ed27503cd..dfd3f6295aa 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -332,6 +332,12 @@ "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", "scope": "resource" }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { "type": "boolean", "default": false, @@ -450,6 +456,12 @@ "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", "scope": "resource" }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { "type": "boolean", "default": false, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 6009e367122..69779f49df7 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -25,6 +25,7 @@ "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": "Defines space handling after opening and before closing non-empty parenthesis.", "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": "Defines space handling after opening and before closing non-empty brackets.", "format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": "Defines space handling after opening and before closing non-empty braces.", + "format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": "Defines space handling after opening and before closing empty braces.", "format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": "Defines space handling after opening and before closing template string braces.", "format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": "Defines space handling after opening and before closing JSX expression braces.", "format.insertSpaceAfterTypeAssertion": "Defines space handling after type assertions in TypeScript.", diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index dbd3513f0d4..03ebff6dd21 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -18,6 +18,11 @@ namespace Experimental { export interface UserPreferences extends Proto.UserPreferences { readonly provideRefactorNotApplicableReason?: boolean; } + + // https://github.com/microsoft/TypeScript/issues/41208 + export interface FormatCodeSettings extends Proto.FormatCodeSettings { + readonly insertSpaceAfterOpeningAndBeforeClosingEmptyBraces?: boolean; + } } interface FileConfiguration { @@ -136,7 +141,7 @@ export default class FileConfigurationManager extends Disposable { private getFormatOptions( document: vscode.TextDocument, options: vscode.FormattingOptions - ): Proto.FormatCodeSettings { + ): Experimental.FormatCodeSettings { const config = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format', document.uri); @@ -157,6 +162,7 @@ export default class FileConfigurationManager extends Disposable { insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'), insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'), insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'), + insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingEmptyBraces'), insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'), insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'), insertSpaceAfterTypeAssertion: config.get('insertSpaceAfterTypeAssertion'), diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index a4de074897f..236ef694967 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -36,11 +36,12 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { } return new vscode.Hover( - this.getContents(response.body, response._serverType), + this.getContents(document.uri, response.body, response._serverType), typeConverters.Range.fromTextSpan(response.body)); } private getContents( + resource: vscode.Uri, data: Proto.QuickInfoResponseBody, source: ServerType | undefined, ) { @@ -49,7 +50,7 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { if (data.displayString) { const displayParts: string[] = []; - if (source === ServerType.Syntax && this.client.capabilities.has(ClientCapability.Semantic)) { + if (source === ServerType.Syntax && this.client.hasCapabilityForResource(resource, ClientCapability.Semantic)) { displayParts.push( localize({ key: 'loadingPrefix', diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 530e79c7fbd..2d9dad6392a 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -677,13 +677,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType switch (capability) { case ClientCapability.Semantic: { - switch (resource.scheme) { - case fileSchemes.file: - case fileSchemes.untitled: - return true; - default: - return false; - } + return fileSchemes.semanticSupportedSchemes.includes(resource.scheme); } case ClientCapability.Syntax: case ClientCapability.EnhancedSyntax: @@ -851,6 +845,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType break; } case EventName.projectsUpdatedInBackground: + this.loadingIndicator.reset(); + const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body; const resources = body.openFiles.map(file => this.toResource(file)); this.bufferSyncSupport.getErr(resources); diff --git a/extensions/typescript-language-features/src/utils/fileSchemes.ts b/extensions/typescript-language-features/src/utils/fileSchemes.ts index d465a60326e..b072923b39a 100644 --- a/extensions/typescript-language-features/src/utils/fileSchemes.ts +++ b/extensions/typescript-language-features/src/utils/fileSchemes.ts @@ -13,6 +13,7 @@ export const walkThroughSnippet = 'walkThroughSnippet'; export const semanticSupportedSchemes = [ file, untitled, + walkThroughSnippet, ]; /** diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index f83a319aeba..b5a0a79166b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -140,20 +140,25 @@ suite('vscode API - editors', () => { } test('TextEditor.edit can control undo/redo stack 1', () => { - return withRandomFileEditor('Hello world!', (editor, doc) => { - return executeReplace(editor, new Range(0, 0, 0, 1), 'h', false, false).then(applied => { - assert.ok(applied); - assert.equal(doc.getText(), 'hello world!'); - assert.ok(doc.isDirty); - return executeReplace(editor, new Range(0, 1, 0, 5), 'ELLO', false, false); - }).then(applied => { - assert.ok(applied); - assert.equal(doc.getText(), 'hELLO world!'); - assert.ok(doc.isDirty); - return commands.executeCommand('undo'); - }).then(_ => { - assert.equal(doc.getText(), 'Hello world!'); - }); + return withRandomFileEditor('Hello world!', async (editor, doc) => { + const applied1 = await executeReplace(editor, new Range(0, 0, 0, 1), 'h', false, false); + assert.ok(applied1); + assert.equal(doc.getText(), 'hello world!'); + assert.ok(doc.isDirty); + + const applied2 = await executeReplace(editor, new Range(0, 1, 0, 5), 'ELLO', false, false); + assert.ok(applied2); + assert.equal(doc.getText(), 'hELLO world!'); + assert.ok(doc.isDirty); + + await commands.executeCommand('undo'); + if (doc.getText() === 'hello world!') { + // see https://github.com/microsoft/vscode/issues/109131 + // it looks like an undo stop was inserted in between these two edits + // it is unclear why this happens, but it can happen for a multitude of reasons + await commands.executeCommand('undo'); + } + assert.equal(doc.getText(), 'Hello world!'); }); }); diff --git a/gulpfile.js b/gulpfile.js index 8a5eff502f6..1d13cff608c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -40,4 +40,4 @@ process.on('unhandledRejection', (reason, p) => { // Load all the gulpfiles only if running tasks other than the editor tasks const build = path.join(__dirname, 'build'); require('glob').sync('gulpfile.*.js', { cwd: build }) - .forEach(f => require(`./build/${f}`)); \ No newline at end of file + .forEach(f => require(`./build/${f}`)); diff --git a/package.json b/package.json index bda7354c003..03c05997786 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.51.0", - "distro": "2efd579dedb12e7e9ed9ebd112120fa8b3b0922e", + "distro": "74c7075cd3aa62f7b74214e04d027bb05b743a1a", "author": { "name": "Microsoft Corporation" }, @@ -168,7 +168,7 @@ "style-loader": "^1.0.0", "ts-loader": "^4.4.2", "tsec": "googleinterns/tsec", - "typescript": "^4.1.0-dev.20200924", + "typescript": "^4.1.0-dev.20201018", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", @@ -195,4 +195,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/resources/linux/debian/control.template b/resources/linux/debian/control.template index 903640cd503..5a6d7be652b 100644 --- a/resources/linux/debian/control.template +++ b/resources/linux/debian/control.template @@ -1,7 +1,7 @@ Package: @@NAME@@ Version: @@VERSION@@ Section: devel -Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1 +Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1, libgbm1 Priority: optional Architecture: @@ARCHITECTURE@@ Maintainer: Microsoft Corporation diff --git a/resources/linux/rpm/dependencies.json b/resources/linux/rpm/dependencies.json index 07e2b307fcd..7f95cd3e5db 100644 --- a/resources/linux/rpm/dependencies.json +++ b/resources/linux/rpm/dependencies.json @@ -62,7 +62,8 @@ "libc.so.6(GLIBC_2.9)(64bit)", "libxcb.so.1()(64bit)", "libxkbfile.so.1()(64bit)", - "libsecret-1.so.0()(64bit)" + "libsecret-1.so.0()(64bit)", + "libgbm.so.1()(64bit)" ], "aarch64": [ "libpthread.so.0()(aarch64)", diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index 7dbc1680ba2..046158e888e 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -31,6 +31,7 @@ parts: - libgconf-2-4 - libglib2.0-bin - libgnome-keyring0 + - libgbm1 - libgtk-3-0 - libnotify4 - libnspr4 diff --git a/resources/web/code-web.js b/resources/web/code-web.js index c38826c0847..464ac2f639d 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -60,6 +60,7 @@ if (args.help) { ' --host Remote host\n' + ' --port Remote/Local port\n' + ' --local_port Local port override\n' + + ' --secondary-port Secondary port\n' + ' --extension Path of an extension to include\n' + ' --github-auth Github authentication token\n' + ' --verbose Print out more information\n' + @@ -72,6 +73,7 @@ if (args.help) { const PORT = args.port || process.env.PORT || 8080; const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT; +const SECONDARY_PORT = args['secondary-port'] || (parseInt(PORT, 10) + 1); const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http'; const HOST = args.host || 'localhost'; const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; @@ -207,7 +209,11 @@ const commandlineProvidedExtensionsPromise = getCommandlineProvidedExtensionInfo const mapCallbackUriToRequestId = new Map(); -const server = http.createServer((req, res) => { +/** + * @param req {http.IncomingMessage} + * @param res {http.ServerResponse} + */ +const requestHandler = (req, res) => { const parsedUrl = url.parse(req.url, true); const pathname = parsedUrl.pathname; @@ -252,16 +258,25 @@ const server = http.createServer((req, res) => { return serveError(req, res, 500, 'Internal Server Error.'); } -}); +}; +const server = http.createServer(requestHandler); server.listen(LOCAL_PORT, () => { if (LOCAL_PORT !== PORT) { - console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`); + console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`); } - console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`); + console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`); +}); +server.on('error', err => { + console.error(`Error occurred in server:`); + console.error(err); }); -server.on('error', err => { +const secondaryServer = http.createServer(requestHandler); +secondaryServer.listen(SECONDARY_PORT, () => { + console.log(`Secondary server available at ${SCHEME}://${HOST}:${SECONDARY_PORT}`); +}); +secondaryServer.on('error', err => { console.error(`Error occurred in server:`); console.error(err); }); @@ -366,6 +381,7 @@ async function handleRoot(req, res) { folderUri: folderUri, staticExtensions, enableSyncByDefault: args['enable-sync'], + webWorkerExtensionHostIframeSrc: `${SCHEME}://${HOST}:${SECONDARY_PORT}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` }; if (args['wrap-iframe']) { webConfigJSON._wrapWebWorkerExtHostInIframe = true; diff --git a/src/vs/base/browser/codicons.ts b/src/vs/base/browser/codicons.ts index ab29f13ce8b..cf55c1a9b92 100644 --- a/src/vs/base/browser/codicons.ts +++ b/src/vs/base/browser/codicons.ts @@ -18,7 +18,7 @@ export function renderCodicons(text: string): Array { textStart = (match.index || 0) + match[0].length; const [, escaped, codicon, name, animation] = match; - elements.push(escaped ? `$(${codicon})` : dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`)); + elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation)); } if (textStart < text.length) { @@ -26,3 +26,7 @@ export function renderCodicons(text: string): Array { } return elements; } + +export function renderCodicon(name: string, animation: string): HTMLSpanElement { + return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`); +} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 0c19deeb873..23ddd6cbea0 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -10,12 +10,13 @@ import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { TimeoutTimer } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FileAccess, RemoteAuthorities } from 'vs/base/common/network'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { insane, InsaneOptions } from 'vs/base/common/insane/insane'; +import { KeyCode } from 'vs/base/common/keyCodes'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { @@ -35,33 +36,17 @@ export function isInDOM(node: Node | null): boolean { interface IDomClassList { addClass(node: HTMLElement | SVGElement, className: string): void; - addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void; - removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void; toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void; } const _classList: IDomClassList = new class implements IDomClassList { - addClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.addClass(node, name))); - } - addClass(node: HTMLElement, className: string): void { if (className && node.classList) { node.classList.add(className); } } - removeClass(node: HTMLElement, className: string): void { - if (className && node.classList) { - node.classList.remove(className); - } - } - - removeClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.removeClass(node, name))); - } - toggleClass(node: HTMLElement, className: string, shouldHaveIt?: boolean): void { if (node.classList) { node.classList.toggle(className, shouldHaveIt); @@ -72,10 +57,6 @@ const _classList: IDomClassList = new class implements IDomClassList { /** @deprecated ES6 - use classList*/ export function addClass(node: HTMLElement | SVGElement, className: string): void { return _classList.addClass(node, className); } /** @deprecated ES6 - use classList*/ -export function addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.addClasses(node, ...classNames); } -/** @deprecated ES6 - use classList*/ -export function removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.removeClasses(node, ...classNames); } -/** @deprecated ES6 - use classList*/ export function toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void { return _classList.toggleClass(node, className, shouldHaveIt); } class DomListener implements IDisposable { @@ -500,6 +481,26 @@ export class Dimension implements IDimension { public readonly height: number, ) { } + with(width: number = this.width, height: number = this.height): Dimension { + if (width !== this.width || height !== this.height) { + return new Dimension(width, height); + } else { + return this; + } + } + + static is(obj: unknown): obj is IDimension { + return typeof obj === 'object' && typeof (obj).height === 'number' && typeof (obj).width === 'number'; + } + + static lift(obj: IDimension): Dimension { + if (obj instanceof Dimension) { + return obj; + } else { + return new Dimension(obj.width, obj.height); + } + } + static equals(a: Dimension | undefined, b: Dimension | undefined): boolean { if (a === b) { return true; @@ -998,8 +999,15 @@ export function prepend(parent: HTMLElement, child: T): T { /** * Removes all children from `parent` and appends `children` */ -export function reset(parent: HTMLElement, ...children: Array) { +export function reset(parent: HTMLElement, ...children: Array): void { parent.innerText = ''; + appendChildren(parent, ...children); +} + +/** + * Appends `children` to `parent` + */ +export function appendChildren(parent: HTMLElement, ...children: Array): void { for (const child of children) { if (child instanceof Node) { parent.appendChild(child); @@ -1346,7 +1354,7 @@ const _ttpSafeInnerHtml = window.trustedTypes?.createPolicy('safeInnerHtml', { export function safeInnerHtml(node: HTMLElement, value: string): void { const options = _extInsaneOptions({ - allowedTags: ['a', 'button', 'code', 'div', 'h1', 'h2', 'h3', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'textarea', 'ul'], + allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], allowedAttributes: { 'a': ['href'], 'button': ['data-href'], @@ -1383,3 +1391,190 @@ function toBinary(str: string): string { export function multibyteAwareBtoa(str: string): string { return btoa(toBinary(str)); } + +/** + * Typings for the https://wicg.github.io/file-system-access + * + * Use `supported(window)` to find out if the browser supports this kind of API. + */ +export namespace WebFileSystemAccess { + + // https://wicg.github.io/file-system-access/#dom-window-showdirectorypicker + export interface FileSystemAccess { + showDirectoryPicker: () => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemdirectoryhandle + export interface FileSystemDirectoryHandle { + readonly kind: 'directory', + readonly name: string, + + getFileHandle: (name: string, options?: { create?: boolean }) => Promise; + getDirectoryHandle: (name: string, options?: { create?: boolean }) => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemfilehandle + export interface FileSystemFileHandle { + readonly kind: 'file', + readonly name: string, + + createWritable: (options?: { keepExistingData?: boolean }) => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemwritablefilestream + export interface FileSystemWritableFileStream { + write: (buffer: Uint8Array) => Promise; + close: () => Promise; + } + + export function supported(obj: any & Window): obj is FileSystemAccess { + const candidate = obj as FileSystemAccess; + if (typeof candidate?.showDirectoryPicker === 'function') { + return true; + } + + return false; + } +} + +type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta'; + +export interface IModifierKeyStatus { + altKey: boolean; + shiftKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; + event?: KeyboardEvent; +} + +export class ModifierKeyEmitter extends Emitter { + + private readonly _subscriptions = new DisposableStore(); + private _keyStatus: IModifierKeyStatus; + private static instance: ModifierKeyEmitter; + + private constructor() { + super(); + + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false + }; + + this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => { + const event = new StandardKeyboardEvent(e); + + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = 'alt'; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = 'ctrl'; + } else if (e.metaKey && !this._keyStatus.metaKey) { + this._keyStatus.lastKeyPressed = 'meta'; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = 'shift'; + } else if (event.keyCode !== KeyCode.Alt) { + this._keyStatus.lastKeyPressed = undefined; + } else { + return; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + })); + + this._subscriptions.add(domEvent(document.body, 'keyup', true)(e => { + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = 'alt'; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = 'ctrl'; + } else if (!e.metaKey && this._keyStatus.metaKey) { + this._keyStatus.lastKeyReleased = 'meta'; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = 'shift'; + } else { + this._keyStatus.lastKeyReleased = undefined; + } + + if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + })); + + this._subscriptions.add(domEvent(document.body, 'mousedown', true)(e => { + this._keyStatus.lastKeyPressed = undefined; + })); + + this._subscriptions.add(domEvent(document.body, 'mouseup', true)(e => { + this._keyStatus.lastKeyPressed = undefined; + })); + + this._subscriptions.add(domEvent(document.body, 'mousemove', true)(e => { + if (e.buttons) { + this._keyStatus.lastKeyPressed = undefined; + } + })); + + this._subscriptions.add(domEvent(window, 'blur')(e => { + this.resetKeyStatus(); + })); + } + + get keyStatus(): IModifierKeyStatus { + return this._keyStatus; + } + + get isModifierPressed(): boolean { + return this._keyStatus.altKey || this._keyStatus.ctrlKey || this._keyStatus.metaKey || this._keyStatus.shiftKey; + } + + /** + * Allows to explicitly reset the key status based on more knowledge (#109062) + */ + resetKeyStatus(): void { + this.doResetKeyStatus(); + this.fire(this._keyStatus); + } + + private doResetKeyStatus(): void { + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false + }; + } + + static getInstance() { + if (!ModifierKeyEmitter.instance) { + ModifierKeyEmitter.instance = new ModifierKeyEmitter(); + } + + return ModifierKeyEmitter.instance; + } + + dispose() { + super.dispose(); + this._subscriptions.dispose(); + } +} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 1f089e2542e..2d601e975df 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -222,11 +222,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende markedOptions.sanitize = true; markedOptions.renderer = renderer; - const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote, Schemas.vscodeRemoteResource]; - if (markdown.isTrusted) { - allowedSchemes.push(Schemas.command); - } - // values that are too long will freeze the UI let value = markdown.value ?? ''; if (value.length > 100_000) { @@ -239,9 +234,43 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const renderedMarkdown = marked.parse(value, markedOptions); - // sanitize with insane - const insaneOptions = { + element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown); + + // signal that async code blocks can be now be inserted + signalInnerHTML!(); + + return element; +} + +function sanitizeRenderedMarkdown( + options: { isTrusted?: boolean }, + renderedMarkdown: string, +): string { + const insaneOptions = getInsaneOptions(options); + if (_ttpInsane) { + return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string; + } else { + return insane(renderedMarkdown, insaneOptions); + } +} + +function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions { + const allowedSchemes = [ + Schemas.http, + Schemas.https, + Schemas.mailto, + Schemas.data, + Schemas.file, + Schemas.vscodeRemote, + Schemas.vscodeRemoteResource, + ]; + + if (options.isTrusted) { + allowedSchemes.push(Schemas.command); + } + + return { allowedSchemes, // allowedTags should included everything that markdown renders to. // Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure. @@ -257,8 +286,8 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende 'th': ['align'], 'td': ['align'] }, - filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { - if (token.tag === 'span' && markdown.isTrusted && (Object.keys(token.attrs).length === 1)) { + filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean { + if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) { if (token.attrs['style']) { return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/); } else if (token.attrs['class']) { @@ -270,15 +299,5 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende return true; } }; - - if (_ttpInsane) { - element.innerHTML = _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string; - } else { - element.innerHTML = insane(renderedMarkdown, insaneOptions); - } - - // signal that async code blocks can be now be inserted - signalInnerHTML!(); - - return element; } + diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 79c2a927201..a88787fd0fa 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -96,3 +96,15 @@ justify-content: center; margin-right: 10px; } + +.monaco-action-bar .action-item.action-dropdown-item { + display: flex; +} + +.monaco-action-bar .action-item.action-dropdown-item > .action-label { + margin-right: 1px; +} + +.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown { + margin-right: 4px; +} diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 07d3893ff83..3fa0c7518ac 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -21,7 +21,7 @@ export const enum ActionsOrientation { } export interface ActionTrigger { - keys: KeyCode[]; + keys?: KeyCode[]; keyDown: boolean; } @@ -49,7 +49,10 @@ export class ActionBar extends Disposable implements IActionRunner { private _actionRunner: IActionRunner; private _context: unknown; private readonly _orientation: ActionsOrientation; - private readonly _triggerKeys: ActionTrigger; + private readonly _triggerKeys: { + keys: KeyCode[]; + keyDown: boolean; + }; private _actionIds: string[]; // View Items @@ -80,9 +83,9 @@ export class ActionBar extends Disposable implements IActionRunner { this.options = options; this._context = options.context ?? null; this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL; - this._triggerKeys = this.options.triggerKeys ?? { - keys: [KeyCode.Enter, KeyCode.Space], - keyDown: false + this._triggerKeys = { + keyDown: this.options.triggerKeys?.keyDown ?? false, + keys: this.options.triggerKeys?.keys ?? [KeyCode.Enter, KeyCode.Space] }; if (this.options.actionRunner) { diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 54d10a71de3..5713a8a9337 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./dropdown'; -import { IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; +import { Action, IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { append, $ } from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; @@ -135,3 +135,32 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { } } } + +export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions { + readonly menuActionsOrProvider: readonly IAction[] | IActionProvider; + readonly menuActionClassNames?: string[]; +} + +export class ActionWithDropdownActionViewItem extends ActionViewItem { + + protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined; + + constructor( + context: unknown, + action: IAction, + options: IActionWithDropdownActionViewItemOptions, + private readonly contextMenuProvider: IContextMenuProvider + ) { + super(context, action, options); + } + + render(container: HTMLElement): void { + super.render(container); + if (this.element) { + this.element.classList.add('action-dropdown-item'); + this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', 'codicon-chevron-down', ...(this.options).menuActionClassNames || []] }); + this.dropdownMenuActionViewItem.render(this.element); + } + } + +} diff --git a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts new file mode 100644 index 00000000000..0e853d988b5 --- /dev/null +++ b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export interface IHoverDelegateTarget extends IDisposable { + readonly targetElements: readonly HTMLElement[]; + x?: number; +} + +export interface IHoverDelegateOptions { + text: IMarkdownString | string; + target: IHoverDelegateTarget | HTMLElement; + anchorPosition?: AnchorPosition; +} + +export interface IHoverDelegate { + showHover(options: IHoverDelegateOptions): IDisposable | undefined; +} diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 3ccf3f0ad88..3916b4f059b 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -7,18 +7,25 @@ import 'vs/css!./iconlabel'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMatch } from 'vs/base/common/filters'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; import { equals } from 'vs/base/common/objects'; +import { isMacintosh } from 'vs/base/common/platform'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { isString } from 'vs/base/common/types'; +import { domEvent } from 'vs/base/browser/event'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; supportDescriptionHighlights?: boolean; supportCodicons?: boolean; + hoverDelegate?: IHoverDelegate; } export interface IIconLabelValueOptions { - title?: string; + title?: string | IMarkdownString | Promise; descriptionTitle?: string; hideIcon?: boolean; extraClasses?: string[]; @@ -35,7 +42,6 @@ class FastLabelNode { private disposed: boolean | undefined; private _textContent: string | undefined; private _className: string | undefined; - private _title: string | undefined; private _empty: boolean | undefined; constructor(private _element: HTMLElement) { @@ -63,19 +69,6 @@ class FastLabelNode { this._element.className = className; } - set title(title: string) { - if (this.disposed || title === this._title) { - return; - } - - this._title = title; - if (this._title) { - this._element.title = title; - } else { - this._element.removeAttribute('title'); - } - } - set empty(empty: boolean) { if (this.disposed || empty === this._empty) { return; @@ -100,6 +93,9 @@ export class IconLabel extends Disposable { private descriptionNode: FastLabelNode | HighlightedLabel | undefined; private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; + private hoverDelegate: IHoverDelegate | undefined = undefined; + private readonly customHovers: Map = new Map(); + constructor(container: HTMLElement, options?: IIconLabelCreationOptions) { super(); @@ -121,6 +117,10 @@ export class IconLabel extends Disposable { } else { this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } + + if (options?.hoverDelegate) { + this.hoverDelegate = options.hoverDelegate; + } } get element(): HTMLElement { @@ -144,7 +144,7 @@ export class IconLabel extends Disposable { } this.domNode.className = classes.join(' '); - this.domNode.title = options?.title || ''; + this.setupHover(this.domNode.element, options?.title); this.nameNode.setLabel(label, options); @@ -155,18 +155,82 @@ export class IconLabel extends Disposable { if (this.descriptionNode instanceof HighlightedLabel) { this.descriptionNode.set(description || '', options ? options.descriptionMatches : undefined); - if (options?.descriptionTitle) { - this.descriptionNode.element.title = options.descriptionTitle; - } else { - this.descriptionNode.element.removeAttribute('title'); - } + this.setupHover(this.descriptionNode.element, options?.descriptionTitle); } else { this.descriptionNode.textContent = description || ''; - this.descriptionNode.title = options?.descriptionTitle || ''; + this.setupHover(this.descriptionNode.element, options?.descriptionTitle || ''); this.descriptionNode.empty = !description; } } } + + private setupHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise | undefined): void { + const previousCustomHover = this.customHovers.get(htmlElement); + if (previousCustomHover) { + previousCustomHover.dispose(); + this.customHovers.delete(htmlElement); + } + + if (!tooltip) { + htmlElement.removeAttribute('title'); + return; + } + + if (!this.hoverDelegate) { + return this.setupNativeHover(htmlElement, tooltip); + } else { + return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); + } + } + + private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise | undefined): void { + htmlElement.removeAttribute('title'); + // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. + // On Mac, the delay is 1500. + const hoverDelay = isMacintosh ? 1500 : 500; + let hoverOptions: IHoverDelegateOptions | undefined; + let mouseX: number | undefined; + function mouseOver(this: HTMLElement, e: MouseEvent): any { + let isHovering = true; + function mouseMove(this: HTMLElement, e: MouseEvent): any { + mouseX = e.x; + } + function mouseLeave(this: HTMLElement, e: MouseEvent): any { + isHovering = false; + } + const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeave.bind(htmlElement)); + const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); + setTimeout(async () => { + if (isHovering && tooltip) { + // Re-use the already computed hover options if they exist. + if (!hoverOptions) { + const target: IHoverDelegateTarget = { + targetElements: [this], + dispose: () => { } + }; + const resolvedTooltip = await tooltip; + if (resolvedTooltip) { + hoverOptions = { text: resolvedTooltip, target, anchorPosition: AnchorPosition.BELOW }; + } + } + if (hoverOptions) { + if (mouseX !== undefined) { + (hoverOptions.target).x = mouseX + 10; + } + hoverDelegate.showHover(hoverOptions); + } + } + mouseMoveDisposable.dispose(); + mouseLeaveDisposable.dispose(); + }, hoverDelay); + } + const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); + this.customHovers.set(htmlElement, mouseOverDisposable); + } + + private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise | undefined): void { + htmlElement.title = isString(tooltip) ? tooltip : ''; + } } class Label { @@ -255,7 +319,7 @@ class LabelWithHighlights { this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons); } - this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines); + this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines); } else { this.container.innerText = ''; this.container.classList.add('multiple'); @@ -271,7 +335,7 @@ class LabelWithHighlights { const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }); const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons); - highlightedLabel.set(l, m, options?.title, options?.labelEscapeNewLines); + highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines); if (i < label.length - 1) { dom.append(name, dom.$('span.label-separator', undefined, separator)); diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 25a09d22fae..4f8e796f206 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -66,11 +66,11 @@ export interface IIdentityProvider { export interface IKeyboardNavigationLabelProvider { /** - * Return a keyboard navigation label which will be used by the - * list for filtering/navigating. Return `undefined` to make an - * element always match. + * Return a keyboard navigation label(s) which will be used by + * the list for filtering/navigating. Return `undefined` to make + * an element always match. */ - getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | undefined; + getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined; } export interface IKeyboardNavigationDelegate { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index e84d1e8b4d7..9dd4ff50457 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -868,7 +868,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { const viewBox = this.submenuContainer.getBoundingClientRect(); - const { top, left } = this.calculateSubmenuMenuLayout({ height: window.innerHeight, width: window.innerWidth }, viewBox, entryBoxUpdated, this.expandDirection); + const { top, left } = this.calculateSubmenuMenuLayout(new Dimension(window.innerWidth, window.innerHeight), Dimension.lift(viewBox), entryBoxUpdated, this.expandDirection); this.submenuContainer.style.left = `${left}px`; this.submenuContainer.style.top = `${top}px`; diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index be71339754f..cb2b3fc0cac 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -8,7 +8,6 @@ import * as browser from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; import * as nls from 'vs/nls'; -import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu'; @@ -16,7 +15,7 @@ import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from ' import { RunOnceScheduler } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable, dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; import { asArray } from 'vs/base/common/arrays'; import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode'; @@ -120,7 +119,7 @@ export class MenuBar extends Disposable { this.setUnfocusedState(); })); - this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + this._register(DOM.ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => { let event = new StandardKeyboardEvent(e as KeyboardEvent); @@ -860,8 +859,8 @@ export class MenuBar extends Disposable { } } - private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { - const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + private onModifierKeyToggled(modifierKeyStatus: DOM.IModifierKeyStatus): void { + const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey && !modifierKeyStatus.metaKey; if (this.options.visibility === 'hidden') { return; @@ -996,119 +995,3 @@ export class MenuBar extends Disposable { }; } } - -type ModifierKey = 'alt' | 'ctrl' | 'shift'; - -interface IModifierKeyStatus { - altKey: boolean; - shiftKey: boolean; - ctrlKey: boolean; - lastKeyPressed?: ModifierKey; - lastKeyReleased?: ModifierKey; - event?: KeyboardEvent; -} - - -class ModifierKeyEmitter extends Emitter { - - private readonly _subscriptions = new DisposableStore(); - private _keyStatus: IModifierKeyStatus; - private static instance: ModifierKeyEmitter; - - private constructor() { - super(); - - this._keyStatus = { - altKey: false, - shiftKey: false, - ctrlKey: false - }; - - this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => { - const event = new StandardKeyboardEvent(e); - - if (e.altKey && !this._keyStatus.altKey) { - this._keyStatus.lastKeyPressed = 'alt'; - } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { - this._keyStatus.lastKeyPressed = 'ctrl'; - } else if (e.shiftKey && !this._keyStatus.shiftKey) { - this._keyStatus.lastKeyPressed = 'shift'; - } else if (event.keyCode !== KeyCode.Alt) { - this._keyStatus.lastKeyPressed = undefined; - } else { - return; - } - - this._keyStatus.altKey = e.altKey; - this._keyStatus.ctrlKey = e.ctrlKey; - this._keyStatus.shiftKey = e.shiftKey; - - if (this._keyStatus.lastKeyPressed) { - this._keyStatus.event = e; - this.fire(this._keyStatus); - } - })); - - this._subscriptions.add(domEvent(document.body, 'keyup', true)(e => { - if (!e.altKey && this._keyStatus.altKey) { - this._keyStatus.lastKeyReleased = 'alt'; - } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { - this._keyStatus.lastKeyReleased = 'ctrl'; - } else if (!e.shiftKey && this._keyStatus.shiftKey) { - this._keyStatus.lastKeyReleased = 'shift'; - } else { - this._keyStatus.lastKeyReleased = undefined; - } - - if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { - this._keyStatus.lastKeyPressed = undefined; - } - - this._keyStatus.altKey = e.altKey; - this._keyStatus.ctrlKey = e.ctrlKey; - this._keyStatus.shiftKey = e.shiftKey; - - if (this._keyStatus.lastKeyReleased) { - this._keyStatus.event = e; - this.fire(this._keyStatus); - } - })); - - this._subscriptions.add(domEvent(document.body, 'mousedown', true)(e => { - this._keyStatus.lastKeyPressed = undefined; - })); - - this._subscriptions.add(domEvent(document.body, 'mouseup', true)(e => { - this._keyStatus.lastKeyPressed = undefined; - })); - - this._subscriptions.add(domEvent(document.body, 'mousemove', true)(e => { - if (e.buttons) { - this._keyStatus.lastKeyPressed = undefined; - } - })); - - this._subscriptions.add(domEvent(window, 'blur')(e => { - this._keyStatus.lastKeyPressed = undefined; - this._keyStatus.lastKeyReleased = undefined; - this._keyStatus.altKey = false; - this._keyStatus.shiftKey = false; - this._keyStatus.shiftKey = false; - - this.fire(this._keyStatus); - })); - } - - static getInstance() { - if (!ModifierKeyEmitter.instance) { - ModifierKeyEmitter.instance = new ModifierKeyEmitter(); - } - - return ModifierKeyEmitter.instance; - } - - dispose() { - super.dispose(); - this._subscriptions.dispose(); - } -} diff --git a/src/vs/base/browser/ui/sash/sash.css b/src/vs/base/browser/ui/sash/sash.css index e8ccd5521d2..8eb8baf9f8b 100644 --- a/src/vs/base/browser/ui/sash/sash.css +++ b/src/vs/base/browser/ui/sash/sash.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +:root { + --sash-size: 4px; +} + .monaco-sash { position: absolute; z-index: 35; @@ -42,6 +46,34 @@ pointer-events: none !important; } +.monaco-sash.vertical { + cursor: ew-resize; + top: 0; + width: var(--sash-size); + height: 100%; +} + +.monaco-sash.horizontal { + cursor: ns-resize; + left: 0; + width: 100%; + height: var(--sash-size); +} + +.monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after { + content: ' '; + height: calc(var(--sash-size) * 2); + width: calc(var(--sash-size) * 2); + z-index: 100; + display: block; + cursor: all-scroll; position: absolute; +} + +.monaco-sash.orthogonal-start.vertical::before { left: -calc(var(--sash-size) / 2); top: -var(--sash-size); } +.monaco-sash.orthogonal-end.vertical::after { left: -calc(var(--sash-size) / 2); bottom: -var(--sash-size); } +.monaco-sash.orthogonal-start.horizontal::before { top: -calc(var(--sash-size) / 2); left: -var(--sash-size); } +.monaco-sash.orthogonal-end.horizontal::after { top: -calc(var(--sash-size) / 2); right: -var(--sash-size); } + /** Debug **/ .monaco-sash.debug { diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts index ed645f8a069..64b8e8c98ee 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts @@ -8,7 +8,6 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Widget } from 'vs/base/browser/ui/widget'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; -import { addClasses } from 'vs/base/browser/dom'; /** * The arrow image size. @@ -62,7 +61,7 @@ export class ScrollbarArrow extends Widget { this.domNode = document.createElement('div'); this.domNode.className = opts.className; - addClasses(this.domNode, opts.icon.classNames); + this.domNode.classList.add(...opts.icon.classNamesArray); this.domNode.style.position = 'absolute'; this.domNode.style.width = ARROW_IMG_SIZE + 'px'; diff --git a/src/vs/base/browser/ui/splitview/paneview.css b/src/vs/base/browser/ui/splitview/paneview.css index fb8ee8e75ef..06743287c1a 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -60,7 +60,7 @@ /* TODO: actions should be part of the pane, but they aren't yet */ .monaco-pane-view .pane > .pane-header > .actions { display: none; - flex: 1; + margin-left: auto; } /* TODO: actions should be part of the pane, but they aren't yet */ diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index db296ffd300..a188ee41f00 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -507,8 +507,9 @@ class TreeRenderer implements IListRenderer } } -class TypeFilter implements ITreeFilter, IDisposable { +export type LabelFuzzyScore = { label: string; score: FuzzyScore }; +class TypeFilter implements ITreeFilter, IDisposable { private _totalCount = 0; get totalCount(): number { return this._totalCount; } private _matchCount = 0; @@ -531,7 +532,7 @@ class TypeFilter implements ITreeFilter, IDisposable { tree.onWillRefilter(this.reset, this, this.disposables); } - filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult { + filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult { if (this._filter) { const result = this._filter.filter(element, parentVisibility); @@ -562,27 +563,28 @@ class TypeFilter implements ITreeFilter, IDisposable { } const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(element); - const labelStr = label && label.toString(); + const labels = Array.isArray(label) ? label : [label]; - if (typeof labelStr === 'undefined') { - return { data: FuzzyScore.Default, visibility: true }; - } - - const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true); - - if (!score) { - if (this.tree.options.filterOnType) { - return TreeVisibility.Recurse; - } else { + for (const l of labels) { + const labelStr = l && l.toString(); + if (typeof labelStr === 'undefined') { return { data: FuzzyScore.Default, visibility: true }; } - // DEMO: smarter filter ? - // return parentVisibility === TreeVisibility.Visible ? true : TreeVisibility.Recurse; + const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true); + if (score) { + this._matchCount++; + return labels.length === 1 ? + { data: score, visibility: true } : + { data: { label: labelStr, score: score }, visibility: true }; + } } - this._matchCount++; - return { data: score, visibility: true }; + if (this.tree.options.filterOnType) { + return TreeVisibility.Recurse; + } else { + return { data: FuzzyScore.Default, visibility: true }; + } } private reset(): void { diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index f7b8d5ed64d..781067fe863 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -97,3 +97,95 @@ export class HistoryNavigator implements INavigator { return elements; } } + +interface HistoryNode { + value: T; + previous: HistoryNode | undefined; + next: HistoryNode | undefined; +} + +export class HistoryNavigator2 { + + private head: HistoryNode; + private tail: HistoryNode; + private cursor: HistoryNode; + private size: number; + + constructor(history: readonly T[], private capacity: number = 10) { + if (history.length < 1) { + throw new Error('not supported'); + } + + this.size = 1; + this.head = this.tail = this.cursor = { + value: history[0], + previous: undefined, + next: undefined + }; + + for (let i = 1; i < history.length; i++) { + this.add(history[i]); + } + } + + add(value: T): void { + const node: HistoryNode = { + value, + previous: this.tail, + next: undefined + }; + + this.tail.next = node; + this.tail = node; + this.cursor = this.tail; + this.size++; + + while (this.size > this.capacity) { + this.head = this.head.next!; + this.head.previous = undefined; + this.size--; + } + } + + replaceLast(value: T): void { + this.tail.value = value; + } + + isAtEnd(): boolean { + return this.cursor === this.tail; + } + + current(): T { + return this.cursor.value; + } + + previous(): T { + if (this.cursor.previous) { + this.cursor = this.cursor.previous; + } + + return this.cursor.value; + } + + next(): T { + if (this.cursor.next) { + this.cursor = this.cursor.next; + } + + return this.cursor.value; + } + + resetCursor(): T { + this.cursor = this.tail; + return this.cursor.value; + } + + *[Symbol.iterator](): Iterator { + let node: HistoryNode | undefined = this.head; + + while (node) { + yield node.value; + node = node.next; + } + } +} diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 5c25b92dffc..3c3510390d6 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -14,7 +14,12 @@ import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; -suite('Storage Library', () => { +suite('Storage Library', function () { + + // Given issues such as https://github.com/microsoft/vscode/issues/108113 + // we see random test failures when accessing the native file system. + this.retries(3); + this.timeout(1000 * 20); function uniqueStorageDir(): string { const id = generateUuid(); @@ -278,7 +283,12 @@ suite('Storage Library', () => { }); }); -suite('SQLite Storage Library', () => { +suite('SQLite Storage Library', function () { + + // Given issues such as https://github.com/microsoft/vscode/issues/108113 + // we see random test failures when accessing the native file system. + this.retries(3); + this.timeout(1000 * 20); function uniqueStorageDir(): string { const id = generateUuid(); @@ -540,8 +550,6 @@ suite('SQLite Storage Library', () => { }); test('real world example', async function () { - this.timeout(20000); - const storageDir = uniqueStorageDir(); await mkdirp(storageDir); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 45c871d9aa9..88f3869f8a3 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -48,10 +48,10 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel, StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -63,13 +63,15 @@ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagemen import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; +import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -149,8 +151,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IStorageService, storageService); disposables.add(toDisposable(() => storageService.flush())); - services.set(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryChannelClient(mainProcessService.getChannel('storageKeysSyncRegistryService'))); - + services.set(IStorageKeysSyncRegistryService, new SyncDescriptor(StorageKeysSyncRegistryService)); services.set(IEnvironmentService, environmentService); services.set(INativeEnvironmentService, environmentService); @@ -198,6 +199,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat } server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + const storageKeysSyncRegistryService = accessor.get(IStorageKeysSyncRegistryService); + const storageKeysSyncChannel = new StorageKeysSyncRegistryChannel(storageKeysSyncRegistryService); + server.registerChannel('storageKeysSyncRegistryService', storageKeysSyncChannel); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); @@ -208,10 +213,12 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); + services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); + services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService)); services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 3e1abf2ee79..ecc062db68a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -72,8 +72,6 @@ import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { coalesce } from 'vs/base/common/arrays'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; -import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; @@ -567,11 +565,6 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); - const storageKeysSyncRegistryService = accessor.get(IStorageKeysSyncRegistryService); - const storageKeysSyncChannel = new StorageKeysSyncRegistryChannel(storageKeysSyncRegistryService); - electronIpcServer.registerChannel('storageKeysSyncRegistryService', storageKeysSyncChannel); - sharedProcessClient.then(client => client.registerChannel('storageKeysSyncRegistryService', storageKeysSyncChannel)); - const loggerChannel = new LoggerChannel(accessor.get(ILogService)); electronIpcServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 1bf92ce1302..10f4a19f1de 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -44,7 +44,6 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; -import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/platform/remote/node/tunnelService'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -169,7 +168,6 @@ class CodeMain { services.set(IRequestService, new SyncDescriptor(RequestMainService)); services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); services.set(ISignService, new SyncDescriptor(SignService)); - services.set(IStorageKeysSyncRegistryService, new SyncDescriptor(StorageKeysSyncRegistryService)); services.set(IProductService, { _serviceBrand: undefined, ...product }); services.set(ITunnelService, new SyncDescriptor(TunnelService)); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 6d1db80fcf5..040144f4c9d 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -23,6 +23,7 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { return !!argv['install-source'] || !!argv['list-extensions'] || !!argv['install-extension'] + || !!argv['install-builtin-extension'] || !!argv['uninstall-extension'] || !!argv['locate-extension'] || !!argv['telemetry']; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 938eb1ed5ed..211aa686de7 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -69,6 +69,7 @@ export function getIdAndVersion(id: string): [string, string | undefined] { return [adoptToGalleryExtensionId(id), undefined]; } +type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; export class Main { @@ -84,8 +85,8 @@ export class Main { await this.setInstallSource(argv['install-source']); } else if (argv['list-extensions']) { await this.listExtensions(!!argv['show-versions'], argv['category']); - } else if (argv['install-extension']) { - await this.installExtensions(argv['install-extension'], !!argv['force'], { isMachineScoped: !!argv['do-not-sync'], isBuiltin: !!argv['builtin'] }); + } else if (argv['install-extension'] || argv['install-builtin-extension']) { + await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); } else if (argv['uninstall-extension']) { await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']); } else if (argv['locate-extension']) { @@ -124,69 +125,119 @@ export class Main { extensions.forEach(e => console.log(getId(e.manifest, showVersions))); } - private async installExtensions(extensions: string[], force: boolean, options: InstallOptions): Promise { + private async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise { const failed: string[] = []; const installedExtensionsManifests: IExtensionManifest[] = []; if (extensions.length) { console.log(localize('installingExtensions', "Installing extensions...")); } + const vsixs: string[] = []; + const installExtensionInfos: InstallExtensionInfo[] = []; for (const extension of extensions) { - try { - const manifest = await this.installExtension(extension, force, options); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(extension); + if (/\.vsix$/i.test(extension)) { + vsixs.push(extension); + } else { + const [id, version] = getIdAndVersion(extension); + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); } } + for (const extension of builtinExtensionIds) { + const [id, version] = getIdAndVersion(extension); + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); + } + + if (vsixs.length) { + await Promise.all(vsixs.map(async vsix => { + try { + const manifest = await this.installVSIX(vsix, force); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + console.error(err.message || err.stack || err); + failed.push(vsix); + } + })); + } + + const [galleryExtensions, installed] = await Promise.all([ + this.getGalleryExtensions(installExtensionInfos), + this.extensionManagementService.getInstalled(ExtensionType.User) + ]); + + await Promise.all(installExtensionInfos.map(async extensionInfo => { + const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); + if (gallery) { + try { + const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + console.error(err.message || err.stack || err); + failed.push(extensionInfo.id); + } + } else { + console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); + failed.push(extensionInfo.id); + } + })); + if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { await this.updateLocalizationsCache(); } - return failed.length ? Promise.reject(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))) : Promise.resolve(); + + if (failed.length) { + throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); + } } - private async installExtension(extension: string, force: boolean, options: InstallOptions): Promise { - if (/\.vsix$/i.test(extension)) { - extension = path.isAbsolute(extension) ? extension : path.join(process.cwd(), extension); - - const manifest = await getManifest(extension); - const valid = await this.validate(manifest, force); - - if (valid) { - try { - await this.extensionManagementService.install(URI.file(extension), options); - console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(extension))); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(extension))); - return null; - } else { - throw error; - } + private async installVSIX(vsix: string, force: boolean): Promise { + vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); + const manifest = await getManifest(vsix); + const valid = await this.validate(manifest, force); + if (valid) { + try { + await this.extensionManagementService.install(URI.file(vsix)); + console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); + return null; + } else { + throw error; } } - return null; } + return null; + } - const [id, version] = getIdAndVersion(extension); - let galleryExtension: IGalleryExtension | null = null; - try { - galleryExtension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - } catch (err) { - const response = JSON.parse(err.responseText); - throw new Error(response.message); - } - if (!galleryExtension) { - throw new Error(`${notFound(version ? `${id}@${version}` : id)}\n${useId}`); - } + private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { + const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); + const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); + const galleryExtensions = new Map(); + await Promise.all([ + (async () => { + const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); + result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); + })(), + Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { + const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); + if (extension) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } + })) + ]); + + return galleryExtensions; + } + + private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise { const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); - const [installedExtension] = installed.filter(e => areSameExtensions(e.identifier, { id })); + const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); if (installedExtension) { if (galleryExtension.version === installedExtension.manifest.version) { console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); @@ -198,8 +249,24 @@ export class Main { } console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); } - await this.installFromGallery(id, galleryExtension, options); - return manifest; + + try { + if (installOptions.isBuiltin) { + console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); + } else { + console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); + } + await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); + return null; + } else { + throw error; + } + } } private async validate(manifest: IExtensionManifest, force: boolean): Promise { @@ -221,21 +288,6 @@ export class Main { return true; } - private async installFromGallery(id: string, extension: IGalleryExtension, options: InstallOptions): Promise { - console.log(localize('installing', "Installing extension '{0}' v{1}...", id, extension.version)); - - try { - await this.extensionManagementService.installFromGallery(extension, options); - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, extension.version)); - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", id)); - } else { - throw error; - } - } - } - private async uninstallExtension(extensions: string[], force: boolean): Promise { async function getExtensionId(extensionDescription: string): Promise { if (!/\.vsix$/i.test(extensionDescription)) { diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 6bc4547463e..2b19fc90cff 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -156,6 +156,18 @@ export interface IContentWidget { * If null is returned, the content widget will be placed off screen. */ getPosition(): IContentWidgetPosition | null; + /** + * Optional function that is invoked before rendering + * the content widget. If a dimension is returned the editor will + * attempt to use it. + */ + beforeRender?(): editorCommon.IDimension | null; + /** + * Optional function that is invoked after rendering the content + * widget. The arguments are the actual dimensions and the selected + * position preference. + */ + afterRender?(position: ContentWidgetPositionPreference | null): void; } /** diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index a3a16436846..f6abaa7cae2 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -14,6 +14,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IDimension } from 'vs/editor/common/editorCommon'; class Coordinate { @@ -171,6 +172,11 @@ interface IBoxLayoutResult { belowLeft: number; } +interface IRenderData { + coordinate: Coordinate, + position: ContentWidgetPositionPreference +} + class Widget { private readonly _context: ViewContext; private readonly _viewDomNode: FastDomNode; @@ -194,7 +200,7 @@ class Widget { private _maxWidth: number; private _isVisible: boolean; - private _renderData: Coordinate | null; + private _renderData: IRenderData | null; constructor(context: ViewContext, viewDomNode: FastDomNode, actual: IContentWidget) { this._context = context; @@ -428,16 +434,26 @@ class Widget { return [topLeft, bottomLeft]; } - private _prepareRenderWidget(ctx: RenderingContext): Coordinate | null { + private _prepareRenderWidget(ctx: RenderingContext): IRenderData | null { const [topLeft, bottomLeft] = this._getTopAndBottomLeft(ctx); if (!topLeft || !bottomLeft) { return null; } if (this._cachedDomNodeClientWidth === -1 || this._cachedDomNodeClientHeight === -1) { - const domNode = this.domNode.domNode; - this._cachedDomNodeClientWidth = domNode.clientWidth; - this._cachedDomNodeClientHeight = domNode.clientHeight; + + let preferredDimensions: IDimension | null = null; + if (typeof this._actual.beforeRender === 'function') { + preferredDimensions = safeInvoke(this._actual.beforeRender, this._actual); + } + if (preferredDimensions) { + this._cachedDomNodeClientWidth = preferredDimensions.width; + this._cachedDomNodeClientHeight = preferredDimensions.height; + } else { + const domNode = this.domNode.domNode; + this._cachedDomNodeClientWidth = domNode.clientWidth; + this._cachedDomNodeClientHeight = domNode.clientHeight; + } } let placement: IBoxLayoutResult | null; @@ -458,7 +474,7 @@ class Widget { return null; } if (pass === 2 || placement.fitsAbove) { - return new Coordinate(placement.aboveTop, placement.aboveLeft); + return { coordinate: new Coordinate(placement.aboveTop, placement.aboveLeft), position: ContentWidgetPositionPreference.ABOVE }; } } else if (pref === ContentWidgetPositionPreference.BELOW) { if (!placement) { @@ -466,13 +482,13 @@ class Widget { return null; } if (pass === 2 || placement.fitsBelow) { - return new Coordinate(placement.belowTop, placement.belowLeft); + return { coordinate: new Coordinate(placement.belowTop, placement.belowLeft), position: ContentWidgetPositionPreference.BELOW }; } } else { if (this.allowEditorOverflow) { - return this._prepareRenderWidgetAtExactPositionOverflowing(topLeft); + return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(topLeft), position: ContentWidgetPositionPreference.EXACT }; } else { - return topLeft; + return { coordinate: topLeft, position: ContentWidgetPositionPreference.EXACT }; } } } @@ -509,16 +525,20 @@ class Widget { this._isVisible = false; this.domNode.setVisibility('hidden'); } + + if (typeof this._actual.afterRender === 'function') { + safeInvoke(this._actual.afterRender, this._actual, null); + } return; } // This widget should be visible if (this.allowEditorOverflow) { - this.domNode.setTop(this._renderData.top); - this.domNode.setLeft(this._renderData.left); + this.domNode.setTop(this._renderData.coordinate.top); + this.domNode.setLeft(this._renderData.coordinate.left); } else { - this.domNode.setTop(this._renderData.top + ctx.scrollTop - ctx.bigNumbersDelta); - this.domNode.setLeft(this._renderData.left); + this.domNode.setTop(this._renderData.coordinate.top + ctx.scrollTop - ctx.bigNumbersDelta); + this.domNode.setLeft(this._renderData.coordinate.left); } if (!this._isVisible) { @@ -526,5 +546,18 @@ class Widget { this.domNode.setAttribute('monaco-visible-content-widget', 'true'); this._isVisible = true; } + + if (typeof this._actual.afterRender === 'function') { + safeInvoke(this._actual.afterRender, this._actual, this._renderData.position); + } + } +} + +function safeInvoke any>(fn: T, thisArg: ThisParameterType, ...args: Parameters): ReturnType | null { + try { + return fn.call(thisArg, ...args); + } catch { + // ignore + return null; } } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index faad9ab6da5..0ab1352cfb0 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -553,6 +553,11 @@ export interface CompletionList { suggestions: CompletionItem[]; incomplete?: boolean; dispose?(): void; + + /** + * @internal + */ + duration?: number; } /** @@ -1293,6 +1298,12 @@ export interface FoldingContext { * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { + + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChange?: Event; + /** * Provides the folding ranges for a specific model. */ diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index 5a8391e91af..69592e735c6 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -23,6 +23,7 @@ import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerSe import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import * as types from 'vs/base/common/types'; import { EditorWorkerHost } from 'vs/editor/common/services/editorWorkerServiceImpl'; +import { StopWatch } from 'vs/base/common/stopwatch'; export interface IMirrorModel { readonly uri: URI; @@ -529,13 +530,13 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { private static readonly _suggestionsLimit = 10000; - public async textualSuggest(modelUrl: string, position: IPosition, wordDef: string, wordDefFlags: string): Promise { + public async textualSuggest(modelUrl: string, position: IPosition, wordDef: string, wordDefFlags: string): Promise<{ words: string[], duration: number } | null> { const model = this._getModel(modelUrl); if (!model) { return null; } - + const sw = new StopWatch(true); const words: string[] = []; const seen = new Set(); const wordDefRegExp = new RegExp(wordDef, wordDefFlags); @@ -558,7 +559,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { break; } } - return words; + return { words, duration: sw.elapsed() }; } diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index 3ee2f5ad182..5976df3c226 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -161,20 +161,21 @@ class WordBasedCompletionItemProvider implements modes.CompletionItemProvider { const insert = replace.setEndPosition(position.lineNumber, position.column); const client = await this._workerManager.withWorker(); - const words = await client.textualSuggest(model.uri, position); - if (!words) { + const data = await client.textualSuggest(model.uri, position); + if (!data) { return undefined; } return { - suggestions: words.map((word): modes.CompletionItem => { + duration: data.duration, + suggestions: data.words.map((word): modes.CompletionItem => { return { kind: modes.CompletionItemKind.Text, label: word, insertText: word, range: { insert, replace } }; - }) + }), }; } } @@ -462,7 +463,7 @@ export class EditorWorkerClient extends Disposable { }); } - public textualSuggest(resource: URI, position: IPosition): Promise { + public textualSuggest(resource: URI, position: IPosition): Promise<{ words: string[], duration: number } | null> { return this._withSyncedResources([resource]).then(proxy => { let model = this._modelService.getModel(resource); if (!model) { diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/lightBulbWidget.css index aadcb4fd6e6..29c4fe0c043 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.css @@ -3,18 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .lightbulb-glyph, -.monaco-editor .codicon-lightbulb { +.monaco-editor .contentWidgets .codicon-light-bulb, +.monaco-editor .contentWidgets .codicon-lightbulb-autofix { display: flex; align-items: center; justify-content: center; - height: 16px; - width: 20px; - padding-left: 2px; } -.monaco-editor .lightbulb-glyph:hover, -.monaco-editor .codicon-lightbulb:hover { +.monaco-editor .contentWidgets .codicon-light-bulb:hover, +.monaco-editor .contentWidgets .codicon-lightbulb-autofix:hover { cursor: pointer; - /* transform: scale(1.3, 1.3); */ } diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts index 5caf1cd6a60..8eac58026d9 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -16,7 +16,7 @@ import * as nls from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorLightBulbForeground, editorLightBulbAutoFixForeground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { Gesture } from 'vs/base/browser/touch'; import type { CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; import { Codicon } from 'vs/base/common/codicons'; @@ -204,8 +204,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { private _updateLightBulbTitleAndIcon(): void { if (this.state.type === LightBulbState.Type.Showing && this.state.actions.hasAutoFix) { // update icon - dom.removeClasses(this._domNode, Codicon.lightBulb.classNames); - dom.addClasses(this._domNode, Codicon.lightbulbAutofix.classNames); + this._domNode.classList.remove(...Codicon.lightBulb.classNamesArray); + this._domNode.classList.add(...Codicon.lightbulbAutofix.classNamesArray); const preferredKb = this._keybindingService.lookupKeybinding(this._preferredFixActionId); if (preferredKb) { @@ -215,8 +215,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { } // update icon - dom.removeClasses(this._domNode, Codicon.lightbulbAutofix.classNames); - dom.addClasses(this._domNode, Codicon.lightBulb.classNames); + this._domNode.classList.remove(...Codicon.lightbulbAutofix.classNamesArray); + this._domNode.classList.add(...Codicon.lightBulb.classNamesArray); const kb = this._keybindingService.lookupKeybinding(this._quickFixActionId); if (kb) { @@ -233,12 +233,15 @@ export class LightBulbWidget extends Disposable implements IContentWidget { registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const editorBackgroundColor = theme.getColor(editorBackground)?.transparent(0.7); + // Lightbulb Icon const editorLightBulbForegroundColor = theme.getColor(editorLightBulbForeground); if (editorLightBulbForegroundColor) { collector.addRule(` .monaco-editor .contentWidgets ${Codicon.lightBulb.cssSelector} { color: ${editorLightBulbForegroundColor}; + background-color: ${editorBackgroundColor}; }`); } @@ -248,6 +251,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = collector.addRule(` .monaco-editor .contentWidgets ${Codicon.lightbulbAutofix.cssSelector} { color: ${editorLightBulbAutoFixForegroundColor}; + background-color: ${editorBackgroundColor}; }`); } diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index b65d6029cfd..4f7b574cd5a 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -473,7 +473,7 @@ export class StartFindAction extends MultiEditorAction { id: FIND_IDS.StartFindAction, label: nls.localize('startFindAction', "Find"), alias: 'Find', - precondition: ContextKeyExpr.has('editorIsOpen'), + precondition: ContextKeyExpr.or(ContextKeyExpr.has('editorFocus'), ContextKeyExpr.has('editorIsOpen')), kbOpts: { kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_F, @@ -722,7 +722,7 @@ export class StartFindReplaceAction extends MultiEditorAction { id: FIND_IDS.StartFindReplaceAction, label: nls.localize('startReplace', "Replace"), alias: 'Replace', - precondition: ContextKeyExpr.has('editorIsOpen'), + precondition: ContextKeyExpr.or(ContextKeyExpr.has('editorFocus'), ContextKeyExpr.has('editorIsOpen')), kbOpts: { kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_H, diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 251f7aa1607..0bfa5f68166 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -264,7 +264,7 @@ export class FoldingController extends Disposable implements IEditorContribution }, 30000); return rangeProvider; // keep memento in case there are still no foldingProviders on the next request. } else if (foldingProviders.length > 0) { - this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders); + this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders, () => this.onModelContentChanged()); } } this.foldingStateMemento = null; diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index 4e66801be36..898694bb2f8 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -9,6 +9,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { RangeProvider } from './folding'; import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; const MAX_FOLDING_REGIONS = 5000; @@ -25,7 +26,17 @@ export class SyntaxRangeProvider implements RangeProvider { readonly id = ID_SYNTAX_PROVIDER; - constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], private limit = MAX_FOLDING_REGIONS) { + readonly disposables: DisposableStore | undefined; + + constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], handleFoldingRangesChange: () => void, private limit = MAX_FOLDING_REGIONS) { + for (const provider of providers) { + if (typeof provider.onDidChange === 'function') { + if (!this.disposables) { + this.disposables = new DisposableStore(); + } + this.disposables.add(provider.onDidChange(handleFoldingRangesChange)); + } + } } compute(cancellationToken: CancellationToken): Promise { @@ -39,8 +50,8 @@ export class SyntaxRangeProvider implements RangeProvider { } dispose() { + this.disposables?.dispose(); } - } function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextModel, cancellationToken: CancellationToken): Promise { diff --git a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts index 86a4280259e..915ba173661 100644 --- a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts @@ -74,7 +74,7 @@ suite('Syntax folding', () => { let providers = [new TestFoldingRangeProvider(model, ranges)]; async function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) { - let indentRanges = await new SyntaxRangeProvider(model, providers, maxEntries).compute(CancellationToken.None); + let indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, maxEntries).compute(CancellationToken.None); let actual: IndentRange[] = []; if (indentRanges) { for (let i = 0; i < indentRanges.length; i++) { diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 7c21c932e17..ad343d031b4 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -209,7 +209,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { private _previewNotAvailableMessage!: TextModel; private _previewContainer!: HTMLElement; private _messageContainer!: HTMLElement; - private _dim: dom.Dimension = { height: 0, width: 0 }; + private _dim = new dom.Dimension(0, 0); constructor( editor: ICodeEditor, @@ -406,7 +406,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { super._doLayoutBody(heightInPixel, widthInPixel); - this._dim = { height: heightInPixel, width: widthInPixel }; + this._dim = new dom.Dimension(widthInPixel, heightInPixel); this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines; this._splitView.layout(widthInPixel); this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index e7ef8cad81a..2d0d4bcd349 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -192,7 +192,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } const multiple = hints.signatures.length > 1; - dom.toggleClass(this.domNodes.element, 'multiple', multiple); + this.domNodes.element.classList.toggle('multiple', multiple); this.keyMultipleSignatures.set(multiple); this.domNodes.signature.innerText = ''; @@ -243,8 +243,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { const hasDocs = this.hasDocs(signature, activeParameter); - dom.toggleClass(this.domNodes.signature, 'has-docs', hasDocs); - dom.toggleClass(this.domNodes.docs, 'empty', !hasDocs); + this.domNodes.signature.classList.toggle('has-docs', hasDocs); + this.domNodes.docs.classList.toggle('empty', !hasDocs); this.domNodes.overloads.textContent = String(hints.activeSignature + 1).padStart(hints.signatures.length.toString().length, '0') + '/' + hints.signatures.length; diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index ead11ae7087..7a7908e600c 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -10,6 +10,7 @@ import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; import { CharCode } from 'vs/base/common/charCode'; import { compareIgnoreCase } from 'vs/base/common/strings'; +import { MovingAverage } from 'vs/base/common/numbers'; type StrictCompletionItem = Required; @@ -25,6 +26,7 @@ export interface ICompletionStats { suggestionCount: number; snippetCount: number; textCount: number; + avgLabelLen: MovingAverage; [name: string]: any; } @@ -147,7 +149,7 @@ export class CompletionModel { private _createCachedState(): void { this._providerInfo = new Map(); - this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0 }; + this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0, avgLabelLen: new MovingAverage() }; const { leadingLineContent, characterCountDelta } = this._lineContext; let word = ''; @@ -183,6 +185,8 @@ export class CompletionModel { wordLow = word.toLowerCase(); } + const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name; + // remember the word against which this item was // scored item.word = word; @@ -208,7 +212,6 @@ export class CompletionModel { } } - const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name; if (wordPos >= wordLen) { // the wordPos at which scoring starts is the whole word // and therefore the same rules as not having a word apply @@ -248,6 +251,7 @@ export class CompletionModel { target.push(item as StrictCompletionItem); // update stats + this._stats.avgLabelLen.update(textLabel.length); this._stats.suggestionCount++; switch (item.completion.kind) { case CompletionItemKind.Snippet: this._stats.snippetCount++; break; diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 4908df1a1d7..8ad1eb61d0c 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -6,114 +6,54 @@ /* Suggest widget*/ .monaco-editor .suggest-widget { - width: 430px; /** Initial widths **/ + width: 430px; z-index: 40; display: flex; - flex-wrap: nowrap; - flex-direction: row; -} - -.monaco-editor .suggest-widget.docs-side { - flex-direction: row; -} - -.monaco-editor .suggest-widget.docs-side.reverse { - flex-direction: row-reverse; -} - -.monaco-editor .suggest-widget.docs-below { flex-direction: column; } -.monaco-editor .suggest-widget.docs-below.reverse { - flex-direction: column-reverse; +.monaco-editor .suggest-widget.message { + flex-direction: row; + align-items: center; } -/* --- fiddle with details margin so that borders merge/overlap */ -.monaco-editor .suggest-widget.docs-side>.details { - margin: 0 0 0 -1px; -} -.monaco-editor .suggest-widget.docs-side.reverse>.details { - margin: 0 -1px 0 0; -} -.monaco-editor.hc-black .suggest-widget.docs-side>.details { - margin: 0 0 0 -2px; -} -.monaco-editor.hc-black .suggest-widget.docs-side.reverse>.details { - margin: 0 -2px 0 0; -} - -.monaco-editor .suggest-widget>.message, -.monaco-editor .suggest-widget>.tree, -.monaco-editor .suggest-widget>.details { +.monaco-editor .suggest-widget, +.monaco-editor .suggest-details { flex: 0 1 auto; width: 100%; border-style: solid; border-width: 1px; } -.monaco-editor .suggest-widget>.tree.docs-higher { - align-self: flex-end; -} - -.monaco-editor.hc-black .suggest-widget>.message, -.monaco-editor.hc-black .suggest-widget>.tree, -.monaco-editor.hc-black .suggest-widget>.details { +.monaco-editor.hc-black .suggest-widget, +.monaco-editor.hc-black .suggest-details { border-width: 2px; } -/** Adjust width when docs are expanded to the side **/ - -.monaco-editor .suggest-widget.docs-side { - width: 660px; -} - -.monaco-editor .suggest-widget.docs-side>.tree, -.monaco-editor .suggest-widget.docs-side>.details { - width: 50%; -} - -/* MarkupContent Layout */ - -.monaco-editor .suggest-widget>.details ul { - padding-left: 20px; -} - -.monaco-editor .suggest-widget>.details ol { - padding-left: 20px; -} - -.monaco-editor .suggest-widget>.details p code { - font-family: var(--monaco-monospace-font); -} - - /* Styles for status bar part */ .monaco-editor .suggest-widget .suggest-status-bar { - visibility: hidden; box-sizing: border-box; - display: flex; + display: none; flex-flow: row nowrap; justify-content: space-between; width: 100%; font-size: 80%; - padding: 0 8px 0 4px; + padding: 0 4px 0 4px; border-top: 1px solid transparent; -} - -.monaco-editor .suggest-widget.list-right.docs-side .suggest-status-bar { - left: auto; - right: 0; + overflow: hidden; } .monaco-editor .suggest-widget.with-status-bar .suggest-status-bar { - visibility: visible; + display: flex; +} + +.monaco-editor .suggest-widget .suggest-status-bar .left { + padding-right: 8px; } .monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-label { - min-height: 18px; opacity: 0.5; color: inherit; } @@ -146,6 +86,7 @@ .monaco-editor .suggest-widget>.tree { height: 100%; + width: 100%; } .monaco-editor .suggest-widget .monaco-list { @@ -193,7 +134,7 @@ /** ReadMore Icon styles **/ -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close, +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close, .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore::before { color: inherit; opacity: 1; @@ -201,13 +142,14 @@ cursor: pointer; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close { position: absolute; top: 6px; right: 2px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close:hover, .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore:hover { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close:hover, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore:hover { opacity: 1; } @@ -248,17 +190,18 @@ /** Details: if using CompletionItem#details, show on focus **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label, .monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label { display: none; } -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { +.monaco-editor .suggest-widget:not(.shows-details) .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { display: inline; } /** Details: if using CompletionItemLabel#details, always show **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label)>.contents>.main>.right>.details-label, .monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused:not(.string-label)>.contents>.main>.right>.details-label { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label)>.contents>.main>.right>.details-label, +.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused:not(.string-label)>.contents>.main>.right>.details-label { display: inline; } @@ -288,8 +231,8 @@ .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right { overflow: hidden; - flex-shrink: 2; - max-width: 77%; + flex-shrink: 4; + max-width: 70%; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore { @@ -319,7 +262,8 @@ display: inline-block; } -.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore, .monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore { +.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore, +.monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore { display: none; } @@ -376,39 +320,32 @@ /** Styles for the docs of the completion item in focus **/ -.monaco-editor .suggest-widget .details { +.monaco-editor .suggest-details-container { + z-index: 41; +} + +.monaco-editor .suggest-details { display: flex; flex-direction: column; cursor: default; } -.monaco-editor .suggest-widget .details.no-docs { +.monaco-editor .suggest-details.no-docs { display: none; } -.monaco-editor .suggest-widget.docs-below .details { - border-top-width: 0; - border-bottom-width: 1px; -} - -.monaco-editor .suggest-widget.docs-below.reverse .details { - border-bottom-width: 0; - border-top-width: 1px; -} - -.monaco-editor .suggest-widget .details>.monaco-scrollable-element { +.monaco-editor .suggest-details>.monaco-scrollable-element { flex: 1; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body { - position: absolute; +.monaco-editor .suggest-details>.monaco-scrollable-element>.body { box-sizing: border-box; height: 100%; width: 100%; padding-right: 22px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.type { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.type { flex: 2; overflow: hidden; text-overflow: ellipsis; @@ -418,44 +355,57 @@ padding: 4px 0 12px 5px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs { margin: 0; padding: 4px 5px; white-space: pre-wrap; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs { padding: 0; white-space: initial; min-height: calc(1rem + 8px); } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div, .monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty) { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div, +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty) { padding: 4px 5px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child { margin-top: 0; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child { margin-bottom: 0; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs .code { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs .code { white-space: pre-wrap; word-wrap: break-word; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs .codicon { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs .codicon { vertical-align: sub; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>p:empty { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>p:empty { display: none; } -.monaco-editor .suggest-widget .details code { +.monaco-editor .suggest-details code { border-radius: 3px; padding: 0 0.4em; } + +.monaco-editor .suggest-details ul { + padding-left: 20px; +} + +.monaco-editor .suggest-details ol { + padding-left: 20px; +} + +.monaco-editor .suggest-details p code { + font-family: var(--monaco-monospace-font); +} diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts new file mode 100644 index 00000000000..6679c3bd4b2 --- /dev/null +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Dimension } from 'vs/base/browser/dom'; +import { Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash'; + + +export interface IResizeEvent { + dimension: Dimension; + done: boolean; + north?: boolean; + east?: boolean; + south?: boolean; + west?: boolean; +} + +export class ResizableHTMLElement { + + readonly domNode: HTMLElement; + + private readonly _onDidWillResize = new Emitter(); + readonly onDidWillResize: Event = this._onDidWillResize.event; + + private readonly _onDidResize = new Emitter(); + readonly onDidResize: Event = this._onDidResize.event; + + private readonly _northSash: Sash; + private readonly _eastSash: Sash; + private readonly _southSash: Sash; + private readonly _westSash: Sash; + private readonly _sashListener = new DisposableStore(); + + private _size = new Dimension(0, 0); + private _minSize = new Dimension(0, 0); + private _maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + private _preferredSize?: Dimension; + + constructor() { + this.domNode = document.createElement('div'); + this._northSash = new Sash(this.domNode, { getHorizontalSashTop: () => 0 }, { orientation: Orientation.HORIZONTAL }); + this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: Orientation.VERTICAL }); + this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: Orientation.HORIZONTAL }); + this._westSash = new Sash(this.domNode, { getVerticalSashLeft: () => 0 }, { orientation: Orientation.VERTICAL }); + + this._northSash.orthogonalStartSash = this._westSash; + this._northSash.orthogonalEndSash = this._eastSash; + this._eastSash.orthogonalStartSash = this._northSash; + this._eastSash.orthogonalEndSash = this._southSash; + this._southSash.orthogonalStartSash = this._westSash; + this._southSash.orthogonalEndSash = this._eastSash; + this._westSash.orthogonalStartSash = this._northSash; + this._westSash.orthogonalEndSash = this._southSash; + + let currentSize: Dimension | undefined; + let deltaY = 0; + let deltaX = 0; + + this._sashListener.add(Event.any(this._northSash.onDidStart, this._eastSash.onDidStart, this._southSash.onDidStart, this._westSash.onDidStart)(() => { + if (currentSize === undefined) { + this._onDidWillResize.fire(); + currentSize = this._size; + deltaY = 0; + deltaX = 0; + } + })); + this._sashListener.add(Event.any(this._northSash.onDidEnd, this._eastSash.onDidEnd, this._southSash.onDidEnd, this._westSash.onDidEnd)(() => { + if (currentSize !== undefined) { + currentSize = undefined; + deltaY = 0; + deltaX = 0; + this._onDidResize.fire({ dimension: this._size, done: true }); + } + })); + + this._sashListener.add(this._eastSash.onDidChange(e => { + if (currentSize) { + deltaX = e.currentX - e.startX; + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimension: this._size, done: false, east: true }); + } + })); + this._sashListener.add(this._westSash.onDidChange(e => { + if (currentSize) { + deltaX = -(e.currentX - e.startX); + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimension: this._size, done: false, west: true }); + } + })); + this._sashListener.add(this._northSash.onDidChange(e => { + if (currentSize) { + deltaY = -(e.currentY - e.startY); + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimension: this._size, done: false, north: true }); + } + })); + this._sashListener.add(this._southSash.onDidChange(e => { + if (currentSize) { + deltaY = e.currentY - e.startY; + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimension: this._size, done: false, south: true }); + } + })); + + this._sashListener.add(Event.any(this._eastSash.onDidReset, this._westSash.onDidReset)(e => { + if (this._preferredSize) { + this.layout(this._size.height, this._preferredSize.width); + this._onDidResize.fire({ dimension: this._size, done: true }); + } + })); + this._sashListener.add(Event.any(this._northSash.onDidReset, this._southSash.onDidReset)(e => { + if (this._preferredSize) { + this.layout(this._preferredSize.height, this._size.width); + this._onDidResize.fire({ dimension: this._size, done: true }); + } + })); + } + + dispose(): void { + this._northSash.dispose(); + this._southSash.dispose(); + this._eastSash.dispose(); + this._westSash.dispose(); + this._sashListener.dispose(); + this.domNode.remove(); + } + + enableSashes(north: boolean, east: boolean, south: boolean, west: boolean): void { + this._northSash.state = north ? SashState.Enabled : SashState.Disabled; + this._eastSash.state = east ? SashState.Enabled : SashState.Disabled; + this._southSash.state = south ? SashState.Enabled : SashState.Disabled; + this._westSash.state = west ? SashState.Enabled : SashState.Disabled; + } + + layout(height: number = this.size.height, width: number = this.size.width): void { + + const { height: minHeight, width: minWidth } = this._minSize; + const { height: maxHeight, width: maxWidth } = this._maxSize; + + height = Math.max(minHeight, Math.min(maxHeight, height)); + width = Math.max(minWidth, Math.min(maxWidth, width)); + + const newSize = new Dimension(width, height); + if (!Dimension.equals(newSize, this._size)) { + this.domNode.style.height = height + 'px'; + this.domNode.style.width = width + 'px'; + this._size = newSize; + this._northSash.layout(); + this._eastSash.layout(); + this._southSash.layout(); + this._westSash.layout(); + } + } + + get size() { + return this._size; + } + + set maxSize(value: Dimension) { + this._maxSize = value; + } + + get maxSize() { + return this._maxSize; + } + + set minSize(value: Dimension) { + this._minSize = value; + } + + get minSize() { + return this._minSize; + } + + set preferredSize(value: Dimension | undefined) { + this._preferredSize = value; + } + + get preferredSize() { + return this._preferredSize; + } +} diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 1e855db7505..d12b0f7d117 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -17,6 +17,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { isDisposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { MenuId } from 'vs/platform/actions/common/actions'; import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { StopWatch } from 'vs/base/common/stopwatch'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false), @@ -164,10 +165,22 @@ export function setSnippetSuggestSupport(support: modes.CompletionItemProvider): return old; } -class CompletionItemModel { +export interface CompletionDurationEntry { + readonly providerName: string; + readonly elapsedProvider: number; + readonly elapsedOverall: number; +} + +export interface CompletionDurations { + readonly entries: readonly CompletionDurationEntry[]; + readonly elapsed: number; +} + +export class CompletionItemModel { constructor( readonly items: CompletionItem[], readonly needsClipboard: boolean, + readonly durations: CompletionDurations, readonly disposable: IDisposable, ) { } } @@ -180,7 +193,7 @@ export async function provideSuggestionItems( token: CancellationToken = CancellationToken.None ): Promise { - // const t1 = Date.now(); + const sw = new StopWatch(true); position = position.clone(); const word = model.getWordAtPosition(position); @@ -189,9 +202,10 @@ export async function provideSuggestionItems( const result: CompletionItem[] = []; const disposables = new DisposableStore(); + const durations: CompletionDurationEntry[] = []; let needsClipboard = false; - const onCompletionList = (provider: modes.CompletionItemProvider, container: modes.CompletionList | null | undefined) => { + const onCompletionList = (provider: modes.CompletionItemProvider, container: modes.CompletionList | null | undefined, sw: StopWatch) => { if (!container) { return; } @@ -214,6 +228,9 @@ export async function provideSuggestionItems( if (isDisposable(container)) { disposables.add(container); } + durations.push({ + providerName: provider._debugDisplayName ?? 'unkown_provider', elapsedProvider: container.duration ?? -1, elapsedOverall: sw.elapsed() + }); }; // ask for snippets in parallel to asking "real" providers. Only do something if configured to @@ -225,8 +242,9 @@ export async function provideSuggestionItems( if (options.providerFilter.size > 0 && !options.providerFilter.has(_snippetSuggestSupport)) { return; } + const sw = new StopWatch(true); const list = await _snippetSuggestSupport.provideCompletionItems(model, position, context, token); - onCompletionList(_snippetSuggestSupport, list); + onCompletionList(_snippetSuggestSupport, list, sw); })(); // add suggestions from contributed providers - providers are ordered in groups of @@ -242,8 +260,9 @@ export async function provideSuggestionItems( return; } try { + const sw = new StopWatch(true); const list = await provider.provideCompletionItems(model, position, context, token); - onCompletionList(provider, list); + onCompletionList(provider, list, sw); } catch (err) { onUnexpectedExternalError(err); } @@ -260,11 +279,12 @@ export async function provideSuggestionItems( disposables.dispose(); return Promise.reject(canceled()); } - // console.log(`${result.length} items AFTER ${Date.now() - t1}ms`); + return new CompletionItemModel( result.sort(getSuggestionComparator(options.snippetSortOrder)), needsClipboard, - disposables + { entries: durations, elapsed: sw.elapsed() }, + disposables, ); } diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 58739e3d3f3..43d10699e11 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -30,7 +30,6 @@ import { State, SuggestModel } from './suggestModel'; import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; import { WordContextKey } from 'vs/editor/contrib/suggest/wordContextKey'; import { Event } from 'vs/base/common/event'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IdleValue } from 'vs/base/common/async'; import { isObject, assertType } from 'vs/base/common/types'; import { CommitCharacterController } from './suggestCommitCharacters'; @@ -43,7 +42,6 @@ import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // sticky suggest widget which doesn't disappear on focus out and such let _sticky = false; @@ -117,16 +115,14 @@ export class SuggestController implements IEditorContribution { constructor( editor: ICodeEditor, - @IEditorWorkerService editorWorker: IEditorWorkerService, @ISuggestMemoryService private readonly _memoryService: ISuggestMemoryService, @ICommandService private readonly _commandService: ICommandService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, - @IClipboardService clipboardService: IClipboardService, ) { this.editor = editor; - this.model = new SuggestModel(this.editor, editorWorker, clipboardService); + this.model = _instantiationService.createInstance(SuggestModel, this.editor,); this.widget = this._toDispose.add(new IdleValue(() => { @@ -649,19 +645,19 @@ KeybindingsRegistry.registerKeybindingRule({ }); MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { - command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.accept', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") }, + command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") }, group: 'left', order: 1, when: SuggestContext.HasInsertAndReplaceRange.toNegated() }); MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { - command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.insert', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") }, + command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") }, group: 'left', order: 1, when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'insert')) }); MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { - command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.replace', comment: ['{0} will be a keybinding, e.g "Enter to replace"'] }, "{0} to replace") }, + command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.replace', "Replace") }, group: 'left', order: 1, when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'replace')) @@ -684,13 +680,13 @@ registerEditorCommand(new SuggestCommand({ group: 'left', order: 2, when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'insert')), - title: nls.localize({ key: 'accept.replace', comment: ['{0} will be a keybinding, e.g "Enter to replace"'] }, "{0} to replace") + title: nls.localize('accept.replace', "Replace") }, { menuId: suggestWidgetStatusbarMenu, group: 'left', order: 2, when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'replace')), - title: nls.localize({ key: 'accept.insert', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") + title: nls.localize('accept.insert', "Insert") }] })); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index bb93c2aca5a..3540212bf35 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,7 +14,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; import { CompletionItemProvider, StandardTokenType, CompletionContext, CompletionProviderRegistry, CompletionTriggerKind, CompletionItemKind } from 'vs/editor/common/modes'; import { CompletionModel } from './completionModel'; -import { CompletionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport, SnippetSortOrder, CompletionOptions } from './suggest'; +import { CompletionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport, SnippetSortOrder, CompletionOptions, CompletionDurations } from './suggest'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -23,6 +22,9 @@ import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { isLowSurrogate, isHighSurrogate } from 'vs/base/common/strings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; export interface ICancelEvent { readonly retrigger: boolean; @@ -118,8 +120,10 @@ export class SuggestModel implements IDisposable { constructor( private readonly _editor: ICodeEditor, - private readonly _editorWorkerService: IEditorWorkerService, - private readonly _clipboardService: IClipboardService + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @IClipboardService private readonly _clipboardService: IClipboardService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, ) { this._currentSelection = this._editor.getSelection() || new Selection(1, 1, 1, 1); @@ -230,8 +234,10 @@ export class SuggestModel implements IDisposable { if (supports) { // keep existing items that where not computed by the // supports/providers that want to trigger now - const items = this._completionModel?.adopt(supports); - this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, items); + const existing = this._completionModel + ? { items: this._completionModel.adopt(supports), clipboardText: this._completionModel.clipboardText } + : undefined; + this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, existing); } }; @@ -248,10 +254,8 @@ export class SuggestModel implements IDisposable { cancel(retrigger: boolean = false): void { if (this._state !== State.Idle) { this._triggerQuickSuggest.cancel(); - if (this._requestToken) { - this._requestToken.cancel(); - this._requestToken = undefined; - } + this._requestToken?.cancel(); + this._requestToken = undefined; this._state = State.Idle; this._completionModel = undefined; this._context = undefined; @@ -376,7 +380,7 @@ export class SuggestModel implements IDisposable { }); } - trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set, existingItems?: CompletionItem[]): void { + trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set, existing?: { items: CompletionItem[], clipboardText: string | undefined }): void { if (!this._editor.hasModel()) { return; } @@ -420,10 +424,10 @@ export class SuggestModel implements IDisposable { break; } - let itemKindFilter = SuggestModel._createItemKindFilter(this._editor); - let wordDistance = WordDistance.create(this._editorWorkerService, this._editor); + const itemKindFilter = SuggestModel._createItemKindFilter(this._editor); + const wordDistance = WordDistance.create(this._editorWorkerService, this._editor); - let completions = provideSuggestionItems( + const completions = provideSuggestionItems( model, this._editor.getPosition(), new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom), @@ -443,17 +447,17 @@ export class SuggestModel implements IDisposable { return; } - let clipboardText: string | undefined; - if (completions.needsClipboard || isNonEmptyArray(existingItems)) { + let clipboardText = existing?.clipboardText; + if (!clipboardText && completions.needsClipboard) { clipboardText = await this._clipboardService.readText(); } const model = this._editor.getModel(); let items = completions.items; - if (isNonEmptyArray(existingItems)) { + if (existing) { const cmpFn = getSuggestionComparator(snippetSortOrder); - items = items.concat(existingItems).sort(cmpFn); + items = items.concat(existing.items).sort(cmpFn); } const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy); @@ -472,9 +476,42 @@ export class SuggestModel implements IDisposable { this._onNewContext(ctx); + // finally report telemetry about durations + this._reportDurationsTelemetry(completions.durations); + }).catch(onUnexpectedError); } + private _reportDurationsTelemetry(durations: CompletionDurations): void { + + type DurationEntry = { + session: string; + providerName: string; + elapsedProvider: number; + elapsedOverall: number; + }; + + type Durations = { + session: string; + elapsedAll: number; + }; + + type PerformanceAndHealth = { [P in keyof T]: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' } }; + type DurationEntryClassification = PerformanceAndHealth; + type DurationsClassification = PerformanceAndHealth; + + setTimeout(() => { + + this._logService.trace('suggest.durations', durations); + + const session = generateUuid(); + this._telemetryService.publicLog2('suggest.durations.all', { session, elapsedAll: durations.elapsed }); + for (let item of durations.entries) { + this._telemetryService.publicLog2('suggest.durations.entry', { session, ...item }); + } + }); + } + private static _createItemKindFilter(editor: ICodeEditor): Set { // kind filter and snippet sort rules const result = new Set(); @@ -563,15 +600,15 @@ export class SuggestModel implements IDisposable { inactiveProvider.delete(provider); } const items = this._completionModel.adopt(new Set()); - this.trigger({ auto: this._context.auto, shy: false }, true, inactiveProvider, items); + this.trigger({ auto: this._context.auto, shy: false }, true, inactiveProvider, { items, clipboardText: this._completionModel.clipboardText }); return; } if (ctx.column > this._context.column && this._completionModel.incomplete.size > 0 && ctx.leadingWord.word.length !== 0) { // typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger const { incomplete } = this._completionModel; - const adopted = this._completionModel.adopt(incomplete); - this.trigger({ auto: this._state === State.Auto, shy: false, triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions }, true, incomplete, adopted); + const items = this._completionModel.adopt(incomplete); + this.trigger({ auto: this._state === State.Auto, shy: false, triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions }, true, incomplete, { items, clipboardText: this._completionModel.clipboardText }); } else { // typed -> moved cursor RIGHT -> update UI diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 5d42225f42a..8dca6d145c0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -8,13 +8,12 @@ import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; +import * as dom from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { append, $, hide, show, getDomNodePagePosition, addDisposableListener, addStandardDisposableListener } from 'vs/base/browser/dom'; -import { IListVirtualDelegate, IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list'; +import { IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; @@ -25,19 +24,15 @@ import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, editorWidgetBackground, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { SuggestionDetails, canExpandCompletionItem } from './suggestWidgetDetails'; +import { SuggestDetailsWidget, canExpandCompletionItem, SuggestDetailsOverlay } from './suggestWidgetDetails'; import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; - -const expandSuggestionDocsByDefault = false; - - +import { ResizableHTMLElement } from './resizable'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IPosition } from 'vs/editor/common/core/position'; /** * Suggest widget colors @@ -48,9 +43,6 @@ export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget. export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); - - - const enum State { Hidden, Loading, @@ -60,26 +52,47 @@ const enum State { Details } - export interface ISelectedSuggestion { item: CompletionItem; index: number; model: CompletionModel; } -export class SuggestWidget implements IContentWidget, IListVirtualDelegate, IDisposable { +class PersistedWidgetSize { - private static readonly ID: string = 'editor.widget.suggestWidget'; + private readonly _key: string; - static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading..."); - static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); + constructor( + private readonly _service: IStorageService, + editor: ICodeEditor + ) { + this._key = `suggestWidget.size/${editor.getEditorType()}/${editor instanceof EmbeddedCodeEditorWidget}`; + } - // Editor.IContentWidget.allowEditorOverflow - readonly allowEditorOverflow = true; - readonly suppressMouseDown = false; + restore(): dom.Dimension | undefined { + const raw = this._service.get(this._key, StorageScope.GLOBAL) ?? ''; + try { + const obj = JSON.parse(raw); + if (dom.Dimension.is(obj)) { + return dom.Dimension.lift(obj); + } + } catch { + // ignore + } + return undefined; + } + + store(size: dom.Dimension) { + this._service.store(this._key, JSON.stringify(size), StorageScope.GLOBAL); + } +} + +export class SuggestWidget implements IDisposable { + + private static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading..."); + private static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); private state: State = State.Hidden; - private isAddedAsContentWidget: boolean = false; private isAuto: boolean = false; private loadingTimeout: IDisposable = Disposable.None; private currentSuggestionDetails?: CancelablePromise; @@ -87,14 +100,13 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate; - private status: SuggestWidgetStatus; - private details: SuggestionDetails; - private listHeight?: number; + readonly element: ResizableHTMLElement; + private readonly messageElement: HTMLElement; + private readonly listElement: HTMLElement; + private readonly list: List; + private readonly status: SuggestWidgetStatus; + private readonly _details: SuggestDetailsOverlay; + private readonly _contentWidget: SuggestContentWidget; private readonly ctxSuggestWidgetVisible: IContextKey; private readonly ctxSuggestWidgetDetailsVisible: IContextKey; @@ -103,6 +115,8 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate(); private readonly onDidFocusEmitter = new Emitter(); private readonly onDidHideEmitter = new Emitter(); @@ -113,15 +127,9 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate = this.onDidHideEmitter.event; readonly onDidShow: Event = this.onDidShowEmitter.event; - private readonly maxWidgetWidth = 660; - private readonly listWidth = 330; private detailsFocusBorderColor?: string; private detailsBorderColor?: string; - private firstFocusInCurrentList: boolean = false; - - private preferDocPositionTop: boolean = false; - private docsPositionPreviousWidgetY?: number; private explainMode: boolean = false; private readonly _onDetailsKeydown = new Emitter(); @@ -129,42 +137,69 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - if (e.target === this.element) { - this.hideWidget(); + this._contentWidget = new SuggestContentWidget(this, editor); + this._persistedSize = new PersistedWidgetSize(_storageService, editor); + + let persistedSize: dom.Dimension | undefined; + let persistHeight = false; + let persistWidth = false; + this._disposables.add(this.element.onDidWillResize(() => { + this._contentWidget.lockPreference(); + persistedSize = this._persistedSize.restore(); + })); + this._disposables.add(this.element.onDidResize(e => { + this._layout(e.dimension); + persistHeight = persistHeight || !!e.north || !!e.south; + persistWidth = persistWidth || !!e.east || !!e.west; + if (e.done) { + + // only store width or height value that have changed + let { width, height } = this.element.size; + if (persistedSize) { + if (!persistHeight) { + height = persistedSize.height; + } + if (!persistWidth) { + width = persistedSize.width; + } + } + this._persistedSize.store(new dom.Dimension(width, height)); + + // reset working state + this._contentWidget.unlockPreference(); + persistedSize = undefined; + persistHeight = false; + persistWidth = false; } })); - this.messageElement = append(this.element, $('.message')); - this.mainElement = append(this.element, $('.tree')); + this.messageElement = dom.append(this.element.domNode, dom.$('.message')); + this.listElement = dom.append(this.element.domNode, dom.$('.tree')); - this.details = instantiationService.createInstance(SuggestionDetails, this.element, this.editor, markdownRenderer, kbToggleDetails); - this.details.onDidClose(this.toggleDetails, this, this._disposables); - hide(this.details.element); + const details = instantiationService.createInstance(SuggestDetailsWidget, this.editor); + details.onDidClose(this.toggleDetails, this, this._disposables); + this._details = new SuggestDetailsOverlay(details, this.editor); - const applyIconStyle = () => this.element.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons); + const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons); applyIconStyle(); - this.listContainer = append(this.mainElement, $('.list-container')); - - const renderer = instantiationService.createInstance(ItemRenderer, this.editor, kbToggleDetails); + const renderer = instantiationService.createInstance(ItemRenderer, this.editor); this._disposables.add(renderer); this._disposables.add(renderer.onDidToggleDetails(() => this.toggleDetails())); - this.list = new List('SuggestWidget', this.listContainer, this, [renderer], { + this.list = new List('SuggestWidget', this.listElement, { + getHeight: (_element: CompletionItem): number => this.getLayoutInfo().itemHeight, + getTemplateId: (_element: CompletionItem): string => 'suggestion' + }, [renderer], { useShadows: false, mouseSupport: false, accessibilityProvider: { @@ -188,16 +223,17 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate this.element.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); + this.status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode); + const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); applyStatusBarStyle(); - this._disposables.add(attachListStyler(this.list, themeService, { + this._disposables.add(attachListStyler(this.list, _themeService, { listInactiveFocusBackground: editorSuggestWidgetSelectedBackground, listInactiveFocusOutline: activeContrastBorder })); - this._disposables.add(themeService.onDidColorThemeChange(t => this.onThemeChange(t))); - this._disposables.add(editor.onDidLayoutChange(() => this.onEditorLayoutChange())); + this._disposables.add(_themeService.onDidColorThemeChange(t => this.onThemeChange(t))); + this.onThemeChange(_themeService.getColorTheme()); + this._disposables.add(this.list.onMouseDown(e => this.onListMouseDownOrTap(e))); this._disposables.add(this.list.onTap(e => this.onListMouseDownOrTap(e))); this._disposables.add(this.list.onDidChangeSelection(e => this.onListSelection(e))); @@ -210,27 +246,37 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { + this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => { this._onDetailsKeydown.fire(e); })); this._disposables.add(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e))); } + dispose(): void { + this._details.widget.dispose(); + this._details.dispose(); + this.list.dispose(); + this.status.dispose(); + this._disposables.dispose(); + this.loadingTimeout.dispose(); + this.showTimeout.dispose(); + this._contentWidget.dispose(); + this.element.dispose(); + } + private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { - // Clicking inside details - if (this.details.element.contains(mouseEvent.target.element)) { - this.details.element.focus(); - } - // Clicking outside details and inside suggest - else { - if (this.element.contains(mouseEvent.target.element)) { + if (this._details.widget.domNode.contains(mouseEvent.target.element)) { + // Clicking inside details + this._details.widget.domNode.focus(); + } else { + // Clicking outside details and inside suggest + if (this.element.domNode.contains(mouseEvent.target.element)) { this.editor.focus(); } } @@ -238,13 +284,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate): void { @@ -324,7 +364,6 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - const loading = disposableTimeout(() => this.showDetails(true), 250); + const loading = disposableTimeout(() => { + if (this._isDetailsVisible()) { + this.showDetails(true); + } + }, 250); token.onCancellationRequested(() => loading.dispose()); const result = await item.resolve(token); loading.dispose(); @@ -356,7 +399,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { + this.element.domNode.classList.add('visible'); + this.onDidShowEmitter.fire(this); + }, 100); + } + showTriggered(auto: boolean, delay: number) { if (this.state !== State.Hidden) { return; } - + this._contentWidget.setPosition(this.editor.getPosition()); this.isAuto = !!auto; if (!this.isAuto) { - this.loadingTimeout = disposableTimeout(() => this.setState(State.Loading), delay); + this.loadingTimeout = disposableTimeout(() => this._setState(State.Loading), delay); } } showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean): void { - this.preferDocPositionTop = false; - this.docsPositionPreviousWidgetY = undefined; + this._contentWidget.setPosition(this.editor.getPosition()); this.loadingTimeout.dispose(); this.currentSuggestionDetails?.cancel(); @@ -452,56 +504,44 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate 1); if (isEmpty) { - if (isAuto) { - this.setState(State.Hidden); - } else { - this.setState(State.Empty); - } - + this._setState(isAuto ? State.Hidden : State.Empty); this.completionModel = undefined; + return; + } - } else { + if (this.state !== State.Open) { + const { stats } = this.completionModel; + stats['wasAutomaticallyTriggered'] = !!isAuto; + /* __GDPR__ + "suggestWidget" : { + "wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${ICompletionStats}" + ] + } + */ + this._telemetryService.publicLog('suggestWidget', { ...stats }); + } - if (this.state !== State.Open) { - const { stats } = this.completionModel; - stats['wasAutomaticallyTriggered'] = !!isAuto; - /* __GDPR__ - "suggestWidget" : { - "wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${ICompletionStats}" - ] - } - */ - this.telemetryService.publicLog('suggestWidget', { ...stats }); - } + this.focusedItem = undefined; + this.list.splice(0, this.list.length, this.completionModel.items); + this._setState(isFrozen ? State.Frozen : State.Open); + this.list.reveal(selectionIndex, 0); + this.list.setFocus([selectionIndex]); - this.focusedItem = undefined; - this.list.splice(0, this.list.length, this.completionModel.items); - - if (isFrozen) { - this.setState(State.Frozen); - } else { - this.setState(State.Open); - } - - this.list.reveal(selectionIndex, 0); - this.list.setFocus([selectionIndex]); - - // Reset focus border - if (this.detailsBorderColor) { - this.details.element.style.borderColor = this.detailsBorderColor; - } + this._layout(this.element.size); + // Reset focus border + if (this.detailsBorderColor) { + this._details.widget.domNode.style.borderColor = this.detailsBorderColor; } } @@ -510,7 +550,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - this.element.classList.add('visible'); - this.onDidShowEmitter.fire(this); - }, 100); - } - - private hide(): void { - // let the editor know that the widget is hidden - this.editor.layoutContentWidget(this); - this.ctxSuggestWidgetVisible.reset(); - this.ctxSuggestWidgetMultipleSuggestions.reset(); - this.element.classList.remove('visible'); - } - hideWidget(): void { this.loadingTimeout.dispose(); - this.setState(State.Hidden); + this._setState(State.Hidden); this.onDidHideEmitter.fire(this); } - getPosition(): IContentWidgetPosition | null { - if (this.state === State.Hidden) { - return null; - } - - let preference = [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]; - if (this.preferDocPositionTop) { - preference = [ContentWidgetPositionPreference.ABOVE]; - } - - return { - position: this.editor.getPosition(), - preference: preference - }; - } - - getDomNode(): HTMLElement { - return this.element; - } - - getId(): string { - return SuggestWidget.ID; - } - isFrozen(): boolean { return this.state === State.Frozen; } - private updateListHeight(): number { - let height = this.unfocusedHeight; - - if (this.state !== State.Empty && this.state !== State.Loading) { - const suggestionCount = this.list.contentHeight / this.unfocusedHeight; - const { maxVisibleSuggestions } = this.editor.getOption(EditorOption.suggest); - height = Math.min(suggestionCount, maxVisibleSuggestions) * this.unfocusedHeight; + _afterRender(position: ContentWidgetPositionPreference | null) { + if (position === null) { + if (this._isDetailsVisible()) { + this._details.hide(); //todo@jrieken soft-hide + } + return; } - - this.element.style.lineHeight = `${this.unfocusedHeight}px`; - this.listContainer.style.height = `${height}px`; - this.mainElement.style.height = `${height + (this.editor.getOption(EditorOption.suggest).statusBar.visible ? this.unfocusedHeight : 0)}px`; - this.list.layout(height); - return height; + if (this.state === State.Empty || this.state === State.Loading) { + // no special positioning when widget isn't showing list + return; + } + if (this._isDetailsVisible()) { + this._details.show(); + } + this._positionDetails(); } - - /** - * Adds the propert classes, margins when positioning the docs to the side - */ - private adjustDocsPosition() { + private _layout(size: dom.Dimension | undefined): void { if (!this.editor.hasModel()) { return; } - - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - const cursorCoords = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); - const editorCoords = getDomNodePagePosition(this.editor.getDomNode()); - const cursorX = editorCoords.left + cursorCoords.left; - const cursorY = editorCoords.top + cursorCoords.top + cursorCoords.height; - const widgetCoords = getDomNodePagePosition(this.element); - const widgetX = widgetCoords.left; - const widgetY = widgetCoords.top; - - // Fixes #27649 - // Check if the Y changed to the top of the cursor and keep the widget flagged to prefer top - if (this.docsPositionPreviousWidgetY !== undefined && - this.docsPositionPreviousWidgetY < widgetY && - !this.preferDocPositionTop - ) { - this.preferDocPositionTop = true; - this.adjustDocsPosition(); - return; - } - this.docsPositionPreviousWidgetY = widgetY; - - const aboveCursor = cursorY - lineHeight > widgetY; - const rowMode = this.element.classList.contains('docs-side'); - - // row mode: reverse doc/list when being too far right - // column mode: reverse doc/list when being too far down - this.element.classList.toggle( - 'reverse', - (rowMode && widgetX < cursorX - this.listWidth) || (!rowMode && aboveCursor) - ); - - // row mode: when detail is higher and when showing above the cursor then align - // the list at the bottom - this.mainElement.classList.toggle( - 'docs-higher', - rowMode && aboveCursor && this.details.element.offsetHeight > this.mainElement.offsetHeight - ); - } - - /** - * Adds the proper classes for positioning the docs to the side or below depending on item - */ - private expandSideOrBelow() { - if (!canExpandCompletionItem(this.focusedItem) && this.firstFocusInCurrentList) { - this.element.classList.remove('docs-side', 'docs-below'); + if (!this.editor.getDomNode()) { + // happens when running tests return; } - let matches = this.element.style.maxWidth.match(/(\d+)px/); - if (!matches || Number(matches[1]) < this.maxWidgetWidth) { - this.element.classList.add('docs-below'); - this.element.classList.remove('docs-side'); - } else if (canExpandCompletionItem(this.focusedItem)) { - this.element.classList.add('docs-side'); - this.element.classList.remove('docs-below'); + let height = size?.height; + let width = size?.width; + + const bodyBox = dom.getClientArea(document.body); + const { itemHeight, statusBarHeight, borderHeight, typicalHalfwidthCharacterWidth } = this.getLayoutInfo(); + + // status bar + this.status.element.style.lineHeight = `${itemHeight}px`; + + if (this.state === State.Empty || this.state === State.Loading) { + // showing a message only + height = itemHeight + borderHeight; + width = 230; + this.element.enableSashes(false, false, false, false); + this.element.minSize = this.element.maxSize = new dom.Dimension(width, height); + this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW); + + } else { + // showing items + + // width math + const maxWidth = bodyBox.width - borderHeight; + if (width === undefined) { + width = 430; + } + if (width > maxWidth) { + width = maxWidth; + } + const preferredWidth = this.completionModel ? this.completionModel.stats.avgLabelLen.value * typicalHalfwidthCharacterWidth : width; + + // height math + const fullHeight = statusBarHeight + this.list.contentHeight + borderHeight; + const preferredHeight = statusBarHeight + (itemHeight * this.editor.getOption(EditorOption.suggest).maxVisibleSuggestions) + borderHeight; + const minHeight = itemHeight + statusBarHeight; + const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); + const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); + const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height; + const maxHeightBelow = bodyBox.height - cursorBottom; + const maxHeightAbove = editorBox.top + cursorBox.top - 22 /*TOP_PADDING of contentWidget#_layoutBoxInPage*/; + let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) - borderHeight, fullHeight); + + + if (height === undefined) { + height = Math.min(preferredHeight, fullHeight); + } + if (height < minHeight) { + height = minHeight; + } + if (height > maxHeight) { + height = maxHeight; + } + + if (height > maxHeightBelow) { + this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE); + this.element.enableSashes(true, true, false, false); + maxHeight = maxHeightAbove; + + } else { + this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW); + this.element.enableSashes(false, true, true, false); + maxHeight = maxHeightBelow; + } + + this.list.layout(height - statusBarHeight, width); + this.listElement.style.height = `${height - statusBarHeight}px`; + + this.element.preferredSize = new dom.Dimension(preferredWidth, preferredHeight); + this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); + this.element.minSize = new dom.Dimension(220, minHeight); + } + + + this.element.layout(height, width); + this._contentWidget.layout(); + + this._positionDetails(); + } + + private _positionDetails(): void { + if (this._isDetailsVisible()) { + this._details.placeAtAnchor(this.element.domNode); } } - // Heights - - private get maxWidgetHeight(): number { - return this.unfocusedHeight * this.editor.getOption(EditorOption.suggest).maxVisibleSuggestions; - } - - private get unfocusedHeight(): number { - const options = this.editor.getOptions(); - return options.get(EditorOption.suggestLineHeight) || options.get(EditorOption.fontInfo).lineHeight; - } - - // IDelegate - - getHeight(_element: CompletionItem): number { - return this.unfocusedHeight; - } - - getTemplateId(_element: CompletionItem): string { - return 'suggestion'; + getLayoutInfo() { + const fontInfo = this.editor.getOption(EditorOption.fontInfo); + const itemHeight = this.editor.getOption(EditorOption.suggestLineHeight) || fontInfo.lineHeight; + const statusBarHeight = !this.editor.getOption(EditorOption.suggest).statusBar.visible || this.state === State.Empty || this.state === State.Loading ? 0 : itemHeight; + const borderWidth = this._details.widget.borderWidth; + const borderHeight = 2 * borderWidth; + return { itemHeight, statusBarHeight, borderWidth, borderHeight, typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth }; } private _isDetailsVisible(): boolean { - return this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault); + return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, false); } private _setDetailsVisible(value: boolean) { - this.storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL); + this._storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL); } +} + +export class SuggestContentWidget implements IContentWidget { + + readonly allowEditorOverflow = true; + readonly suppressMouseDown = false; + + private _position?: IPosition | null; + private _preference?: ContentWidgetPositionPreference; + private _preferenceLocked = false; + + private _added: boolean = false; + private _hidden: boolean = false; + + constructor( + private readonly _widget: SuggestWidget, + private readonly _editor: ICodeEditor + ) { } dispose(): void { - this.details.dispose(); - this.list.dispose(); - this.status.dispose(); - this._disposables.dispose(); - this.loadingTimeout.dispose(); - this.showTimeout.dispose(); - this.editor.removeContentWidget(this); + if (this._added) { + this._added = false; + this._editor.removeContentWidget(this); + } + } + + getId(): string { + return 'editor.widget.suggestWidget'; + } + + getDomNode(): HTMLElement { + return this._widget.element.domNode; + } + + show(): void { + this._hidden = false; + if (!this._added) { + this._added = true; + this._editor.addContentWidget(this); + } + } + + hide(): void { + if (!this._hidden) { + this._hidden = true; + this.layout(); + } + } + + layout(): void { + this._editor.layoutContentWidget(this); + } + + getPosition(): IContentWidgetPosition | null { + if (this._hidden || !this._position || !this._preference) { + return null; + } + return { + position: this._position, + preference: [this._preference] + }; + } + + beforeRender() { + const { height, width } = this._widget.element.size; + const { borderWidth } = this._widget.getLayoutInfo(); + return new dom.Dimension(width + 2 * borderWidth, height + 2 * borderWidth); + } + + afterRender(position: ContentWidgetPositionPreference | null) { + this._widget._afterRender(position); + } + + setPreference(preference: ContentWidgetPositionPreference) { + if (!this._preferenceLocked) { + this._preference = preference; + } + } + + lockPreference() { + this._preferenceLocked = true; + } + + unlockPreference() { + this._preferenceLocked = false; + } + + setPosition(position: IPosition | null): void { + this._position = position; } } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts index 8d1834e7f96..9009bfbe169 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts @@ -4,28 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget } from 'vs/editor/browser/editorBrowser'; import { CompletionItem } from './suggest'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; +import { ResizableHTMLElement } from 'vs/editor/contrib/suggest/resizable'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export function canExpandCompletionItem(item: CompletionItem | undefined): boolean { return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label); } -export class SuggestionDetails { +export class SuggestDetailsWidget { - readonly element: HTMLElement; + readonly domNode: HTMLDivElement; private readonly _onDidClose = new Emitter(); readonly onDidClose: Event = this._onDidClose.event; + private readonly _onDidChangeContents = new Emitter(); + readonly onDidChangeContents: Event = this._onDidChangeContents.event; + private readonly _close: HTMLElement; private readonly _scrollbar: DomScrollableElement; private readonly _body: HTMLElement; @@ -34,27 +39,29 @@ export class SuggestionDetails { private readonly _docs: HTMLElement; private readonly _disposables = new DisposableStore(); + private readonly _markdownRenderer: MarkdownRenderer; private _renderDisposeable?: IDisposable; private _borderWidth: number = 1; + private _size = new dom.Dimension(330, 0); constructor( - container: HTMLElement, private readonly _editor: ICodeEditor, - private readonly _markdownRenderer: MarkdownRenderer, - private readonly _kbToggleDetails: string + @IInstantiationService instaService: IInstantiationService, ) { - this.element = dom.append(container, dom.$('.details')); - this._disposables.add(toDisposable(() => this.element.remove())); + this.domNode = dom.$('.suggest-details'); + this.domNode.classList.add('no-docs'); + + this._markdownRenderer = instaService.createInstance(MarkdownRenderer, { editor: _editor }); this._body = dom.$('.body'); this._scrollbar = new DomScrollableElement(this._body, {}); - dom.append(this.element, this._scrollbar.getDomNode()); + dom.append(this.domNode, this._scrollbar.getDomNode()); this._disposables.add(this._scrollbar); this._header = dom.append(this._body, dom.$('.header')); this._close = dom.append(this._header, dom.$('span' + Codicon.close.cssSelector)); - this._close.title = nls.localize('readLess', "Read Less ({0})", this._kbToggleDetails); + this._close.title = nls.localize('details.close', "Close"); this._type = dom.append(this._header, dom.$('p.type')); this._docs = dom.append(this._body, dom.$('p.docs')); @@ -67,7 +74,7 @@ export class SuggestionDetails { } })); - _markdownRenderer.onDidRenderCodeBlock(() => this._scrollbar.scanDomNode(), this, this._disposables); + // this._disposables.add(this._markdownRenderer.onDidRenderCodeBlock(() => this.layout())); } dispose(): void { @@ -76,7 +83,7 @@ export class SuggestionDetails { this._renderDisposeable = undefined; } - private _configureFont() { + private _configureFont(): void { const options = this._editor.getOptions(); const fontInfo = options.get(EditorOption.fontInfo); const fontFamily = fontInfo.fontFamily; @@ -86,25 +93,36 @@ export class SuggestionDetails { const fontSizePx = `${fontSize}px`; const lineHeightPx = `${lineHeight}px`; - this.element.style.fontSize = fontSizePx; - this.element.style.fontWeight = fontWeight; - this.element.style.fontFeatureSettings = fontInfo.fontFeatureSettings; + this.domNode.style.fontSize = fontSizePx; + this.domNode.style.fontWeight = fontWeight; + this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; this._type.style.fontFamily = fontFamily; this._close.style.height = lineHeightPx; this._close.style.width = lineHeightPx; } + getLayoutInfo() { + const lineHeight = this._editor.getOption(EditorOption.suggestLineHeight) || this._editor.getOption(EditorOption.fontInfo).lineHeight; + const borderWidth = this._borderWidth; + const borderHeight = borderWidth * 2; + return { lineHeight, borderWidth, borderHeight }; + } + + renderLoading(): void { this._type.textContent = nls.localize('loading', "Loading..."); this._docs.textContent = ''; + this.domNode.classList.remove('no-docs'); + this.layout(this.size.width, this.getLayoutInfo().lineHeight * 2); + this._onDidChangeContents.fire(this); } renderItem(item: CompletionItem, explainMode: boolean): void { this._renderDisposeable?.dispose(); this._renderDisposeable = undefined; - let { documentation, detail } = item.completion; - // --- documentation + let { detail, documentation } = item.completion; + if (explainMode) { let md = ''; md += `score: ${item.score[0]}${item.word ? `, compared '${item.completion.filterText && (item.completion.filterText + ' (filterText)') || item.completion.label}' with '${item.word}'` : ' (no prefix)'}\n`; @@ -117,20 +135,11 @@ export class SuggestionDetails { if (!explainMode && !canExpandCompletionItem(item)) { this._type.textContent = ''; this._docs.textContent = ''; - this.element.classList.add('no-docs'); + this.domNode.classList.add('no-docs'); return; } - this.element.classList.remove('no-docs'); - if (typeof documentation === 'string') { - this._docs.classList.remove('markdown-docs'); - this._docs.textContent = documentation; - } else { - this._docs.classList.add('markdown-docs'); - this._docs.innerText = ''; - const renderedContents = this._markdownRenderer.render(documentation); - this._renderDisposeable = renderedContents; - this._docs.appendChild(renderedContents.element); - } + + this.domNode.classList.remove('no-docs'); // --- details if (detail) { @@ -141,9 +150,22 @@ export class SuggestionDetails { dom.hide(this._type); } - this.element.style.height = this._header.offsetHeight + this._docs.offsetHeight + (this._borderWidth * 2) + 'px'; - this.element.style.userSelect = 'text'; - this.element.tabIndex = -1; + // --- documentation + dom.clearNode(this._docs); + if (typeof documentation === 'string') { + this._docs.classList.remove('markdown-docs'); + this._docs.textContent = documentation; + + } else if (documentation) { + this._docs.classList.add('markdown-docs'); + dom.clearNode(this._docs); + const renderedContents = this._markdownRenderer.render(documentation); + this._renderDisposeable = renderedContents; + this._docs.appendChild(renderedContents.element); + } + + this.domNode.style.userSelect = 'text'; + this.domNode.tabIndex = -1; this._close.onmousedown = e => { e.preventDefault(); @@ -156,6 +178,20 @@ export class SuggestionDetails { }; this._body.scrollTop = 0; + this.layout(this._size.width, this.getLayoutInfo().lineHeight * (2 + (documentation ? 5 : 0))); + this._onDidChangeContents.fire(this); + } + + get size() { + return this._size; + } + + layout(width: number, height: number): void { + const newSize = new dom.Dimension(width, height); + if (!dom.Dimension.equals(newSize, this._size)) { + this._size = newSize; + dom.size(this.domNode, width, height); + } this._scrollbar.scanDomNode(); } @@ -183,7 +219,155 @@ export class SuggestionDetails { this.scrollUp(80); } - setBorderWidth(width: number): void { + set borderWidth(width: number) { this._borderWidth = width; } + + get borderWidth() { + return this._borderWidth; + } +} + +export class SuggestDetailsOverlay implements IOverlayWidget { + + private readonly _disposables = new DisposableStore(); + private readonly _resizable: ResizableHTMLElement; + + private _added: boolean = false; + private _anchorBox?: dom.IDomNodePagePosition; + private _userSize?: dom.Dimension; + + constructor( + readonly widget: SuggestDetailsWidget, + private readonly _editor: ICodeEditor + ) { + + this._resizable = new ResizableHTMLElement(); + this._resizable.domNode.classList.add('suggest-details-container'); + this._resizable.domNode.appendChild(widget.domNode); + this._resizable.enableSashes(false, true, true, false); + + this._disposables.add(this._resizable.onDidResize(e => { + if (this._anchorBox) { + this._placeAtAnchor(this._anchorBox, e.dimension); + this._userSize = e.dimension; + } + })); + + this._disposables.add(this.widget.onDidChangeContents(() => { + if (this._anchorBox) { + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + })); + } + + dispose(): void { + this._disposables.dispose(); + this.hide(); + } + + getId(): string { + return 'suggest.details'; + } + + getDomNode(): HTMLElement { + return this._resizable.domNode; + } + + getPosition(): null { + return null; + } + + show(): void { + if (!this._added) { + this._editor.addOverlayWidget(this); + this.getDomNode().style.position = 'fixed'; + this._added = true; + } + } + + hide(): void { + if (this._added) { + this._editor.removeOverlayWidget(this); + this._added = false; + this._anchorBox = undefined; + this._userSize = undefined; + } + } + + placeAtAnchor(anchor: HTMLElement) { + const anchorBox = dom.getDomNodePagePosition(anchor); + this._anchorBox = anchorBox; + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + + _placeAtAnchor(anchorBox: dom.IDomNodePagePosition, size: dom.Dimension) { + const bodyBox = dom.getClientArea(document.body); + + const { borderWidth, borderHeight, lineHeight } = this.widget.getLayoutInfo(); + + let maxSizeTop: dom.Dimension; + let maxSizeBottom: dom.Dimension; + let minSize = new dom.Dimension(220, 2 * lineHeight); + + let left = 0; + let top = anchorBox.top; + let bottom = anchorBox.top + anchorBox.height - borderHeight; + + // position: EAST, west, south + let width = bodyBox.width - (anchorBox.left + anchorBox.width); + left = -borderWidth + anchorBox.left + anchorBox.width; + maxSizeTop = new dom.Dimension(bodyBox.width - (anchorBox.left + anchorBox.width), bodyBox.height - anchorBox.top); + maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height); + + // position: east, WEST, south + if (anchorBox.left > width) { + // pos = SuggestDetailsPosition.West; + width = anchorBox.left; + left = Math.max(0, anchorBox.left - (size.width + borderWidth)); + maxSizeTop = new dom.Dimension(anchorBox.left, bodyBox.height - anchorBox.top); + maxSizeBottom = maxSizeTop.with(undefined, maxSizeBottom.height); + } + + // position: east, west, SOUTH + if (anchorBox.width > width * 1.3 && bodyBox.height - (anchorBox.top + anchorBox.height) > anchorBox.height) { + width = anchorBox.width; + left = anchorBox.left; + top = -borderWidth + anchorBox.top + anchorBox.height; + maxSizeTop = new dom.Dimension(anchorBox.width - borderHeight, bodyBox.height - (anchorBox.top + anchorBox.height)); + maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top); + minSize = minSize.with(maxSizeTop.width); + } + + // top/bottom placement + let alignAtTop: boolean; + let height = size.height; + let maxHeight = Math.max(maxSizeTop.height, maxSizeBottom.height); + if (height > maxHeight) { + height = maxHeight; + } + let maxSize: dom.Dimension; + if (height < maxSizeTop.height) { + alignAtTop = true; + maxSize = maxSizeTop; + } else { + alignAtTop = false; + maxSize = maxSizeBottom; + } + + this.getDomNode().style.position = 'fixed'; + this.getDomNode().style.left = `${left}px`; + if (alignAtTop) { + this.getDomNode().style.top = `${top}px`; + this._resizable.enableSashes(false, true, true, false); + } else { + this.getDomNode().style.top = `${bottom - height}px`; + this._resizable.enableSashes(true, true, false, false); + } + + this._resizable.minSize = minSize; + this._resizable.maxSize = maxSize; + this._resizable.layout(height, Math.min(maxSize.width, size.width)); + this.widget.layout(this._resizable.size.width, this._resizable.size.height); + } } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index d79017d1b44..66e6e51cef6 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -83,7 +83,6 @@ export class ItemRenderer implements IListRenderer { const options = this._editor.getOptions(); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts index fa0aed4fcf0..eafca367afc 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts @@ -5,15 +5,32 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IActionViewItemProvider, IAction } from 'vs/base/common/actions'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { format } from 'vs/base/common/strings'; import { suggestWidgetStatusbarMenu } from 'vs/editor/contrib/suggest/suggest'; -import { IMenuService } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +class StatusBarViewItem extends MenuEntryActionViewItem { + + updateLabel() { + const kb = this._keybindingService.lookupKeybinding(this._action.id); + if (!kb) { + return super.updateLabel(); + } + if (this.label) { + this.label.textContent = localize('ddd', '{0} ({1})', this._action.label, StatusBarViewItem.symbolPrintEnter(kb)); + } + } + + static symbolPrintEnter(kb: ResolvedKeybinding) { + return kb.getLabel()?.replace(/\benter\b/gi, '\u23CE'); + } +} export class SuggestWidgetStatus { @@ -23,33 +40,24 @@ export class SuggestWidgetStatus { constructor( container: HTMLElement, - @IKeybindingService keybindingService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, ) { this.element = dom.append(container, dom.$('.suggest-status-bar')); - const actionViewItemProvider = (action => { - const kb = keybindingService.lookupKeybindings(action.id); - return new class extends ActionViewItem { - constructor() { - super(undefined, action, { label: true, icon: false }); - } - updateLabel() { - if (isFalsyOrEmpty(kb) || !this.label) { - return super.updateLabel(); - } - const { label } = this.getAction(); - this.label.textContent = /{\d}/.test(label) - ? format(this.getAction().label, kb[0].getLabel()) - : `${this.getAction().label} (${kb[0].getLabel()})`; - } - }; + return action instanceof MenuItemAction + ? instantiationService.createInstance(StatusBarViewItem, action) + : undefined; }); const leftActions = new ActionBar(this.element, { actionViewItemProvider }); const rightActions = new ActionBar(this.element, { actionViewItemProvider }); const menu = menuService.createMenu(suggestWidgetStatusbarMenu, contextKeyService); + + leftActions.domNode.classList.add('left'); + rightActions.domNode.classList.add('right'); + const renderMenu = () => { const left: IAction[] = []; const right: IAction[] = []; diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 2db871d0caa..3193c678796 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -35,6 +35,7 @@ import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKe import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { mock } from 'vs/base/test/common/mock'; +import { NullLogService } from 'vs/platform/log/common/log'; function createMockEditor(model: TextModel): ITestCodeEditor { @@ -201,7 +202,9 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { readText() { return Promise.resolve('CLIPPY'); } - } + }, + NullTelemetryService, + new NullLogService() ); disposables.push(oracle, editor); diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index 274ad8a2060..9da4eb8dacb 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -328,7 +328,6 @@ class ShowAccessibilityHelpAction extends EditorAction { alias: 'Show Accessibility Help', precondition: undefined, kbOpts: { - kbExpr: EditorContextKeys.focus, primary: KeyMod.Alt | KeyCode.F1, weight: KeybindingWeight.EditorContrib, linux: { diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index da25cab90cb..2b2a748c662 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -167,8 +167,9 @@ suite('EditorSimpleWorker', () => { assert.ok(false); return; } - assert.equal(result.length, 1); - assert.equal(result, 'foobar'); + assert.equal(result.words.length, 1); + assert.equal(typeof result.duration, 'number'); + assert.equal(result.words[0], 'foobar'); }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a44cb3bb307..12faeaf905d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4246,6 +4246,18 @@ declare namespace monaco.editor { * If null is returned, the content widget will be placed off screen. */ getPosition(): IContentWidgetPosition | null; + /** + * Optional function that is invoked before rendering + * the content widget. If a dimension is returned the editor will + * attempt to use it. + */ + beforeRender?(): IDimension | null; + /** + * Optional function that is invoked after rendering the content + * widget. The arguments are the actual dimensions and the selected + * position preference. + */ + afterRender?(position: ContentWidgetPositionPreference | null): void; } /** @@ -6137,6 +6149,10 @@ declare namespace monaco.languages { * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChange?: IEvent; /** * Provides the folding ranges for a specific model. */ diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 3975c7fbd7e..81db82116ae 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -3,13 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; +import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { IAction, Separator } from 'vs/base/common/actions'; -import { Emitter } from 'vs/base/common/event'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { isLinux, isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -19,68 +17,9 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; -// The alternative key on all platforms is alt. On windows we also support shift as an alternative key #44136 -class AlternativeKeyEmitter extends Emitter { - - private readonly _subscriptions = new DisposableStore(); - private _isPressed: boolean = false; - private static instance: AlternativeKeyEmitter; - private _suppressAltKeyUp: boolean = false; - - private constructor(contextMenuService: IContextMenuService) { - super(); - - this._subscriptions.add(domEvent(document.body, 'keydown')(e => { - this.isPressed = e.altKey || ((isWindows || isLinux) && e.shiftKey); - })); - this._subscriptions.add(domEvent(document.body, 'keyup')(e => { - if (this.isPressed) { - if (this._suppressAltKeyUp) { - e.preventDefault(); - } - } - - this._suppressAltKeyUp = false; - this.isPressed = false; - })); - this._subscriptions.add(domEvent(document.body, 'mouseleave')(e => this.isPressed = false)); - this._subscriptions.add(domEvent(document.body, 'blur')(e => this.isPressed = false)); - // Workaround since we do not get any events while a context menu is shown - this._subscriptions.add(contextMenuService.onDidContextMenu(() => this.isPressed = false)); - } - - get isPressed(): boolean { - return this._isPressed; - } - - set isPressed(value: boolean) { - this._isPressed = value; - this.fire(this._isPressed); - } - - suppressAltKeyUp() { - // Sometimes the native alt behavior needs to be suppresed since the alt was already used as an alternative key - // Example: windows behavior to toggle tha top level menu #44396 - this._suppressAltKeyUp = true; - } - - static getInstance(contextMenuService: IContextMenuService) { - if (!AlternativeKeyEmitter.instance) { - AlternativeKeyEmitter.instance = new AlternativeKeyEmitter(contextMenuService); - } - - return AlternativeKeyEmitter.instance; - } - - dispose() { - super.dispose(); - this._subscriptions.dispose(); - } -} - -export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, contextMenuService: IContextMenuService, isPrimaryGroup?: (group: string) => boolean): IDisposable { +export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); - const useAlternativeActions = AlternativeKeyEmitter.getInstance(contextMenuService).isPressed; + const useAlternativeActions = ModifierKeyEmitter.getInstance().keyStatus.altKey; fillInActions(groups, target, useAlternativeActions, isPrimaryGroup); return asDisposable(groups); } @@ -133,16 +72,15 @@ export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; private readonly _itemClassDispose = this._register(new MutableDisposable()); - private readonly _altKey: AlternativeKeyEmitter; + private readonly _altKey: ModifierKeyEmitter; constructor( readonly _action: MenuItemAction, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @INotificationService protected _notificationService: INotificationService, - @IContextMenuService _contextMenuService: IContextMenuService + @IKeybindingService protected readonly _keybindingService: IKeybindingService, + @INotificationService protected _notificationService: INotificationService ) { super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon }); - this._altKey = AlternativeKeyEmitter.getInstance(_contextMenuService); + this._altKey = ModifierKeyEmitter.getInstance(); } protected get _commandAction(): IAction { @@ -153,10 +91,6 @@ export class MenuEntryActionViewItem extends ActionViewItem { event.preventDefault(); event.stopPropagation(); - if (this._altKey.isPressed) { - this._altKey.suppressAltKeyUp(); - } - this.actionRunner.run(this._commandAction, this._context) .then(undefined, err => this._notificationService.error(err)); } @@ -168,7 +102,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { let mouseOver = false; - let alternativeKeyDown = this._altKey.isPressed; + let alternativeKeyDown = this._altKey.keyStatus.altKey; const updateAltState = () => { const wantsAltCommand = mouseOver && alternativeKeyDown; @@ -182,7 +116,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { if (this._action.alt) { this._register(this._altKey.event(value => { - alternativeKeyDown = value; + alternativeKeyDown = value.altKey; updateAltState(); })); } diff --git a/src/vs/platform/contextview/browser/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 3ef087c575b..77c92d6dd26 100644 --- a/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/src/vs/platform/contextview/browser/contextMenuService.ts @@ -6,19 +6,16 @@ import { ContextMenuHandler, IContextMenuHandlerOptions } from './contextMenuHandler'; import { IContextViewService, IContextMenuService } from './contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { Event, Emitter } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ModifierKeyEmitter } from 'vs/base/browser/dom'; export class ContextMenuService extends Disposable implements IContextMenuService { declare readonly _serviceBrand: undefined; - private _onDidContextMenu = this._register(new Emitter()); - readonly onDidContextMenu: Event = this._onDidContextMenu.event; - private contextMenuHandler: ContextMenuHandler; constructor( @@ -41,6 +38,6 @@ export class ContextMenuService extends Disposable implements IContextMenuServic showContextMenu(delegate: IContextMenuDelegate): void { this.contextMenuHandler.showContextMenu(delegate); - this._onDidContextMenu.fire(); + ModifierKeyEmitter.getInstance().resetKeyStatus(); } } diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index c6511397a9f..dcba62aa330 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; @@ -41,5 +40,4 @@ export interface IContextMenuService { readonly _serviceBrand: undefined; showContextMenu(delegate: IContextMenuDelegate): void; - onDidContextMenu: Event; // TODO@isidor these event should be removed once we get async context menus } diff --git a/src/vs/platform/encryption/electron-main/common/encryptionService.ts b/src/vs/platform/encryption/common/encryptionService.ts similarity index 100% rename from src/vs/platform/encryption/electron-main/common/encryptionService.ts rename to src/vs/platform/encryption/common/encryptionService.ts diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index d91b6b96b61..bf5041ff784 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICommonEncryptionService } from 'vs/platform/encryption/electron-main/common/encryptionService'; +import { ICommonEncryptionService } from 'vs/platform/encryption/common/encryptionService'; export const IEncryptionMainService = createDecorator('encryptionMainService'); diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index d92a54c4a69..409bb7e1960 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -52,6 +52,7 @@ export interface NativeParsedArgs { 'show-versions'?: boolean; 'category'?: string; 'install-extension'?: string[]; // undefined or array of 1 or more + 'install-builtin-extension'?: string[]; // undefined or array of 1 or more 'uninstall-extension'?: string[]; // undefined or array of 1 or more 'locate-extension'?: string[]; // undefined or array of 1 or more 'enable-proposed-api'?: string[]; // undefined or array of 1 or more @@ -74,7 +75,6 @@ export interface NativeParsedArgs { 'disable-user-env-probe'?: boolean; 'force'?: boolean; 'do-not-sync'?: boolean; - 'builtin'?: boolean; 'force-user-env'?: boolean; 'sync'?: 'on' | 'off'; '__sandbox'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index baf52bf2dcb..149e6ffb41a 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -103,9 +103,9 @@ export const OPTIONS: OptionDescriptions> = { 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, 'driver-verbose': { type: 'boolean' }, + 'install-builtin-extension': { type: 'string[]' }, 'force': { type: 'boolean' }, 'do-not-sync': { type: 'boolean' }, - 'builtin': { type: 'boolean' }, 'trace': { type: 'boolean' }, 'trace-category-filter': { type: 'string' }, 'trace-options': { type: 'string' }, diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e352246246d..b9e1f47e86c 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -371,6 +371,21 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } + async getExtensions(names: string[], token: CancellationToken): Promise { + const result: IGalleryExtension[] = []; + let { total, firstPage: pageResult, getPage } = await this.query({ names, pageSize: names.length }, token); + result.push(...pageResult); + for (let pageIndex = 1; result.length < total; pageIndex++) { + pageResult = await getPage(pageIndex, token); + if (pageResult.length) { + result.push(...pageResult); + } else { + break; + } + } + return result; + } + async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { const extension = await this.getCompatibleExtensionByEngine(arg1, version); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 8148e986c9e..8b8f2f217e3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -152,6 +152,7 @@ export interface IExtensionGalleryService { isEnabled(): boolean; query(token: CancellationToken): Promise>; query(options: IQueryOptions, token: CancellationToken): Promise>; + getExtensions(ids: string[], token: CancellationToken): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; getReadme(extension: IGalleryExtension, token: CancellationToken): Promise; @@ -217,6 +218,7 @@ export interface IExtensionManagementService { getExtensionsReport(): Promise; updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; + updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise; } export const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/disabled'; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 048aa90adf3..da354045436 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; @@ -64,11 +64,12 @@ export class ExtensionManagementChannel implements IServerChannel { case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); case 'canInstall': return this.service.canInstall(args[0]); - case 'installFromGallery': return this.service.installFromGallery(args[0]); + case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); case 'getInstalled': return this.service.getInstalled(args[0]).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer))); case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer)); + case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer)); case 'getExtensionsReport': return this.service.getExtensionsReport(); } @@ -109,8 +110,8 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return true; } - installFromGallery(extension: IGalleryExtension): Promise { - return Promise.resolve(this.channel.call('installFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); + installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + return Promise.resolve(this.channel.call('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null)); } uninstall(extension: ILocalExtension, force = false): Promise { @@ -131,6 +132,11 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer .then(extension => transformIncomingExtension(extension, null)); } + updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise { + return Promise.resolve(this.channel.call('updateExtensionScope', [local, isMachineScoped])) + .then(extension => transformIncomingExtension(extension, null)); + } + getExtensionsReport(): Promise { return Promise.resolve(this.channel.call('getExtensionsReport')); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 051d48d4553..615a6565e1b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -44,7 +44,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; -import { ExtensionsScanner, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner'; +import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled'; @@ -201,7 +201,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } catch (e) { /* Ignore */ } try { - const local = await this.installFromZipPath(identifierWithVersion, zipPath, { ...(metadata || {}), ...options }, operation, token); + const local = await this.installFromZipPath(identifierWithVersion, zipPath, { ...(metadata || {}), ...options }, options, operation, token); this.logService.info('Successfully installed the extension:', identifier.id); return local; } catch (e) { @@ -224,11 +224,11 @@ export class ExtensionManagementService extends Disposable implements IExtension return downloadedLocation; } - private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise { + private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise { try { const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token); try { - await this.installDependenciesAndPackExtensions(local, undefined); + await this.installDependenciesAndPackExtensions(local, undefined, options); } catch (error) { if (isNonEmptyArray(local.manifest.extensionDependencies)) { this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); @@ -298,7 +298,7 @@ export class ExtensionManagementService extends Disposable implements IExtension try { await this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)); } catch (error) { /* Ignore */ } try { - await this.installDependenciesAndPackExtensions(local, existingExtension); + await this.installDependenciesAndPackExtensions(local, existingExtension, options); } catch (error) { try { await this.uninstall(local); } catch (error) { /* Ignore */ } throw error; @@ -434,7 +434,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return local; } - private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined): Promise { + private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined, options: InstallOptions): Promise { if (!this.galleryService.isEnabled()) { return; } @@ -457,7 +457,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None); const extensionsToInstall = galleryResult.firstPage; try { - await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e))); + await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e, options))); } catch (error) { try { await this.rollback(extensionsToInstall); } catch (e) { /* ignore */ } throw error; @@ -489,7 +489,14 @@ export class ExtensionManagementService extends Disposable implements IExtension async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...metadata, isMachineScoped: local.isMachineScoped }); + local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((local.manifest).__metadata || {}), ...metadata }); + this.manifestCache.invalidate(); + return local; + } + + async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise { + this.logService.trace('ExtensionManagementService#updateExtensionScope', local.identifier.id); + local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((local.manifest).__metadata || {}), isMachineScoped }); this.manifestCache.invalidate(); return local; } diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 878e6bd5970..3f224d1714e 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -31,7 +31,7 @@ const INSTALL_ERROR_DELETING = 'deleting'; const INSTALL_ERROR_RENAMING = 'renaming'; export type IMetadata = Partial; -type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata }; +export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata }; type IRelaxedLocalExtension = Omit & { isBuiltin: boolean }; export class ExtensionsScanner extends Disposable { @@ -365,7 +365,6 @@ export class ExtensionsScanner extends Disposable { try { const manifest = JSON.parse(raw); const metadata = manifest.__metadata || null; - delete manifest.__metadata; c({ manifest, metadata }); } catch (err) { e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index daaaa86a98b..fa087862bc7 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -24,8 +24,8 @@ export class IndexedDB { this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]); } - async createFileSystemProvider(scheme: string, store: string): Promise { - let fsp: IFileSystemProvider | null = null; + async createFileSystemProvider(scheme: string, store: string): Promise { + let fsp: IIndexedDBFileSystemProvider | null = null; const indexedDB = await this.indexedDBPromise; if (indexedDB) { if (indexedDB.objectStoreNames.contains(store)) { @@ -68,7 +68,8 @@ export class IndexedDB { } -class IndexedDBFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { +export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { } +class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 52e9ecdf293..2d2cdcfeef4 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isUndefinedOrNull } from 'vs/base/common/types'; +import { isNumber, isUndefinedOrNull } from 'vs/base/common/types'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -978,8 +978,12 @@ export class BinarySize { static readonly TB = BinarySize.GB * BinarySize.KB; static formatSize(size: number): string { + if (!isNumber(size)) { + size = 0; + } + if (size < BinarySize.KB) { - return localize('sizeB', "{0}B", size); + return localize('sizeB', "{0}B", size.toFixed(0)); } if (size < BinarySize.MB) { diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts new file mode 100644 index 00000000000..abb3034a366 --- /dev/null +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { Schemas } from 'vs/base/common/network'; +import { posix } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { assertIsDefined } from 'vs/base/common/types'; + +// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s, +// making /-style absolute paths fail isAbsolute checks. +const join = posix.join; + +suite('IndexedDB File Service', function () { + + const logSchema = 'logs'; + + let service: FileService; + let logFileProvider: IIndexedDBFileSystemProvider; + let userdataFileProvider: IIndexedDBFileSystemProvider; + const testDir = '/'; + + const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path }); + const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path }); + + const disposables = new DisposableStore(); + + setup(async () => { + const logService = new NullLogService(); + + service = new FileService(logService); + disposables.add(service); + + logFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(Schemas.file, INDEXEDDB_LOGS_OBJECT_STORE)); + disposables.add(service.registerProvider(logSchema, logFileProvider)); + disposables.add(logFileProvider); + + userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE)); + disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); + disposables.add(userdataFileProvider); + }); + + teardown(async () => { + disposables.clear(); + + await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false }); + await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false }); + }); + + test('createFolder', async () => { + let event: FileOperationEvent | undefined; + disposables.add(service.onDidRunOperation(e => event = e)); + + const parent = await service.resolve(makeUserdataURI(testDir)); + + const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder')); + + assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0); + const newFolder = await service.createFolder(newFolderResource); + assert.equal(newFolder.name, 'newFolder'); + // Invalid.. dirs dont exist in our IDBFSB. + // assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1); + + assert.ok(event); + assert.equal(event!.resource.path, newFolderResource.path); + assert.equal(event!.operation, FileOperation.CREATE); + assert.equal(event!.target!.resource.path, newFolderResource.path); + assert.equal(event!.target!.isDirectory, true); + }); +}); diff --git a/src/vs/platform/lifecycle/common/lifecycle.ts b/src/vs/platform/lifecycle/common/lifecycle.ts index 5d047b97813..c67d56a0f16 100644 --- a/src/vs/platform/lifecycle/common/lifecycle.ts +++ b/src/vs/platform/lifecycle/common/lifecycle.ts @@ -3,182 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { isThenable } from 'vs/base/common/async'; -export const ILifecycleService = createDecorator('lifecycleService'); - -/** - * An event that is send out when the window is about to close. Clients have a chance to veto - * the closing by either calling veto with a boolean "true" directly or with a promise that - * resolves to a boolean. Returning a promise is useful in cases of long running operations - * on shutdown. - * - * Note: It is absolutely important to avoid long running promises if possible. Please try hard - * to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence! - */ -export interface BeforeShutdownEvent { - - /** - * Allows to veto the shutdown. The veto can be a long running operation but it - * will block the application from closing. - */ - veto(value: boolean | Promise): void; - - /** - * The reason why the application will be shutting down. - */ - readonly reason: ShutdownReason; -} - -/** - * An event that is send out when the window closes. Clients have a chance to join the closing - * by providing a promise from the join method. Returning a promise is useful in cases of long - * running operations on shutdown. - * - * Note: It is absolutely important to avoid long running promises if possible. Please try hard - * to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence! - */ -export interface WillShutdownEvent { - - /** - * Allows to join the shutdown. The promise can be a long running operation but it - * will block the application from closing. - */ - join(promise: Promise): void; - - /** - * The reason why the application is shutting down. - */ - readonly reason: ShutdownReason; -} - -export const enum ShutdownReason { - - /** Window is closed */ - CLOSE = 1, - - /** Application is quit */ - QUIT = 2, - - /** Window is reloaded */ - RELOAD = 3, - - /** Other configuration loaded into window */ - LOAD = 4 -} - -export const enum StartupKind { - NewWindow = 1, - ReloadedWindow = 3, - ReopenedWindow = 4, -} - -export function StartupKindToString(startupKind: StartupKind): string { - switch (startupKind) { - case StartupKind.NewWindow: return 'NewWindow'; - case StartupKind.ReloadedWindow: return 'ReloadedWindow'; - case StartupKind.ReopenedWindow: return 'ReopenedWindow'; - } -} - -export const enum LifecyclePhase { - - /** - * The first phase signals that we are about to startup getting ready. - */ - Starting = 1, - - /** - * Services are ready and the view is about to restore its state. - */ - Ready = 2, - - /** - * Views, panels and editors have restored. For editors this means, that - * they show their contents fully. - */ - Restored = 3, - - /** - * The last phase after views, panels and editors have restored and - * some time has passed (few seconds). - */ - Eventually = 4 -} - -export function LifecyclePhaseToString(phase: LifecyclePhase) { - switch (phase) { - case LifecyclePhase.Starting: return 'Starting'; - case LifecyclePhase.Ready: return 'Ready'; - case LifecyclePhase.Restored: return 'Restored'; - case LifecyclePhase.Eventually: return 'Eventually'; - } -} - -/** - * A lifecycle service informs about lifecycle events of the - * application, such as shutdown. - */ -export interface ILifecycleService { - - readonly _serviceBrand: undefined; - - /** - * Value indicates how this window got loaded. - */ - readonly startupKind: StartupKind; - - /** - * A flag indicating in what phase of the lifecycle we currently are. - */ - phase: LifecyclePhase; - - /** - * Fired before shutdown happens. Allows listeners to veto against the - * shutdown to prevent it from happening. - * - * The event carries a shutdown reason that indicates how the shutdown was triggered. - */ - readonly onBeforeShutdown: Event; - - /** - * Fired when no client is preventing the shutdown from happening (from onBeforeShutdown). - * Can be used to save UI state even if that is long running through the WillShutdownEvent#join() - * method. - * - * The event carries a shutdown reason that indicates how the shutdown was triggered. - */ - readonly onWillShutdown: Event; - - /** - * Fired when the shutdown is about to happen after long running shutdown operations - * have finished (from onWillShutdown). This is the right place to dispose resources. - */ - readonly onShutdown: Event; - - /** - * Returns a promise that resolves when a certain lifecycle phase - * has started. - */ - when(phase: LifecyclePhase): Promise; -} - -export const NullLifecycleService: ILifecycleService = { - - _serviceBrand: undefined, - - onBeforeShutdown: Event.None, - onWillShutdown: Event.None, - onShutdown: Event.None, - - phase: LifecyclePhase.Restored, - startupKind: StartupKind.NewWindow, - - when() { return Promise.resolve(); } -}; - // Shared veto handling across main and renderer export function handleVetos(vetos: (boolean | Promise)[], onError: (error: Error) => void): Promise { if (vetos.length === 0) { diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts index 3f85535c791..a6318446b88 100644 --- a/src/vs/platform/log/common/fileLogService.ts +++ b/src/vs/platform/log/common/fileLogService.ts @@ -5,7 +5,7 @@ import { ILogService, LogLevel, AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; -import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath, basename } from 'vs/base/common/resources'; @@ -87,8 +87,12 @@ export class FileLogService extends AbstractLogService implements ILogService { } private async initialize(): Promise { - if (!await this.fileService.exists(this.resource)) { + try { await this.fileService.createFile(this.resource); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_MODIFIED_SINCE) { + throw error; + } } } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e986ae7fbba..4bcb50b4c8d 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -645,6 +645,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain if (password) { try { let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); + if (!content || !hasNextChunk) { + return password; + } + let index = 1; while (hasNextChunk) { const nextChunk = await keytar.getPassword(service, `${account}-${index}`); diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 53dc899d48d..333e5b24b05 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -51,6 +51,7 @@ export interface IProductConfiguration { readonly downloadUrl?: string; readonly updateUrl?: string; + readonly webEndpointUrl?: string; readonly target?: string; readonly settingsSearchBuildId?: number; diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index 208ffc71dcd..a87802a84fc 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -18,11 +18,6 @@ import { URI } from 'vs/base/common/uri'; suite('StorageService', function () { - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 10); - test('Remove Data (global, in-memory)', () => { removeData(StorageScope.GLOBAL); }); @@ -89,6 +84,12 @@ suite('StorageService', function () { } test('Migrate Data', async () => { + + // Given issues such as https://github.com/microsoft/vscode/issues/108113 + // we see random test failures when accessing the native file system. + this.retries(3); + this.timeout(1000 * 20); + class StorageTestEnvironmentService extends NativeEnvironmentService { constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) { diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index ee245e0c0e0..c9de28ba8d3 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -448,12 +448,12 @@ export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foregr */ export const chartsForeground = registerColor('charts.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('chartsForeground', "The foreground color used in charts.")); export const chartsLines = registerColor('charts.lines', { dark: transparent(foreground, .5), light: transparent(foreground, .5), hc: transparent(foreground, .5) }, nls.localize('chartsLines', "The color used for horizontal lines in charts.")); -export const chartsRed = registerColor('charts.red', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('chartsRed', "The red color used charts.")); -export const chartsBlue = registerColor('charts.blue', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('chartsBlue', "The blue color used charts.")); -export const chartsYellow = registerColor('charts.yellow', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('chartsYellow', "The yellow color used charts.")); -export const chartsOrange = registerColor('charts.orange', { dark: minimapFindMatch, light: minimapFindMatch, hc: minimapFindMatch }, nls.localize('chartsOrange', "The orange color used charts.")); -export const chartsGreen = registerColor('charts.green', { dark: '#89D185', light: '#388A34', hc: '#89D185' }, nls.localize('chartsGreen', "The green color used charts.")); -export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', light: '#652D90', hc: '#B180D7' }, nls.localize('chartsPurple', "The purple color used charts.")); +export const chartsRed = registerColor('charts.red', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('chartsRed', "The red color used in chart visualizations.")); +export const chartsBlue = registerColor('charts.blue', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); +export const chartsYellow = registerColor('charts.yellow', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); +export const chartsOrange = registerColor('charts.orange', { dark: minimapFindMatch, light: minimapFindMatch, hc: minimapFindMatch }, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); +export const chartsGreen = registerColor('charts.green', { dark: '#89D185', light: '#388A34', hc: '#89D185' }, nls.localize('chartsGreen', "The green color used in chart visualizations.")); +export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', light: '#652D90', hc: '#B180D7' }, nls.localize('chartsPurple', "The purple color used in chart visualizations.")); // ----- color functions diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index 1540037a7b9..c67ce0ff249 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -6,9 +6,6 @@ import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { deepClone } from 'vs/base/common/objects'; -import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { distinct } from 'vs/base/common/arrays'; export interface IMergeResult { added: ISyncExtension[]; @@ -202,31 +199,3 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync } return massagedExtension; } - -export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] { - const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase()); - const value = getConfiguredIgnoredExtensions(configurationService).map(id => id.toLowerCase()); - const added: string[] = [], removed: string[] = []; - if (Array.isArray(value)) { - for (const key of value) { - if (key.startsWith('-')) { - removed.push(key.substring(1)); - } else { - added.push(key); - } - } - } - return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1)); -} - -function getConfiguredIgnoredExtensions(configurationService: IConfigurationService): string[] { - let userValue = configurationService.inspect('settingsSync.ignoredExtensions').userValue; - if (userValue !== undefined) { - return userValue; - } - userValue = configurationService.inspect('sync.ignoredExtensions').userValue; - if (userValue !== undefined) { - return userValue; - } - return configurationService.getValue('settingsSync.ignoredExtensions') || []; -} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 1b10c8ac135..c65ee377e1b 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -14,7 +14,7 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; +import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; @@ -23,6 +23,7 @@ import { applyEdits } from 'vs/base/common/jsonEdit'; import { compare } from 'vs/base/common/strings'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; interface IExtensionResourceMergeResult extends IAcceptResult { readonly added: ISyncExtension[]; @@ -94,6 +95,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, + @IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService configurationService: IConfigurationService, @@ -117,7 +119,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -201,7 +203,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); const { added, removed, updated, remote } = mergeResult; return { @@ -217,7 +219,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); @@ -277,7 +279,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))); return this.format(localExtensions); } diff --git a/src/vs/platform/userDataSync/common/ignoredExtensions.ts b/src/vs/platform/userDataSync/common/ignoredExtensions.ts new file mode 100644 index 00000000000..97296af8d37 --- /dev/null +++ b/src/vs/platform/userDataSync/common/ignoredExtensions.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { distinct } from 'vs/base/common/arrays'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IIgnoredExtensionsManagementService = createDecorator('IIgnoredExtensionsManagementService'); +export interface IIgnoredExtensionsManagementService { + readonly _serviceBrand: any; + + getIgnoredExtensions(installed: ILocalExtension[]): string[]; + + hasToNeverSyncExtension(extensionId: string): boolean; + hasToAlwaysSyncExtension(extensionId: string): boolean; + updateIgnoredExtensions(ignoredExtensionId: string, ignore: boolean): Promise; + updateSynchronizedExtensions(ignoredExtensionId: string, sync: boolean): Promise; +} + +export class IgnoredExtensionsManagementService implements IIgnoredExtensionsManagementService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + hasToNeverSyncExtension(extensionId: string): boolean { + const configuredIgnoredExtensions = this.getConfiguredIgnoredExtensions(); + return configuredIgnoredExtensions.includes(extensionId.toLowerCase()); + } + + hasToAlwaysSyncExtension(extensionId: string): boolean { + const configuredIgnoredExtensions = this.getConfiguredIgnoredExtensions(); + return configuredIgnoredExtensions.includes(`-${extensionId.toLowerCase()}`); + } + + updateIgnoredExtensions(ignoredExtensionId: string, ignore: boolean): Promise { + // first remove the extension completely from ignored extensions + let currentValue = [...this.configurationService.getValue('settingsSync.ignoredExtensions')].map(id => id.toLowerCase()); + currentValue = currentValue.filter(v => v !== ignoredExtensionId && v !== `-${ignoredExtensionId}`); + + // Add only if ignored + if (ignore) { + currentValue.push(ignoredExtensionId.toLowerCase()); + } + + return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + } + + updateSynchronizedExtensions(extensionId: string, sync: boolean): Promise { + // first remove the extension completely from ignored extensions + let currentValue = [...this.configurationService.getValue('settingsSync.ignoredExtensions')].map(id => id.toLowerCase()); + currentValue = currentValue.filter(v => v !== extensionId && v !== `-${extensionId}`); + + // Add only if synced + if (sync) { + currentValue.push(`-${extensionId.toLowerCase()}`); + } + + return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + } + + getIgnoredExtensions(installed: ILocalExtension[]): string[] { + const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase()); + const value = this.getConfiguredIgnoredExtensions().map(id => id.toLowerCase()); + const added: string[] = [], removed: string[] = []; + if (Array.isArray(value)) { + for (const key of value) { + if (key.startsWith('-')) { + removed.push(key.substring(1)); + } else { + added.push(key); + } + } + } + return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1)); + } + + private getConfiguredIgnoredExtensions(): string[] { + let userValue = this.configurationService.inspect('settingsSync.ignoredExtensions').userValue; + if (userValue !== undefined) { + return userValue; + } + userValue = this.configurationService.inspect('sync.ignoredExtensions').userValue; + if (userValue !== undefined) { + return userValue; + } + return (this.configurationService.getValue('settingsSync.ignoredExtensions') || []).map(id => id.toLowerCase()); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index aa678e17ef7..cb2342a2eae 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -6,7 +6,7 @@ import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -37,15 +37,22 @@ const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const sessionIdKey = 'sync.sessionId'; const storeUrlKey = 'sync.storeUrl'; -export class UserDataAutoSyncEnablementService extends Disposable { +interface _IUserDataAutoSyncEnablementService extends IUserDataAutoSyncEnablementService { + canToggleEnablement(): boolean; + setEnablement(enabled: boolean): void; +} + +export class UserDataAutoSyncEnablementService extends Disposable implements _IUserDataAutoSyncEnablementService { + + _serviceBrand: any; private _onDidChangeEnablement = new Emitter(); readonly onDidChangeEnablement: Event = this._onDidChangeEnablement.event; constructor( - @IStorageService protected readonly storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @IEnvironmentService protected readonly environmentService: IEnvironmentService, - @IUserDataSyncStoreManagementService protected readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, ) { super(); this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); @@ -65,24 +72,29 @@ export class UserDataAutoSyncEnablementService extends Disposable { return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined; } - protected setEnablement(enabled: boolean): void { + setEnablement(enabled: boolean): void { this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL); } private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void { - if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) { - if (enablementKey === workspaceStorageChangeEvent.key) { - this._onDidChangeEnablement.fire(this.isEnabled()); - } + if (workspaceStorageChangeEvent.scope !== StorageScope.GLOBAL) { + return; + } + + if (enablementKey === workspaceStorageChangeEvent.key) { + this._onDidChangeEnablement.fire(this.isEnabled()); + return; } } } -export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { +export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { _serviceBrand: any; + private readonly userDataAutoSyncEnablementService: _IUserDataAutoSyncEnablementService; + private readonly autoSync = this._register(new MutableDisposable()); private successiveFailures: number = 0; private lastSyncTriggerTime: number | undefined = undefined; @@ -105,7 +117,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } constructor( - @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -113,10 +125,11 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, - @IStorageService storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService + @IStorageService private readonly storageService: IStorageService, + @IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService ) { - super(storageService, environmentService, userDataSyncStoreManagementService); + super(); + this.userDataAutoSyncEnablementService = userDataAutoSyncEnablementService as _IUserDataAutoSyncEnablementService; this.syncTriggerDelayer = this._register(new Delayer(0)); this.lastSyncUrl = this.syncUrl; @@ -135,7 +148,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } })); - if (this.isEnabled()) { + if (this.userDataAutoSyncEnablementService.isEnabled()) { this.logService.info('Auto Sync is enabled.'); } else { this.logService.info('Auto Sync is disabled.'); @@ -174,7 +187,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } /* log message when auto sync is not disabled by user */ - else if (message && this.isEnabled()) { + else if (message && this.userDataAutoSyncEnablementService.isEnabled()) { this.logService.info(message); } } @@ -184,7 +197,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i protected startAutoSync(): boolean { return true; } private isAutoSyncEnabled(): { enabled: boolean, message?: string } { - if (!this.isEnabled()) { + if (!this.userDataAutoSyncEnablementService.isEnabled()) { return { enabled: false, message: 'Auto Sync: Disabled.' }; } if (!this.userDataSyncAccountService.account) { @@ -234,9 +247,9 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } private updateEnablement(enabled: boolean): void { - if (this.isEnabled() !== enabled) { + if (this.userDataAutoSyncEnablementService.isEnabled() !== enabled) { this.telemetryService.publicLog2<{ enabled: boolean }, AutoSyncEnablementClassification>(enablementKey, { enabled }); - this.setEnablement(enabled); + this.userDataAutoSyncEnablementService.setEnablement(enabled); this.updateAutoSync(); } } @@ -323,7 +336,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.stopDisableMachineEventually(); // disable only if sync is disabled - if (!this.isEnabled() && this.userDataSyncAccountService.account) { + if (!this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account) { await this.userDataSyncMachinesService.removeCurrentMachine(); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 52f721f88ad..e654f0dc867 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -460,13 +460,18 @@ export interface IUserDataSyncService { getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise; } +export const IUserDataAutoSyncEnablementService = createDecorator('IUserDataAutoSyncEnablementService'); +export interface IUserDataAutoSyncEnablementService { + _serviceBrand: any; + readonly onDidChangeEnablement: Event; + isEnabled(): boolean; + canToggleEnablement(): boolean; +} + export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; readonly onError: Event; - readonly onDidChangeEnablement: Event; - isEnabled(): boolean; - canToggleEnablement(): boolean; turnOn(): Promise; turnOff(everywhere: boolean): Promise; triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; diff --git a/src/vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService.ts index 6c9d0f00e2a..115f5ff011f 100644 --- a/src/vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -26,9 +25,9 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService, @IStorageService storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, ) { - super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); + super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, userDataAutoSyncEnablementService); this._register(Event.debounce(Event.any( Event.map(nativeHostService.onDidFocusWindow, () => 'windowFocus'), diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index a329a554820..6e821b1bafd 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -38,6 +38,8 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; +import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; export class UserDataSyncClient extends Disposable { @@ -105,6 +107,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IStorageKeysSyncRegistryService, this.instantiationService.createInstance(StorageKeysSyncRegistryService)); this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); + this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService)); this.instantiationService.stub(IExtensionManagementService, >{ async getInstalled() { return []; }, onDidInstallExtension: new Emitter().event, @@ -115,6 +118,7 @@ export class UserDataSyncClient extends Disposable { async getCompatibleExtension() { return null; } }); + this.instantiationService.stub(IUserDataAutoSyncEnablementService, this.instantiationService.createInstance(UserDataAutoSyncEnablementService)); this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); if (!empty) { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 4a1a51b934c..a0fd1ff7a04 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -808,11 +808,22 @@ declare module 'vscode' { */ static readonly Folder: ThemeIcon; + /** + * The id of the icon. The available icons are listed in https://microsoft.github.io/vscode-codicons/dist/codicon.html. + */ + readonly id: string; + + /** + * The optional ThemeColor of the icon. The color is currently only used in [TreeItem](#TreeItem). + */ + readonly themeColor?: ThemeColor; + /** * Creates a reference to a theme icon. * @param id id of the icon. The available icons are listed in https://microsoft.github.io/vscode-codicons/dist/codicon.html. + * @param color optional `ThemeColor` for the icon. The color is currently only used in [TreeItem](#TreeItem). */ - constructor(id: string); + constructor(id: string, color?: ThemeColor); } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5a3de044162..f1af33b0f35 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1529,7 +1529,7 @@ declare module 'vscode' { * * Messages are only delivered if the editor is live. * - * @param message Body of the message. This must be a string or other json serilizable object. + * @param message Body of the message. This must be a string or other json serializable object. */ postMessage(message: any): Thenable; @@ -1731,7 +1731,7 @@ declare module 'vscode' { * * Messages are only delivered if the editor is live. * - * @param message Body of the message. This must be a string or other json serilizable object. + * @param message Body of the message. This must be a string or other json serializable object. */ postMessage(message: any): Thenable; @@ -2159,28 +2159,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/103120 @alexr00 - export class ThemeIcon2 extends ThemeIcon { - - /** - * The id of the icon. The available icons are listed in https://microsoft.github.io/vscode-codicons/dist/codicon.html. - */ - readonly id: string; - - /** - * The optional ThemeColor of the icon. The color is currently only used in [TreeItem](#TreeItem). - */ - readonly themeColor?: ThemeColor; - - /** - * Creates a reference to a theme icon. - * @param id id of the icon. The available icons are listed in https://microsoft.github.io/vscode-codicons/dist/codicon.html. - * @param color optional `ThemeColor` for the icon. The color is currently only used in [TreeItem](#TreeItem). - */ - constructor(id: string, color?: ThemeColor); - } - //#endregion - //#region https://github.com/microsoft/vscode/issues/102665 Comment API @rebornix export interface CommentThread { /** @@ -2190,4 +2168,15 @@ declare module 'vscode' { canReply: boolean; } //#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/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 9e264fb33b9..a4df8523631 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; // --- other interested parties import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint'; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 635730ba75b..64a3f19a3be 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -441,26 +441,25 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }; } - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void { + $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { const provider: modes.CompletionItemProvider = { triggerCharacters, - _debugDisplayName: extensionId.value, - provideCompletionItems: (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise => { - return this._proxy.$provideCompletionItems(handle, model.uri, position, context, token).then(result => { - if (!result) { - return result; - } - - return { - suggestions: result[ISuggestResultDtoField.completions].map(d => MainThreadLanguageFeatures._inflateSuggestDto(result[ISuggestResultDtoField.defaultRanges], d)), - incomplete: result[ISuggestResultDtoField.isIncomplete] || false, - dispose: () => { - if (typeof result.x === 'number') { - this._proxy.$releaseCompletionItems(handle, result.x); - } + _debugDisplayName: displayName, + provideCompletionItems: async (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise => { + const result = await this._proxy.$provideCompletionItems(handle, model.uri, position, context, token); + if (!result) { + return result; + } + return { + suggestions: result[ISuggestResultDtoField.completions].map(d => MainThreadLanguageFeatures._inflateSuggestDto(result[ISuggestResultDtoField.defaultRanges], d)), + incomplete: result[ISuggestResultDtoField.isIncomplete] || false, + duration: result[ISuggestResultDtoField.duration], + dispose: () => { + if (typeof result.x === 'number') { + this._proxy.$releaseCompletionItems(handle, result.x); } - }; - }); + } + }; } }; if (supportsResolveDetails) { @@ -571,13 +570,27 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- folding - $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void { - const proxy = this._proxy; - this._registrations.set(handle, modes.FoldingRangeProviderRegistry.register(selector, { + $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { provideFoldingRanges: (model, context, token) => { - return proxy.$provideFoldingRanges(handle, model.uri, context, token); + return this._proxy.$provideFoldingRanges(handle, model.uri, context, token); } - })); + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChange = emitter.event; + } + + this._registrations.set(handle, modes.FoldingRangeProviderRegistry.register(selector, provider)); + } + + $emitFoldingRangeEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } } // -- smart select diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 2f0b06a143d..0b70be0ca26 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -133,8 +133,7 @@ class MainThreadSCMProvider implements ISCMProvider { private readonly _handle: number, private readonly _contextValue: string, private readonly _label: string, - private readonly _rootUri: URI | undefined, - @ISCMService scmService: ISCMService + private readonly _rootUri: URI | undefined ) { } $updateSourceControl(features: SCMProviderFeatures): void { @@ -290,7 +289,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, this.scmService); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); @@ -398,7 +397,7 @@ export class MainThreadSCM implements MainThreadSCMShape { return; } - repository.input.value = value; + repository.input.setValue(value, false); } $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 350ce8e8753..391cd0cef99 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -434,7 +434,6 @@ export class MainThreadTask implements MainThreadTaskShape { this._proxy.$OnDidEndTask(TaskExecutionDTO.from(task.getTaskExecution())); } }); - this._taskService.setJsonTasksSupported(Promise.resolve(this._proxy.$jsonTasksSupported())); } public dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index c5c03a86786..8e54cb8f41e 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -63,7 +63,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._toDispose.add(_terminalService.onInstanceRequestSpawnExtHostProcess(request => this._onRequestSpawnExtHostProcess(request))); this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.id : null))); - this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => this._onTitleChanged(instance.id, instance.title))); + this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.id, instance.title))); this._toDispose.add(_terminalService.configHelper.onWorkspacePermissionsChanged(isAllowed => this._onWorkspacePermissionsChanged(isAllowed))); this._toDispose.add(_terminalService.onRequestAvailableShells(e => this._onRequestAvailableShells(e))); @@ -111,7 +111,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: launchConfig.env, strictEnv: launchConfig.strictEnv, hideFromUser: launchConfig.hideFromUser, - isExtensionTerminal: launchConfig.isExtensionTerminal + isExtensionTerminal: launchConfig.isExtensionTerminal, + isFeatureTerminal: launchConfig.isFeatureTerminal }; const terminal = this._terminalService.createTerminal(shellLaunchConfig); return Promise.resolve({ diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index c0dbac194c1..561e3c219eb 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -128,9 +128,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc dispose() { super.dispose(); - for (const disposable of this._editorProviders.values()) { - disposable.dispose(); - } + dispose(this._editorProviders.values()); this._editorProviders.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index 346ca9957b6..e369b4f088d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; @@ -28,6 +28,15 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); } + dispose() { + super.dispose(); + + dispose(this._webviewViewProviders.values()); + this._webviewViewProviders.clear(); + + dispose(this._webviewViews.values()); + } + public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void { const webviewView = this.getWebviewView(handle); webviewView.title = value; @@ -54,7 +63,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc const extension = reviveWebviewExtension(extensionData); - this._webviewViewService.register(viewType, { + const registration = this._webviewViewService.register(viewType, { resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { const handle = webviewView.webview.id; @@ -93,6 +102,8 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc } } }); + + this._webviewViewProviders.set(viewType, registration); } public $unregisterWebviewViewProvider(viewType: string): void { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index f88bdb42230..d6a1d810a08 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -13,7 +13,7 @@ import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { coalesce, } from 'vs/base/common/arrays'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d4393a332bd..284c6aff854 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1173,7 +1173,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind, ThemeColor: extHostTypes.ThemeColor, ThemeIcon: extHostTypes.ThemeIcon, - ThemeIcon2: extHostTypes.ThemeIcon, TreeItem: extHostTypes.TreeItem, TreeItem2: extHostTypes.TreeItem, TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ddca6becf25..bbb93b68860 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -394,11 +394,12 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend, eventHandle: number | undefined): void; $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; + $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; - $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitFoldingRangeEvent(eventHandle: number, event?: any): void; $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; @@ -446,6 +447,7 @@ export interface TerminalLaunchConfig { strictEnv?: boolean; hideFromUser?: boolean; isExtensionTerminal?: boolean; + isFeatureTerminal?: boolean; } export interface MainThreadTerminalServiceShape extends IDisposable { @@ -1214,13 +1216,15 @@ export interface ISuggestDataDto { export const enum ISuggestResultDtoField { defaultRanges = 'a', completions = 'b', - isIncomplete = 'c' + isIncomplete = 'c', + duration = 'd', } export interface ISuggestResultDto { [ISuggestResultDtoField.defaultRanges]: { insert: IRange, replace: IRange; }; [ISuggestResultDtoField.completions]: ISuggestDataDto[]; [ISuggestResultDtoField.isIncomplete]: undefined | true; + [ISuggestResultDtoField.duration]: number; x?: number; } diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index a5d97d15a11..02165712177 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -10,6 +10,11 @@ import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthen import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +interface GetSessionsRequest { + scopes: string; + result: Promise; +} + export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _proxy: MainThreadAuthenticationShape; private _authenticationProviders: Map = new Map(); @@ -27,6 +32,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangePassword = new Emitter(); readonly onDidChangePassword: Event = this._onDidChangePassword.event; + private _inFlightRequests = new Map(); + constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } @@ -50,10 +57,30 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { + const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); + const inFlightRequests = this._inFlightRequests.get(extensionId) || []; + const sortedScopes = scopes.sort().join(' '); + let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.scopes === sortedScopes); + + if (inFlightRequest) { + return inFlightRequest.result; + } else { + const session = this._getSession(requestingExtension, extensionId, providerId, scopes, options); + inFlightRequest = { + scopes: sortedScopes, + result: session + }; + + inFlightRequests.push(inFlightRequest); + this._inFlightRequests.set(extensionId, inFlightRequests); + return session; + } + } + + private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); const provider = this._authenticationProviders.get(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); if (!provider) { return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); @@ -62,20 +89,20 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const orderedScopes = scopes.sort().join(' '); const sessions = (await provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); + let session: vscode.AuthenticationSession | undefined = undefined; if (sessions.length) { if (!provider.supportsMultipleAccounts) { - const session = sessions[0]; + session = sessions[0]; const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName); - if (allowed) { - return session; - } else { + if (!allowed) { throw new Error('User did not consent to login.'); } + } else { + // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid + const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + session = sessions.find(session => session.id === selected.id); } - // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); - return sessions.find(session => session.id === selected.id); } else { if (options.createIfNone) { const isAllowed = await this._proxy.$loginPrompt(provider.label, extensionName); @@ -83,14 +110,21 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error('User did not consent to login.'); } - const session = await provider.login(scopes); + session = await provider.login(scopes); await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); - return session; } else { await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); - return undefined; } } + + const inFlightRequests = this._inFlightRequests.get(extensionId) || []; + const requestIndex = inFlightRequests.findIndex(request => request.scopes === scopes.sort().join(' ')); + if (requestIndex > -1) { + inFlightRequests.splice(requestIndex); + this._inFlightRequests.set(extensionId, inFlightRequests); + } + + return session; } async logout(providerId: string, sessionId: string): Promise { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ef49bb42954..8f14bc22f4d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -31,6 +31,7 @@ import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semantic 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'; // --- adapter @@ -896,6 +897,7 @@ class SuggestAdapter { const replaceRange = doc.getWordRangeAtPosition(pos) || new Range(pos, pos); const insertRange = replaceRange.with({ end: pos }); + const sw = new StopWatch(true); const itemsOrList = await asPromise(() => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.to(context))); if (!itemsOrList) { @@ -921,7 +923,8 @@ class SuggestAdapter { x: pid, [extHostProtocol.ISuggestResultDtoField.completions]: completions, [extHostProtocol.ISuggestResultDtoField.defaultRanges]: { replace: typeConvert.Range.from(replaceRange), insert: typeConvert.Range.from(insertRange) }, - [extHostProtocol.ISuggestResultDtoField.isIncomplete]: list.isIncomplete || undefined + [extHostProtocol.ISuggestResultDtoField.isIncomplete]: list.isIncomplete || undefined, + [extHostProtocol.ISuggestResultDtoField.duration]: sw.elapsed() }; for (let i = 0; i < list.items.length; i++) { @@ -1740,7 +1743,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); - this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), extension.identifier); + this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); return this._createDisposable(handle); } @@ -1810,10 +1813,20 @@ 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.FoldingRangeProvider): vscode.Disposable { - const handle = this._addNewAdapter(new FoldingProviderAdapter(this._documents, provider), extension); - this._proxy.$registerFoldingRangeProvider(handle, this._transformDocumentSelector(selector)); - return this._createDisposable(handle); + registerFoldingRangeProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider2): vscode.Disposable { + const handle = this._nextHandle(); + const eventHandle = typeof provider.onDidChangeFoldingRanges === 'function' ? this._nextHandle() : undefined; + + this._adapter.set(handle, new AdapterData(new FoldingProviderAdapter(this._documents, provider), extension)); + this._proxy.$registerFoldingRangeProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeFoldingRanges!(_ => this._proxy.$emitFoldingRangeEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + + return result; } $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 1a87b23acb6..4adf04afdc6 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -446,24 +446,17 @@ function getIconUris(iconPath: QuickInputButton['iconPath']): { dark: URI, light if (iconPath instanceof ThemeIcon) { return { id: iconPath.id }; } - const dark = getDarkIconUri(iconPath as any); - const light = getLightIconUri(iconPath as any); + const dark = getDarkIconUri(iconPath as URI | { light: URI; dark: URI; }); + const light = getLightIconUri(iconPath as URI | { light: URI; dark: URI; }); return { dark, light }; } -function getLightIconUri(iconPath: string | URI | { light: URI; dark: URI; }) { - return getIconUri(typeof iconPath === 'object' && 'light' in iconPath ? iconPath.light : iconPath); +function getLightIconUri(iconPath: URI | { light: URI; dark: URI; }) { + return typeof iconPath === 'object' && 'light' in iconPath ? iconPath.light : iconPath; } -function getDarkIconUri(iconPath: string | URI | { light: URI; dark: URI; }) { - return getIconUri(typeof iconPath === 'object' && 'dark' in iconPath ? iconPath.dark : iconPath); -} - -function getIconUri(iconPath: string | URI) { - if (URI.isUri(iconPath)) { - return iconPath; - } - return URI.file(iconPath); +function getDarkIconUri(iconPath: URI | { light: URI; dark: URI; }) { + return typeof iconPath === 'object' && 'dark' in iconPath ? iconPath.dark : iconPath; } class ExtHostQuickPick extends ExtHostQuickInput implements QuickPick { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index d722f2b428e..1cccd958fb6 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -9,7 +9,7 @@ import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShap import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITerminalChildProcess, ITerminalDimensions, EXT_HOST_CREATION_DELAY, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalChildProcess, EXT_HOST_CREATION_DELAY, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { timeout } from 'vs/base/common/async'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; @@ -36,7 +36,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidWriteTerminalData: Event; createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; - createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal; + createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; @@ -130,9 +130,10 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi env?: { [key: string]: string | null }, waitOnExit?: boolean, strictEnv?: boolean, - hideFromUser?: boolean + hideFromUser?: boolean, + isFeatureTerminal?: boolean ): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser }); + const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); this._name = result.name; this._runQueuedRequests(result.id); } @@ -245,8 +246,8 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = new Emitter(); public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = new Emitter(); - public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } + private readonly _onProcessOverrideDimensions = new Emitter(); + public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } constructor(private readonly _pty: vscode.Pseudoterminal) { } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 5406cfa39d1..d7194fc2317 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -601,9 +601,6 @@ class ExtHostTreeView extends Disposable { } private getThemeIcon(extensionTreeItem: vscode.TreeItem2): ThemeIcon | undefined { - if ((extensionTreeItem.iconPath instanceof ThemeIcon) && extensionTreeItem.iconPath.themeColor) { - checkProposedApiEnabled(this.extension); - } return extensionTreeItem.iconPath instanceof ThemeIcon ? extensionTreeItem.iconPath : undefined; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index ef8a0019524..93cb324ff2d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2729,7 +2729,7 @@ export enum DebugConfigurationProviderTriggerKind { @es5ClassCompat export class QuickInputButtons { - static readonly Back: vscode.QuickInputButton = { iconPath: 'back.svg' }; + static readonly Back: vscode.QuickInputButton = { iconPath: new ThemeIcon('arrow-left') }; private constructor() { } } diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 4206b06776a..c68c242a4a8 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -100,7 +100,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwd: args.cwd, name: args.title || nls.localize('debug.terminal.title', "debuggee"), }; - this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options); + this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options, true); } else { cwdForPrepareCommand = args.cwd; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index e40d7763acf..0cb87355ad3 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -53,10 +53,10 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { return terminal; } - public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { + public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, options, options.name); this._terminals.push(terminal); - terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env, /*options.waitOnExit*/ undefined, options.strictEnv, options.hideFromUser); + terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env, /*options.waitOnExit*/ undefined, options.strictEnv, options.hideFromUser, isFeatureTerminal); return terminal; } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index b62d60bf29e..376a2a46884 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -28,6 +28,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { Codicon } from 'vs/base/common/codicons'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); @@ -807,7 +808,7 @@ export abstract class BaseResizeViewAction extends Action { super(id, label); } - protected resizePart(sizeChange: number): void { + protected resizePart(widthChange: number, heightChange: number): void { const isEditorFocus = this.layoutService.hasFocus(Parts.EDITOR_PART); const isSidebarFocus = this.layoutService.hasFocus(Parts.SIDEBAR_PART); const isPanelFocus = this.layoutService.hasFocus(Parts.PANEL_PART); @@ -822,7 +823,7 @@ export abstract class BaseResizeViewAction extends Action { } if (part) { - this.layoutService.resizePart(part, sizeChange); + this.layoutService.resizePart(part, widthChange, heightChange); } } } @@ -841,7 +842,43 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { } async run(): Promise { - this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT); + this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, BaseResizeViewAction.RESIZE_INCREMENT); + } +} + +export class IncreaseViewWidthAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.increaseViewWidth'; + static readonly LABEL = nls.localize('increaseViewWidth', "Increase Current View Width"); + + constructor( + id: string, + label: string, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(id, label, layoutService); + } + + async run(): Promise { + this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT, 0); + } +} + +export class IncreaseViewHeightAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.increaseViewHeight'; + static readonly LABEL = nls.localize('increaseViewHeight', "Increase Current View Height"); + + constructor( + id: string, + label: string, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(id, label, layoutService); + } + + async run(): Promise { + this.resizePart(0, BaseResizeViewAction.RESIZE_INCREMENT); } } @@ -860,9 +897,50 @@ export class DecreaseViewSizeAction extends BaseResizeViewAction { } async run(): Promise { - this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT); + this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, -BaseResizeViewAction.RESIZE_INCREMENT); + } +} + +export class DecreaseViewWidthAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.decreaseViewWidth'; + static readonly LABEL = nls.localize('decreaseViewWidth', "Decrease Current View Width"); + + constructor( + id: string, + label: string, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(id, label, layoutService); + } + + async run(): Promise { + this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT, 0); + } +} + + +export class DecreaseViewHeightAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.decreaseViewHeight'; + static readonly LABEL = nls.localize('decreaseViewHeight', "Decrease Current View Height"); + + constructor( + id: string, + label: string, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(id, label, layoutService); + } + + async run(): Promise { + this.resizePart(0, -BaseResizeViewAction.RESIZE_INCREMENT); } } registry.registerWorkbenchAction(SyncActionDescriptor.from(IncreaseViewSizeAction, undefined), 'View: Increase Current View Size', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(IncreaseViewWidthAction, undefined), 'View: Increase Editor Width', CATEGORIES.View.value, EditorContextKeys.focus); +registry.registerWorkbenchAction(SyncActionDescriptor.from(IncreaseViewHeightAction, undefined), 'View: Increase Editor Height', CATEGORIES.View.value, EditorContextKeys.focus); registry.registerWorkbenchAction(SyncActionDescriptor.from(DecreaseViewSizeAction, undefined), 'View: Decrease Current View Size', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DecreaseViewWidthAction, undefined), 'View: Decrease Editor Width', CATEGORIES.View.value, EditorContextKeys.focus); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DecreaseViewHeightAction, undefined), 'View: Decrease Editor Height', CATEGORIES.View.value, EditorContextKeys.focus); diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index b9198a1e02f..7344a3a29b3 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -18,7 +18,7 @@ import { Direction } from 'vs/base/browser/ui/grid/grid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isAncestor } from 'vs/base/browser/dom'; diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index 33856f6f6cf..8b63f052b9c 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { EventHelper } from 'vs/base/browser/dom'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isNative } from 'vs/base/common/platform'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 1115d77f027..e5fdcdbca0a 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -403,9 +403,9 @@ KeybindingsRegistry.registerKeybindingRule({ CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => { const configurationService = accessor.get(IConfigurationService); - const setting = configurationService.inspect('window.confirmBeforeClose').userValue; + const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; - return configurationService.updateValue('window.confirmBeforeClose', setting === false ? true : false, ConfigurationTarget.USER); + return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER); }); // --- Menu Registration @@ -415,7 +415,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { command: { id: 'workbench.action.toggleConfirmBeforeClose', title: nls.localize('miConfirmClose', "Confirm Before Close"), - toggled: ContextKeyExpr.equals('config.window.confirmBeforeClose', true) + toggled: ContextKeyExpr.notEquals('config.window.confirmBeforeClose', 'never') }, order: 1, when: IsWebContext diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 1e9fb3756bb..10e7b53ef34 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext } from 'vs/workbench/common/editor'; -import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { trackFocus, addDisposableListener, EventType, WebFileSystemAccess } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -32,6 +32,9 @@ export const RemoteNameContext = new RawContextKey('remoteName', ''); export const IsFullscreenContext = new RawContextKey('isFullscreen', false); +// Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access) +export const HasWebFileSystemAccess = new RawContextKey('hasWebFileSystemAccess', false); + export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; @@ -85,6 +88,9 @@ export class WorkbenchContextKeysHandler extends Disposable { RemoteNameContext.bindTo(this.contextKeyService).set(getRemoteName(this.environmentService.remoteAuthority) || ''); + // Capabilities + HasWebFileSystemAccess.bindTo(this.contextKeyService).set(WebFileSystemAccess.supported(window)); + // Development IsDevelopmentContext.bindTo(this.contextKeyService).set(!this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 924fe6a25cb..5984484d21e 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -511,7 +511,7 @@ class ResourceLabelWidget extends IconLabel { const resource = toResource(this.label); const label = this.label.name; - if (this.options && typeof this.options.title === 'string') { + if (this.options && (this.options.title !== undefined)) { iconLabelOptions.title = this.options.title; } else if (resource && resource.scheme !== Schemas.data /* do not accidentally inline Data URIs */) { if (!this.computedPathLabel) { @@ -541,7 +541,7 @@ class ResourceLabelWidget extends IconLabel { if (deco) { this.renderDisposables.add(deco); - if (deco.tooltip) { + if (deco.tooltip && (typeof iconLabelOptions.title === 'string')) { iconLabelOptions.title = `${iconLabelOptions.title} • ${deco.tooltip}`; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 02e4ac93844..d2bd0558df2 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -22,7 +22,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase, StartupKind, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, IPath } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IEditor } from 'vs/editor/common/editorCommon'; @@ -43,7 +43,7 @@ import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from 'vs/workbench/commo import { LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { URI } from 'vs/base/common/uri'; -import { IViewDescriptorService, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { mark } from 'vs/base/common/performance'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -178,7 +178,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private titleService!: ITitleService; private viewletService!: IViewletService; private viewDescriptorService!: IViewDescriptorService; - private viewsService!: IViewsService; private contextService!: IWorkspaceContextService; private backupFileService!: IBackupFileService; private notificationService!: INotificationService; @@ -273,7 +272,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.panelService = accessor.get(IPanelService); this.viewletService = accessor.get(IViewletService); this.viewDescriptorService = accessor.get(IViewDescriptorService); - this.viewsService = accessor.get(IViewsService); this.titleService = accessor.get(ITitleService); this.notificationService = accessor.get(INotificationService); this.activityBarService = accessor.get(IActivityBarService); @@ -851,52 +849,60 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (this.state.views.defaults?.length) { mark('willOpenDefaultViews'); - const defaultViews = [...this.state.views.defaults]; + let locationsRestored: { id: string; order: number }[] = []; - let locationsRestored: boolean[] = []; + const tryOpenView = (view: { id: string; order: number }): boolean => { + const location = this.viewDescriptorService.getViewLocationById(view.id); + if (location !== null) { + const container = this.viewDescriptorService.getViewContainerByViewId(view.id); + if (container) { + if (view.order >= (locationsRestored?.[location]?.order ?? 0)) { + locationsRestored[location] = { id: container.id, order: view.order }; + } - const tryOpenView = async (viewId: string, index: number) => { - const location = this.viewDescriptorService.getViewLocationById(viewId); - if (location) { + const containerModel = this.viewDescriptorService.getViewContainerModel(container); + containerModel.setCollapsed(view.id, false); + containerModel.setVisible(view.id, true); - // If the view is in the same location that has already been restored, remove it and continue - if (locationsRestored[location]) { - defaultViews.splice(index, 1); - - return; - } - - const view = await this.viewsService.openView(viewId); - if (view) { - locationsRestored[location] = true; - defaultViews.splice(index, 1); + return true; } } + + return false; }; - let i = -1; - for (const viewId of defaultViews) { - await tryOpenView(viewId, ++i); + const defaultViews = [...this.state.views.defaults].reverse().map((v, index) => ({ id: v, order: index })); + + let i = defaultViews.length; + while (i) { + i--; + if (tryOpenView(defaultViews[i])) { + defaultViews.splice(i, 1); + } } // If we still have views left over, wait until all extensions have been registered and try again if (defaultViews.length) { await this.extensionService.whenInstalledExtensionsRegistered(); - let i = -1; - for (const viewId of defaultViews) { - await tryOpenView(viewId, ++i); + + let i = defaultViews.length; + while (i) { + i--; + if (tryOpenView(defaultViews[i])) { + defaultViews.splice(i, 1); + } } } // If we opened a view in the sidebar, stop any restore there if (locationsRestored[ViewContainerLocation.Sidebar]) { - this.state.sideBar.viewletToRestore = undefined; + this.state.sideBar.viewletToRestore = locationsRestored[ViewContainerLocation.Sidebar].id; } // If we opened a view in the panel, stop any restore there if (locationsRestored[ViewContainerLocation.Panel]) { - this.state.panel.panelToRestore = undefined; + this.state.panel.panelToRestore = locationsRestored[ViewContainerLocation.Panel].id; } mark('didOpenDefaultViews'); @@ -1095,7 +1101,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const availableWidth = this.dimension.width - takenWidth; const availableHeight = this.dimension.height - takenHeight; - return { width: availableWidth, height: availableHeight }; + return new Dimension(availableWidth, availableHeight); } getWorkbenchContainer(): HTMLElement { @@ -1397,9 +1403,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this._onCenteredLayoutChange.fire(this.state.editor.centered); } - resizePart(part: Parts, sizeChange: number): void { - const sizeChangePxWidth = this.workbenchGrid.width * sizeChange / 100; - const sizeChangePxHeight = this.workbenchGrid.height * sizeChange / 100; + resizePart(part: Parts, sizeChangeWidth: number, sizeChangeHeight: number): void { + const sizeChangePxWidth = this.workbenchGrid.width * sizeChangeWidth / 100; + const sizeChangePxHeight = this.workbenchGrid.height * sizeChangeHeight / 100; let viewSize: IViewSize; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index cfd194a7466..6a373572a36 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -11,7 +11,7 @@ import { Part } from 'vs/workbench/browser/part'; import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem, ACCOUNTS_VISIBILITY_PREFERENCE_KEY } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; @@ -41,6 +41,8 @@ import { Action, Separator } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; interface IPlaceholderViewContainer { id: string; @@ -1042,4 +1044,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } +class FocusActivityBarAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.focusActivityBar', + title: { value: nls.localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, + category: CATEGORIES.View, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const activityBarService = accessor.get(IActivityBarService); + activityBarService.focusActivityBar(); + } +} + registerSingleton(IActivityBarService, ActivitybarPart); +registerAction2(FocusActivityBarAction); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index d6d1c7a74ba..06a031a5632 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -227,7 +227,8 @@ export class CompositeBar extends Widget implements ICompositeBar { ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), animated: false, preventLoopNavigation: this.options.preventLoopNavigation, - ignoreOrientationForPreviousAndNextKey: true + ignoreOrientationForPreviousAndNextKey: true, + triggerKeys: { keyDown: true } })); // Contextmenu for composites diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 84de9096251..ee234378560 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -483,7 +483,7 @@ export abstract class CompositePart extends Part { super.layout(width, height); // Layout contents - this.contentAreaSize = super.layoutContents(width, height).contentSize; + this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize); // Layout composite if (this.activeComposite) { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 31f947d8e1b..05f02021e20 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -46,7 +46,7 @@ import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/edit import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalResource } from 'vs/base/common/resources'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index e6d4dd3491f..d01c17c4aa6 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -340,7 +340,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Fill in contributed actions const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(menu, undefined, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(menu, undefined, actions); // Show it this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 8b577f47ef9..e6e216a5ed4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1075,7 +1075,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro const contentAreaSize = super.layoutContents(width, height).contentSize; // Layout editor container - this.doLayout(contentAreaSize); + this.doLayout(Dimension.lift(contentAreaSize)); } private doLayout(dimension: Dimension): void { diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 49944a9ae70..66a7caec173 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -9,7 +9,7 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, isAncestor } from 'vs/base/browser/dom'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Color } from 'vs/base/common/color'; @@ -110,6 +110,16 @@ export class NoTabsTitleControl extends TitleControl { } private onTitleTap(e: GestureEvent): void { + + // We only want to open the quick access picker when + // the tap occured over the editor label, so we need + // to check on the target + // (https://github.com/microsoft/vscode/issues/107543) + const target = e.initialTarget; + if (!(target instanceof HTMLElement) || !this.editorLabel || !isAncestor(target, this.editorLabel.element)) { + return; + } + // TODO@rebornix gesture tap should open the quick access // editorGroupView will focus on the editor again when there are mouse/pointer/touch down events // we need to wait a bit as `GesureEvent.Tap` is generated from `touchstart` and then `touchend` evnets, which are not an atom event. diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 8a7695ce6bd..bed3f20944d 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -47,6 +47,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPath, win32, posix } from 'vs/base/common/path'; import { insert } from 'vs/base/common/arrays'; import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { isSafari } from 'vs/base/browser/browser'; interface IEditorInputLabel { name?: string; @@ -1283,7 +1284,7 @@ export class TabsTitleControl extends TitleControl { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); + this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; } } @@ -1631,7 +1632,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = } // Fade out styles via linear gradient (when tabs are set to shrink) - if (theme.type !== 'hc') { + // But not when: + // - in high contrast theme + // - if we have a contrast border (which draws an outline - https://github.com/microsoft/vscode/issues/109117) + // - on Safari (https://github.com/microsoft/vscode/issues/108996) + if (theme.type !== 'hc' && !isSafari && !activeContrastBorderColor) { const workbenchBackground = WORKBENCH_BACKGROUND(theme); const editorBackgroundColor = theme.getColor(editorBackground); const editorGroupHeaderTabsBackground = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index ef49ef6ed5d..8358dfcbaac 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -347,7 +347,7 @@ export abstract class TitleControl extends Themable { // Fill in contributed actions const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true, arg: this.resourceContext.get() }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true, arg: this.resourceContext.get() }, actions); // Show it this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 7d7056fad8f..6e8b4e940f1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -59,7 +59,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente private registerListeners(): void { this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); - this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); + this._register(this.layoutService.onLayout(dimension => this.layout(Dimension.lift(dimension)))); } get isVisible(): boolean { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 3ed4c64890f..bbf8896aba5 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -19,7 +19,7 @@ import { NotificationsToastsVisibleContext, INotificationsToastController } from import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Severity, NotificationsFilter } from 'vs/platform/notification/common/notification'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IntervalCounter, timeout } from 'vs/base/common/async'; import { assertIsDefined } from 'vs/base/common/types'; @@ -90,7 +90,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast private registerListeners(): void { // Layout - this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); + this._register(this.layoutService.onLayout(dimension => this.layout(Dimension.lift(dimension)))); // Delay some tasks until after we can show notifications this.onCanShowNotifications().then(() => { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index ce83ddf403b..ced7b6480a6 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -21,7 +21,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor } from 'vs/base/browser/dom'; +import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -39,6 +39,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons'; interface IPendingStatusbarEntry { id: string; @@ -64,17 +65,18 @@ class StatusbarViewModel extends Disposable { static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden'; + private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); + readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; + private readonly _entries: IStatusbarViewModelEntry[] = []; get entries(): IStatusbarViewModelEntry[] { return this._entries; } - private hidden!: Set; + private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; get lastFocusedEntry(): IStatusbarViewModelEntry | undefined { return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined; } - private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; - private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); - readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; + private hidden!: Set; constructor(private readonly storageService: IStorageService) { super(); @@ -706,12 +708,67 @@ export class StatusbarPart extends Part implements IStatusbarService { } } +class StatusBarCodiconLabel extends CodiconLabel { + + private readonly progressCodicon = renderCodicon('sync', 'spin'); + + private currentText = ''; + private currentShowProgress = false; + + constructor( + private readonly container: HTMLElement + ) { + super(container); + } + + set showProgress(showProgress: boolean) { + if (this.currentShowProgress !== showProgress) { + this.currentShowProgress = showProgress; + this.text = this.currentText; + } + } + + set text(text: string) { + + // Progress: insert progress codicon as first element as needed + // but keep it stable so that the animation does not reset + if (this.currentShowProgress) { + + // Append as needed + if (this.container.firstChild !== this.progressCodicon) { + this.container.appendChild(this.progressCodicon); + } + + // Remove others + for (const node of Array.from(this.container.childNodes)) { + if (node !== this.progressCodicon) { + node.remove(); + } + } + + // If we have text to show, add a space to separate from progress + let textContent = text ?? ''; + if (textContent) { + textContent = ` ${textContent}`; + } + + // Append new elements + appendChildren(this.container, ...renderCodicons(textContent)); + } + + // No Progress: no special handling + else { + super.text = text; + } + } +} + class StatusbarEntryItem extends Disposable { - private entry!: IStatusbarEntry; + readonly labelContainer: HTMLElement; + private readonly label: StatusBarCodiconLabel; - labelContainer!: HTMLElement; - private label!: CodiconLabel; + private entry: IStatusbarEntry | undefined = undefined; private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); @@ -729,26 +786,25 @@ class StatusbarEntryItem extends Disposable { ) { super(); - this.create(); - this.update(entry); - } - - private create(): void { - // Label Container this.labelContainer = document.createElement('a'); this.labelContainer.tabIndex = -1; // allows screen readers to read title, but still prevents tab focus. this.labelContainer.setAttribute('role', 'button'); - // Label - this.label = new CodiconLabel(this.labelContainer); + // Label (with support for progress) + this.label = new StatusBarCodiconLabel(this.labelContainer); // Add to parent this.container.appendChild(this.labelContainer); + + this.update(entry); } update(entry: IStatusbarEntry): void { + // Update: Progress + this.label.showProgress = !!entry.showProgress; + // Update: Text if (!this.entry || entry.text !== this.entry.text) { this.label.text = entry.text; @@ -760,8 +816,9 @@ class StatusbarEntryItem extends Disposable { } } + // Set the aria label on both elements so screen readers would read + // the correct thing without duplication #96210 if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) { - // Set the aria label on both elements so screen readers would read the correct thing without duplication #96210 this.container.setAttribute('aria-label', entry.ariaLabel); this.labelContainer.setAttribute('aria-label', entry.ariaLabel); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index ddb8f2fd842..1dc512aa73c 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -429,7 +429,7 @@ export class TitlebarPart extends Part implements ITitleService { // Fill in contributed actions const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, undefined, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, undefined, actions); // Show it this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index ebe6736547c..3669114bf2a 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -46,6 +46,9 @@ export class TreeViewPane extends ViewPane { if (options.title !== this.treeView.title) { this.updateTitle(this.treeView.title); } + if (options.titleDescription !== this.treeView.description) { + this.updateTitleDescription(this.treeView.description); + } this.updateTreeVisibility(); } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 3024b15fdba..4fa93931ef7 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -305,6 +305,8 @@ export abstract class ViewPane extends Pane implements IView { this._register(this.toolbar); this.setActions(); + this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { this.updateTitle(this.title); })); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 9c6dcaa9556..687fbdb8823 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -326,7 +326,7 @@ export class ViewsService extends Disposable implements IViewsService { return null; } - async openView(id: string, focus: boolean): Promise { + async openView(id: string, focus?: boolean): Promise { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(id); if (!viewContainer) { return null; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 50755cf112a..5481f5e2f1d 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -44,7 +44,7 @@ import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/wi import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { WebResourceIdentityService, IResourceIdentityService } from 'vs/workbench/services/resourceIdentity/common/resourceIdentityService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { BrowserRequestService } from 'vs/workbench/services/request/browser/requestService'; @@ -52,6 +52,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; import { UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; class BrowserMain extends Disposable { @@ -97,11 +98,13 @@ class BrowserMain extends Disposable { // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); + const lifecycleService = accessor.get(ILifecycleService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) - } + }, + shutdown: () => lifecycleService.shutdown() }; }); } @@ -127,7 +130,7 @@ class BrowserMain extends Disposable { // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { if (storageService.hasPendingUpdate) { - console.warn('Unload prevented: pending storage update'); + console.warn('Unload veto: pending storage update'); event.veto(true); // prevent data loss from pending storage update } })); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index c28dada37fb..255f9cac587 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -404,9 +404,15 @@ import { isStandalone } from 'vs/base/browser/browser'; 'markdownDescription': nls.localize('openFoldersInNewWindow', "Controls whether folders should open in a new window or replace the last active window.\nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).") }, 'window.confirmBeforeClose': { - 'type': 'boolean', - 'default': isWeb && !isStandalone, // on by default in web, unless PWA - 'description': nls.localize('confirmBeforeCloseWeb', "Controls whether to ask for confirmation before closing the browser tab or window."), + 'type': 'string', + 'enum': ['always', 'keyboardOnly', 'never'], + 'enumDescriptions': [ + nls.localize('window.confirmBeforeClose.always', "Always ask for confirmation."), + nls.localize('window.confirmBeforeClose.keyboardOnly', "Only ask for confirmation if the browser tab or window was closed by pressing a keybinding."), + nls.localize('window.confirmBeforeClose.never', "Never explicitly ask for confirmation unless data loss is imminent.") + ], + 'default': isWeb && !isStandalone ? 'keyboardOnly' : 'never', // on by default in web, unless PWA + 'description': nls.localize('confirmBeforeCloseWeb', "Controls whether to ask for confirmation before closing the browser tab or window. Independent of this setting, there will always be a confirmation to prevent data loss."), 'scope': ConfigurationScope.APPLICATION, 'included': isWeb } diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index a340d1aff08..dce86a91142 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -21,7 +21,7 @@ import { IStorageService, WillSaveStateReason, StorageScope } from 'vs/platform/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { LifecyclePhase, ILifecycleService, WillShutdownEvent, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, ILifecycleService, WillShutdownEvent, BeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { NotificationService } from 'vs/workbench/services/notification/common/notificationService'; import { NotificationsCenter } from 'vs/workbench/browser/parts/notifications/notificationsCenter'; diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index f739d5d4b8e..de82ce88e15 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -10,7 +10,7 @@ import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/c import { SyncActionDescriptor, MenuRegistry, MenuId, ICommandAction } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index 9625606b070..56ab4b25eb4 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { runWhenIdle, IdleDeadline } from 'vs/base/common/async'; diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fb4a4d67db6..f7451eda51d 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -306,31 +306,31 @@ export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSectio dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, hc: EDITOR_DRAG_AND_DROP_BACKGROUND, -}, nls.localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), hc: null -}, nls.localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { dark: null, light: null, hc: null -}, nls.localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder -}, nls.localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { dark: PANEL_BORDER, light: PANEL_BORDER, hc: PANEL_BORDER -}, nls.localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); // < --- Status --- > @@ -521,25 +521,25 @@ export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBack dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, hc: EDITOR_DRAG_AND_DROP_BACKGROUND, -}, nls.localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), hc: null -}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { dark: SIDE_BAR_FOREGROUND, light: SIDE_BAR_FOREGROUND, hc: SIDE_BAR_FOREGROUND -}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder -}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); // < --- Title Bar --- > diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index cb690ac93e7..24f43b1d740 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -184,7 +184,7 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe } getViewContainerLocation(container: ViewContainer): ViewContainerLocation { - return [...this.viewContainers.keys()].filter(location => this.getViewContainers(location).filter(viewContainer => viewContainer.id === container.id).length > 0)[0]; + return [...this.viewContainers.keys()].filter(location => this.getViewContainers(location).filter(viewContainer => viewContainer?.id === container.id).length > 0)[0]; } getDefaultViewContainer(location: ViewContainerLocation): ViewContainer | undefined { diff --git a/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts index 50f14d80aea..c68942985a5 100644 --- a/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts +++ b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; // Register Backup Tracker diff --git a/src/vs/workbench/contrib/backup/browser/backupTracker.ts b/src/vs/workbench/contrib/backup/browser/backupTracker.ts index f4be13d34b9..e28ef9cd9a3 100644 --- a/src/vs/workbench/contrib/backup/browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/browser/backupTracker.ts @@ -7,7 +7,7 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopy, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; @@ -72,7 +72,7 @@ export class BrowserBackupTracker extends BackupTracker implements IWorkbenchCon for (const dirtyWorkingCopy of dirtyWorkingCopies) { if (!this.backupFileService.hasBackupSync(dirtyWorkingCopy.resource, this.getContentVersion(dirtyWorkingCopy))) { - console.warn('Unload prevented: pending backups'); + console.warn('Unload veto: pending backups'); return true; // dirty without backup: veto } } diff --git a/src/vs/workbench/contrib/backup/common/backup.contribution.ts b/src/vs/workbench/contrib/backup/common/backup.contribution.ts index e51ea2d1aba..0ae3302ffaf 100644 --- a/src/vs/workbench/contrib/backup/common/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/common/backup.contribution.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; // Register Backup Restorer Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index c32a30eeca2..cc08328c69c 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -9,7 +9,7 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IUntitledTextResourceEditorInput, IEditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputWithOptions } from 'vs/workbench/common/editor'; import { toLocalResource, isEqual } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index b79002d2009..d8f35fc9dae 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -7,7 +7,7 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; -import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; export abstract class BackupTracker extends Disposable { diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts index 43a574d8bf4..cf529c7e698 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; // Register Backup Tracker diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index 51cc964f498..d2c4f92fbd5 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -8,7 +8,7 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ConfirmResult, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index ae5b2e4a3f6..f2e134c7c25 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -31,7 +31,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; -import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index feeaaabdce3..139ee6ddbc3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 90142bddf01..f36f5f0e9c3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -364,7 +364,7 @@ export class BulkEditPane extends ViewPane { private _onContextMenu(e: ITreeContextMenuEvent): void { const menu = this._menuService.createMenu(MenuId.BulkEditContext, this._contextKeyService); const actions: IAction[] = []; - const disposable = createAndFillInContextMenuActions(menu, undefined, actions, this._contextMenuService); + const disposable = createAndFillInContextMenuActions(menu, undefined, actions); this._contextMenuService.showContextMenu({ getActions: () => actions, diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index e51a91dc96b..b6866acaaff 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -150,7 +150,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { protected _fillBody(parent: HTMLElement): void { this._layoutInfo = LayoutInfo.retrieve(this._storageService); - this._dim = { height: 0, width: 0 }; + this._dim = new Dimension(0, 0); this._parent = parent; parent.classList.add('call-hierarchy'); @@ -427,7 +427,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { protected _doLayoutBody(height: number, width: number): void { if (this._dim.height !== height || this._dim.width !== width) { super._doLayoutBody(height, width); - this._dim = { height, width }; + this._dim = new Dimension(width, height); this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height; this._splitView.layout(width); this._splitView.resizeView(0, width * this._layoutInfo.ratio); diff --git a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts index c7969c9d1c0..36a0a7d2a17 100644 --- a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts +++ b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts @@ -5,7 +5,7 @@ import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { CodeActionsContribution, editorConfiguration } from 'vs/workbench/contrib/codeActions/common/codeActionsContribution'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index f76fc93385e..cd181b13ca3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -282,7 +282,6 @@ class ShowAccessibilityHelpAction extends EditorAction { alias: 'Show Accessibility Help', precondition: undefined, kbOpts: { - kbExpr: EditorContextKeys.focus, primary: KeyMod.Alt | KeyCode.F1, weight: KeybindingWeight.EditorContrib, linux: { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css index 63601ef1c79..5f7ec45d7f7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css @@ -31,10 +31,26 @@ .tiw-metadata-value { font-family: var(--monaco-monospace-font); - text-align: right; word-break: break-word; } + +.tiw-metadata-values { + list-style: none; + max-height: 300px; + overflow-y: auto; + margin-right: -10px; + padding-left: 0; +} + +.tiw-metadata-values > .tiw-metadata-value { + margin-right: 10px; +} + .tiw-metadata-key { + width: 1px; + min-width: 150px; + padding-right: 10px; + white-space: nowrap; vertical-align: top; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 75138d4a33c..97a0010dfbd 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -594,11 +594,17 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { theme.resolveScopes(definition, scopesDefinition); const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { - const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); + const scopes = $('ul.tiw-metadata-values'); + const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope : [String(matchingRule.scope)]; + + for (let strScope of strScopes) { + scopes.appendChild($('li.tiw-metadata-value.tiw-metadata-scopes', undefined, strScope)); + } + elements.push( scopesDefinition.scope.join(' '), - $('br'), - $('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t'))); + scopes, + $('code.tiw-theme-selector', undefined, JSON.stringify(matchingRule.settings, null, '\t'))); return elements; } return elements; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 698828c06d7..f2770354531 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -28,7 +28,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index 33d7253576c..2c340ac02b3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -9,7 +9,7 @@ import * as platform from 'vs/base/common/platform'; import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts index 592605c0759..015984db6ab 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts @@ -16,7 +16,7 @@ import { IEditorContribution, Handler } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts index 46019e55ba7..f0e5225baa5 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts index 9ac56e09797..a5351b13023 100644 --- a/src/vs/workbench/contrib/comments/browser/commentMenus.ts +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -7,14 +7,12 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Comment, CommentThread } from 'vs/editor/common/modes'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export class CommentMenus implements IDisposable { constructor( - @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IMenuService private readonly menuService: IMenuService ) { } getCommentThreadTitleActions(commentThread: CommentThread, contextKeyService: IContextKeyService): IMenu { @@ -40,7 +38,7 @@ export class CommentMenus implements IDisposable { const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); return menu; } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 25b63f19e78..9e66d94a6ae 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -47,6 +47,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action codicon-chevron-up'; @@ -916,6 +917,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget content.push(`.monaco-editor .review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`); } + const border = theme.getColor(PANEL_BORDER); + if (border) { + content.push(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`); + } + const hcBorder = theme.getColor(contrastBorder); if (hcBorder) { content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 584c433d6c3..da9c9f970d8 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -119,18 +119,17 @@ white-space: pre; text-align: center; font-size: 12px; + display: flex; } .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-icon { - background-size: 12px; + background-size: 14px; background-position: left center; background-repeat: no-repeat; - width: 16px; - height: 12px; + width: 14px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-block; - margin-top: 3px; margin-right: 4px; } @@ -165,11 +164,7 @@ } .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { - border: 1px solid transparent; -} - -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active { - border: 1px solid grey; + border: 1px solid; } .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled { diff --git a/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.ts b/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.ts index e780491f3cf..127283add02 100644 --- a/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.ts +++ b/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { DefaultConfigurationExportHelper } from 'vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper'; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts index 63dfa598903..3c6118f6f33 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts @@ -6,7 +6,7 @@ import { Schemas } from 'vs/base/common/network'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index a41b87bfb84..362d0544725 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -98,7 +98,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { - super(editor, { showFrame: true, showArrow: false, frameWidth: 1 }); + super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true }); this.toDispose = []; const model = this.editor.getModel(); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index b0c25ff46dd..3aea8856b9f 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -14,6 +14,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { distinct } from 'vs/base/common/arrays'; const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; @@ -128,7 +129,8 @@ export class CallStackEditorContribution implements IEditorContribution { }); }); - return decorations; + // Deduplicate same decorations so colors do not stack #109045 + return distinct(decorations, d => `${d.options.className} ${d.options.glyphMarginClassName} ${d.range.startLineNumber} ${d.range.startColumn}`); } dispose(): void { @@ -141,7 +143,6 @@ registerThemingParticipant((theme, collector) => { const topStackFrame = theme.getColor(topStackFrameColor); if (topStackFrame) { collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); - collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); } const focusedStackFrame = theme.getColor(focusedStackFrameColor); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 272a59189ad..c7d8774c8dd 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -111,8 +111,8 @@ async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsync } export class CallStackView extends ViewPane { - private pauseMessage!: HTMLSpanElement; - private pauseMessageLabel!: HTMLSpanElement; + private stateMessage!: HTMLSpanElement; + private stateMessageLabel!: HTMLSpanElement; private onCallStackChangeScheduler: RunOnceScheduler; private needsRefresh = false; private ignoreSelectionChangedEvent = false; @@ -156,15 +156,19 @@ export class CallStackView extends ViewPane { const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined; if (thread && thread.stoppedDetails) { - this.pauseMessageLabel.textContent = thread.stateLabel; - this.pauseMessageLabel.title = thread.stateLabel; - this.pauseMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); - this.pauseMessage.hidden = false; - this.updateActions(); + this.stateMessageLabel.textContent = thread.stateLabel; + this.stateMessageLabel.title = thread.stateLabel; + this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); + this.stateMessage.hidden = false; + } else if (sessions.length === 1 && sessions[0].state === State.Running) { + this.stateMessageLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); + this.stateMessageLabel.title = sessions[0].getLabel(); + this.stateMessageLabel.classList.remove('exception'); + this.stateMessage.hidden = false; } else { - this.pauseMessage.hidden = true; - this.updateActions(); + this.stateMessage.hidden = true; } + this.updateActions(); this.needsRefresh = false; this.dataSource.deemphasizedStackFramesToShow = []; @@ -195,13 +199,13 @@ export class CallStackView extends ViewPane { const titleContainer = dom.append(container, $('.debug-call-stack-title')); super.renderHeaderTitle(titleContainer, this.options.title); - this.pauseMessage = dom.append(titleContainer, $('span.pause-message')); - this.pauseMessage.hidden = true; - this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label')); + this.stateMessage = dom.append(titleContainer, $('span.state-message')); + this.stateMessage.hidden = true; + this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); } getActions(): IAction[] { - if (this.pauseMessage.hidden) { + if (this.stateMessage.hidden) { return [new CollapseAction(() => this.tree, true, 'explorer-action codicon-collapse-all')]; } @@ -437,7 +441,7 @@ export class CallStackView extends ViewPane { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, g => /^inline/.test(g)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -930,7 +934,7 @@ class CallStackAccessibilityProvider implements IListAccessibilityProvider t.stopped); const state = thread ? thread.stateLabel : nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - return nls.localize('sessionLabel', "Session {0} {1}", element.getLabel(), state); + return nls.localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); } if (typeof element === 'string') { return element; diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 009fe56029d..a26cea18ba4 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -29,7 +29,7 @@ import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debugStatus'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index fd8ae385b05..83e1459eaae 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -699,8 +699,17 @@ abstract class AbstractLaunch { if (!config || !config.configurations) { return undefined; } - - return config.configurations.find(config => config && config.name === name); + const configuration = config.configurations.find(config => config && config.name === name); + if (configuration) { + if (this instanceof UserLaunch) { + configuration.__configurationTarget = ConfigurationTarget.USER; + } else if (this instanceof WorkspaceLaunch) { + configuration.__configurationTarget = ConfigurationTarget.WORKSPACE; + } else { + configuration.__configurationTarget = ConfigurationTarget.WORKSPACE_FOLDER; + } + } + return configuration; } async getInitialConfigurationContent(folderUri?: uri, type?: string, token?: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5a648e644f2..21e77a6be12 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -11,7 +11,7 @@ import * as errors from 'vs/base/common/errors'; import severity from 'vs/base/common/severity'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; @@ -450,7 +450,7 @@ export class DebugService implements IDebugService { actionList.push(new Action( 'installAdditionalDebuggers', - nls.localize('installAdditionalDebuggers', "Install {0} Extension", resolvedConfig.type), + nls.localize({ key: 'installAdditionalDebuggers', comment: ['Placeholder is the debug type, so for example "node", "python"'] }, "Install {0} Extension", resolvedConfig.type), undefined, true, async () => this.commandService.executeCommand('debug.installAdditionalDebuggers') @@ -798,7 +798,8 @@ export class DebugService implements IDebugService { const lineNumber = stackFrame.range.startLineNumber; if (lineNumber >= 1 && lineNumber <= model.getLineCount()) { const lineContent = control.getModel().getLineContent(lineNumber); - aria.alert(nls.localize('debuggingPaused', "{1}:{2}, debugging paused {0}, {3}", thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber, lineContent)); + aria.alert(nls.localize({ key: 'debuggingPaused', comment: ['First placeholder is the stack frame name, second is the line number, third placeholder is the reason why debugging is stopped, for example "breakpoint" and the last one is the file line content.'] }, + "{0}:{1}, debugging paused {2}, {3}", stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber, thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', lineContent)); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 26ccaab1f06..663ff7109de 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -31,7 +31,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { localize } from 'vs/nls'; import { canceled } from 'vs/base/common/errors'; import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 8d7b89429d5..a42bdff4488 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -18,6 +18,7 @@ import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/brows import { IProcessEnvironment } from 'vs/base/common/platform'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient implements IExtensionHostDebugService { @@ -26,7 +27,8 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @ILogService logService: ILogService + @ILogService logService: ILogService, + @IHostService hostService: IHostService ) { const connection = remoteAgentService.getConnection(); let channel: IChannel; @@ -49,14 +51,14 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i // Reload window on reload request this._register(this.onReload(event => { if (environmentService.isExtensionDevelopment && environmentService.debugExtensionHost.debugId === event.sessionId) { - window.location.reload(); + hostService.reload(); } })); // Close window on close request this._register(this.onClose(event => { if (environmentService.isExtensionDevelopment && environmentService.debugExtensionHost.debugId === event.sessionId) { - window.close(); + hostService.close(); } })); } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 78195e5fd2c..e0b6acb323a 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -98,7 +98,7 @@ width: 100%; } -.debug-pane .debug-call-stack-title > .pause-message { +.debug-pane .debug-call-stack-title > .state-message { flex: 1; text-align: right; text-overflow: ellipsis; @@ -107,7 +107,7 @@ margin: 0px 10px; } -.debug-pane .debug-call-stack-title > .pause-message > .label { +.debug-pane .debug-call-stack-title > .state-message > .label { border-radius: 3px; padding: 1px 2px; font-size: 9px; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 7fe44b35aaf..9aa9f44be57 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -461,7 +461,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (action.id === SelectReplAction.ID) { return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction); } else if (action.id === FILTER_ACTION_ID) { - this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter (e.g. text, !exclude)"), this.filterState); + this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude)"), this.filterState); return this.filterActionViewItem; } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index eaad9cd2c63..9a4560c0319 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -217,7 +217,7 @@ export class VariablesView extends ViewPane { variable: variable.toDebugProtocolObject() }; const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 8b5ab068ee3..a2a177f6e94 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -41,7 +41,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { } onMessage(callback: (message: DebugProtocol.ProtocolMessage) => void): void { - if (this.eventCallback) { + if (this.messageCallback) { this._onError.fire(new Error(`attempt to set more than one 'Message' callback`)); } this.messageCallback = callback; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 4035cc5ba46..fd56628f5af 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDisposable } from 'vs/base/common/lifecycle'; import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; @@ -538,6 +538,7 @@ export interface IConfig extends IEnvConfig { linux?: IEnvConfig; // internals + __configurationTarget?: ConfigurationTarget; __sessionId?: string; __restart?: any; __autoAttach?: boolean; diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 6f0a211b3bd..ae8bc52db80 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -108,7 +108,7 @@ export class Debugger implements IDebugger { substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise { return this.configurationManager.substituteVariables(this.type, folder, config).then(config => { - return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables); + return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables, config.__configurationTarget); }); } diff --git a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts index cc24a8695e8..b13961ea6b6 100644 --- a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts +++ b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index fa09f280e86..ee883ca582e 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -7,7 +7,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 { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { language, OperatingSystem, OS } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index 2990fcce10f..12454df7f2a 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -21,7 +21,7 @@ import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/teleme import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; 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 474a88e30c5..b0e18778a2e 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 @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, IPromptChoice, IPromptOptions, Severity } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index d01094f2853..16dac4771f3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -27,7 +27,7 @@ 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 { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem } 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'; @@ -59,6 +59,7 @@ import { TokenizationRegistry } from 'vs/editor/common/modes'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; function removeEmbeddedSVGs(documentContent: string): string { const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); @@ -195,6 +196,7 @@ export class ExtensionEditor extends EditorPane { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IWebviewService private readonly webviewService: IWebviewService, @IModeService private readonly modeService: IModeService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(ExtensionEditor.ID, telemetryService, themeService, storageService); this.extensionReadme = null; @@ -249,8 +251,8 @@ export class ExtensionEditor extends EditorPane { const extensionActionBar = this._register(new ActionBar(extensionActions, { animated: false, actionViewItemProvider: (action: IAction) => { - if (action instanceof ExtensionEditorDropDownAction) { - return action.createActionViewItem(); + if (action instanceof ActionWithDropDownAction) { + return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); } return undefined; } @@ -399,7 +401,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(RatingsWidget, template.rating, false) ]; const reloadAction = this.instantiationService.createInstance(ReloadAction); - const combinedInstallAction = this.instantiationService.createInstance(CombinedInstallAction); + const combinedInstallAction = this.instantiationService.createInstance(InstallDropdownAction); const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction); const actions = [ reloadAction, @@ -415,6 +417,8 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), combinedInstallAction, + this.instantiationService.createInstance(InstallingLabelAction), + this.instantiationService.createInstance(UninstallAction), systemDisabledWarningAction, this.instantiationService.createInstance(ExtensionToolTipAction, systemDisabledWarningAction, reloadAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, true), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 02e53565da8..7df6cf1b99e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -13,7 +13,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, shuffle } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { DynamicWorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations'; import { ExeBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/exeBasedRecommendations'; import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/browser/experimentalRecommendations'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 205ca37d121..b4ad140d918 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -31,7 +31,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtensionActivationProgress } from 'vs/workbench/contrib/extensions/browser/extensionsActivationProgress'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -80,7 +80,7 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro // Explorer MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '4_extensions', + group: 'extensions', command: { id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, title: localize('installVSIX', "Install VSIX"), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8ef92310fcb..aaeb9d2bc3b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, 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 } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions } 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 { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -53,7 +53,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -62,9 +62,17 @@ 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 { 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'; +import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; const promptDownloadManually = (extension: IGalleryExtension | undefined, message: string, error: Error, instantiationService: IInstantiationService): Promise => { return instantiationService.invokeFunction(accessor => { + if (isPromiseCanceledError(error)) { + return Promise.resolve(); + } + const productService = accessor.get(IProductService); const openerService = accessor.get(IOpenerService); const notificationService = accessor.get(INotificationService); @@ -141,75 +149,86 @@ export abstract class ExtensionAction extends Action implements IExtensionContai abstract update(): void; } -export class InstallAction extends ExtensionAction { +export abstract class ActionWithDropDownAction extends ExtensionAction { - private static readonly INSTALL_LABEL = localize('install', "Install"); - private static readonly INSTALLING_LABEL = localize('installing', "Installing"); + private action: IAction | undefined; - private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; - private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; + private _menuActions: IAction[] = []; + get menuActions(): IAction[] { return [...this._menuActions]; } - private _manifest: IExtensionManifest | null = null; + set extension(extension: IExtension | null) { + this.actions.forEach(a => a.extension = extension); + super.extension = extension; + } + + constructor( + id: string, label: string, + protected readonly actions: ExtensionAction[], + ) { + super(id, label); + this.update(); + this._register(Event.any(...actions.map(a => a.onDidChange))(() => this.update(true))); + } + + update(donotUpdateActions?: boolean): void { + if (!donotUpdateActions) { + this.actions.forEach(a => a.update()); + } + + const enabledActions = this.actions.filter(a => a.enabled); + this.action = enabledActions[0]; + this._menuActions = enabledActions.slice(1); + + this.enabled = !!this.action; + if (this.action) { + this.label = this.action.label; + this.tooltip = this.action.tooltip; + } + + let clazz = (this.action || this.actions[0])?.class || ''; + clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown'; + if (this._menuActions.length === 0) { + clazz += ' action-dropdown'; + } + this.class = clazz; + } + + run(): Promise { + const enabledActions = this.actions.filter(a => a.enabled); + return enabledActions[0].run(); + } +} + +export abstract class AbstractInstallAction extends ExtensionAction { + + static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + + protected _manifest: IExtensionManifest | null = null; set manifest(manifest: IExtensionManifest) { this._manifest = manifest; this.updateLabel(); } constructor( + id: string, label: string, cssClass: string, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly runtimeExtensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IProductService private readonly productService: IProductService, @ILabelService private readonly labelService: ILabelService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService ) { - super(`extensions.install`, InstallAction.INSTALL_LABEL, InstallAction.Class, false); + super(id, label, cssClass, false); this.update(); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } update(): void { this.enabled = false; - this.class = InstallAction.Class; - this.label = InstallAction.INSTALL_LABEL; if (this.extension && !this.extension.isBuiltin) { if (this.extension.state === ExtensionState.Uninstalled && this.extensionsWorkbenchService.canInstall(this.extension)) { this.enabled = true; this.updateLabel(); - return; - } - if (this.extension.state === ExtensionState.Installing) { - this.enabled = false; - this.updateLabel(); - this.class = this.extension.state === ExtensionState.Installing ? InstallAction.InstallingClass : InstallAction.Class; - return; - } - } - } - - private updateLabel(): void { - if (!this.extension) { - return; - } - if (this.extension.state === ExtensionState.Installing) { - this.label = InstallAction.INSTALLING_LABEL; - this.tooltip = InstallAction.INSTALLING_LABEL; - } else { - if (this._manifest && this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - if (prefersExecuteOnUI(this._manifest, this.productService, this.configurationService)) { - this.label = `${InstallAction.INSTALL_LABEL} ${localize('locally', "Locally")}`; - this.tooltip = `${InstallAction.INSTALL_LABEL} ${localize('locally', "Locally")}`; - } else { - const host = this.extensionManagementServerService.remoteExtensionManagementServer.label; - this.label = `${InstallAction.INSTALL_LABEL} on ${host}`; - this.tooltip = `${InstallAction.INSTALL_LABEL} on ${host}`; - } - } else { - this.label = InstallAction.INSTALL_LABEL; - this.tooltip = InstallAction.INSTALL_LABEL; } } } @@ -245,7 +264,7 @@ export class InstallAction extends ExtensionAction { } private install(extension: IExtension): Promise { - return this.extensionsWorkbenchService.install(extension) + return this.extensionsWorkbenchService.install(extension, this.getInstallOptions()) .then(null, err => { if (!extension.gallery) { return this.notificationService.error(err); @@ -275,6 +294,135 @@ export class InstallAction extends ExtensionAction { } return null; } + + protected abstract updateLabel(): void; + protected abstract getInstallOptions(): InstallOptions; +} + +export class InstallAction extends AbstractInstallAction { + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService instantiationService: IInstantiationService, + @INotificationService notificationService: INotificationService, + @IExtensionService runtimeExtensionService: IExtensionService, + @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, + @ILabelService labelService: ILabelService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IUserDataAutoSyncEnablementService protected readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, + @IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + ) { + super(`extensions.installAndSync`, localize('install', "Install"), InstallAction.Class, + extensionsWorkbenchService, instantiationService, notificationService, runtimeExtensionService, workbenchThemeService, labelService); + this.updateLabel(); + this._register(labelService.onDidChangeFormatters(() => this.updateLabel(), this)); + this._register(Event.any(userDataAutoSyncEnablementService.onDidChangeEnablement, + Event.filter(userDataSyncResourceEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update())); + } + + protected updateLabel(): void { + if (!this.extension) { + return; + } + + const isMachineScoped = this.getInstallOptions().isMachineScoped; + this.label = isMachineScoped ? localize('install and do no sync', "Install (Do not sync)") : localize('install', "Install"); + + // When remote connection exists + if (this._manifest && this.extensionManagementServerService.remoteExtensionManagementServer) { + + // On Desktop and UI Extension + if (this.extensionManagementServerService.localExtensionManagementServer && prefersExecuteOnUI(this._manifest, this.productService, this.configurationService)) { + this.label = isMachineScoped ? localize('install locally and do not sync', "Install Locally (Do not sync)") : localize('install locally', "Install Locally"); + return; + } + + // On Web and Web Extension + if (this.extensionManagementServerService.webExtensionManagementServer && prefersExecuteOnWeb(this._manifest, this.productService, this.configurationService)) { + this.label = isMachineScoped ? localize('install locally and do not sync', "Install Locally (Do not sync)") : localize('install locally', "Install Locally"); + return; + } + + const host = this.extensionManagementServerService.remoteExtensionManagementServer.label; + this.label = isMachineScoped ? localize('install on remote and do not sync', "Install on {0} (Do not sync)", host) : localize('install on remote', "Install on {0}", host); + return; + } + } + + protected getInstallOptions(): InstallOptions { + return { isMachineScoped: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) }; + } + +} + +export class InstallAndSyncAction extends AbstractInstallAction { + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService instantiationService: IInstantiationService, + @INotificationService notificationService: INotificationService, + @IExtensionService runtimeExtensionService: IExtensionService, + @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, + @ILabelService labelService: ILabelService, + @IProductService productService: IProductService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, + @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + ) { + super(`extensions.installAndSync`, localize('install', "Install"), InstallAndSyncAction.Class, + extensionsWorkbenchService, instantiationService, notificationService, runtimeExtensionService, workbenchThemeService, labelService); + this.tooltip = localize('install everywhere tooltip', "Install this extension in all your synced {0} instances", productService.nameLong); + this._register(Event.any(userDataAutoSyncEnablementService.onDidChangeEnablement, + Event.filter(userDataSyncResourceEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update())); + } + + + update(): void { + super.update(); + if (this.enabled) { + this.enabled = this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions); + } + } + + protected updateLabel(): void { } + + protected getInstallOptions(): InstallOptions { + return { isMachineScoped: false }; + } +} + +export class InstallDropdownAction extends ActionWithDropDownAction { + + set manifest(manifest: IExtensionManifest) { + this.actions.forEach(a => (a).manifest = manifest); + this.actions.forEach(a => a.update()); + this.update(); + } + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(`extensions.installActions`, '', [ + instantiationService.createInstance(InstallAndSyncAction), + instantiationService.createInstance(InstallAction), + ]); + } + +} + +export class InstallingLabelAction extends ExtensionAction { + + private static readonly LABEL = localize('installing', "Installing"); + private static readonly CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; + + constructor() { + super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false); + } + + update(): void { + this.class = `${InstallingLabelAction.CLASS}${this.extension && this.extension.state === ExtensionState.Installing ? '' : ' hide'}`; + } } export abstract class InstallInOtherServerAction extends ExtensionAction { @@ -476,73 +624,6 @@ export class UninstallAction extends ExtensionAction { } } -export class CombinedInstallAction extends ExtensionAction { - - private static readonly NoExtensionClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install no-extension`; - private installAction: InstallAction; - private uninstallAction: UninstallAction; - - constructor( - @IInstantiationService instantiationService: IInstantiationService - ) { - super('extensions.combinedInstall', '', '', false); - - this.installAction = this._register(instantiationService.createInstance(InstallAction)); - this.uninstallAction = this._register(instantiationService.createInstance(UninstallAction)); - - this.update(); - } - - set manifest(manifiest: IExtensionManifest) { this.installAction.manifest = manifiest; this.update(); } - - update(): void { - this.installAction.extension = this.extension; - this.uninstallAction.extension = this.extension; - this.installAction.update(); - this.uninstallAction.update(); - - if (!this.extension || this.extension.type === ExtensionType.System) { - this.enabled = false; - this.class = CombinedInstallAction.NoExtensionClass; - } else if (this.extension.state === ExtensionState.Installing) { - this.enabled = false; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } else if (this.extension.state === ExtensionState.Uninstalling) { - this.enabled = false; - this.label = this.uninstallAction.label; - this.class = this.uninstallAction.class; - this.tooltip = this.uninstallAction.tooltip; - } else if (this.installAction.enabled) { - this.enabled = true; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } else if (this.uninstallAction.enabled) { - this.enabled = true; - this.label = this.uninstallAction.label; - this.class = this.uninstallAction.class; - this.tooltip = this.uninstallAction.tooltip; - } else { - this.enabled = false; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } - } - - run(): Promise { - if (this.installAction.enabled) { - return this.installAction.run(); - } else if (this.uninstallAction.enabled) { - return this.uninstallAction.run(); - } - - return Promise.resolve(); - } -} - export class UpdateAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent update`; @@ -607,7 +688,7 @@ export class UpdateAction extends ExtensionAction { } } -interface IExtensionActionViewItemOptions extends IActionViewItemOptions { +export interface IExtensionActionViewItemOptions extends IActionViewItemOptions { tabOnlyOnFocus?: boolean; } @@ -641,6 +722,53 @@ export class ExtensionActionViewItem extends ActionViewItem { } } +export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdownActionViewItem { + + constructor( + action: ActionWithDropDownAction, + options: IExtensionActionViewItemOptions & IActionWithDropdownActionViewItemOptions, + contextMenuProvider: IContextMenuProvider + ) { + super(null, action, options, contextMenuProvider); + } + + render(container: HTMLElement): void { + super.render(container); + this.updateClass(); + } + + updateClass(): void { + super.updateClass(); + if (this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); + } + } + + updateEnabled(): void { + super.updateEnabled(); + + if (this.label && (this.options).tabOnlyOnFocus && this.getAction().enabled && !this._hasFocus) { + DOM.removeTabIndexAndUpdateFocus(this.label); + } + } + + private _hasFocus: boolean = false; + setFocus(value: boolean): void { + if (!(this.options).tabOnlyOnFocus || this._hasFocus === value) { + return; + } + this._hasFocus = value; + if (this.label && this.getAction().enabled) { + if (this._hasFocus) { + this.label.tabIndex = 0; + } else { + DOM.removeTabIndexAndUpdateFocus(this.label); + } + } + } + +} + export abstract class ExtensionDropDownAction extends ExtensionAction { constructor( @@ -891,7 +1019,7 @@ export class EnableForWorkspaceAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { - super(EnableForWorkspaceAction.ID, EnableForWorkspaceAction.LABEL); + super(EnableForWorkspaceAction.ID, EnableForWorkspaceAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.update(); } @@ -921,7 +1049,7 @@ export class EnableGloballyAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { - super(EnableGloballyAction.ID, EnableGloballyAction.LABEL); + super(EnableGloballyAction.ID, EnableGloballyAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.update(); } @@ -952,7 +1080,7 @@ export class DisableForWorkspaceAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { - super(DisableForWorkspaceAction.ID, DisableForWorkspaceAction.LABEL); + super(DisableForWorkspaceAction.ID, DisableForWorkspaceAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.update(); } @@ -982,7 +1110,7 @@ export class DisableGloballyAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { - super(DisableGloballyAction.ID, DisableGloballyAction.LABEL); + super(DisableGloballyAction.ID, DisableGloballyAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.update(); } @@ -1003,51 +1131,7 @@ export class DisableGloballyAction extends ExtensionAction { } } -export abstract class ExtensionEditorDropDownAction extends ExtensionDropDownAction { - - private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} extension-editor-dropdown-action`; - private static readonly EnabledDropDownClass = `${ExtensionEditorDropDownAction.EnabledClass} dropdown enable`; - private static readonly DisabledClass = `${ExtensionEditorDropDownAction.EnabledClass} disabled`; - - constructor( - id: string, private readonly initialLabel: string, - readonly actions: ExtensionAction[], - @IInstantiationService instantiationService: IInstantiationService - ) { - super(id, initialLabel, ExtensionEditorDropDownAction.DisabledClass, false, false, instantiationService); - this.update(); - } - - update(): void { - this.actions.forEach(a => a.extension = this.extension); - this.actions.forEach(a => a.update()); - const enabledActions = this.actions.filter(a => a.enabled); - this.enabled = enabledActions.length > 0; - if (this.enabled) { - if (enabledActions.length === 1) { - this.label = enabledActions[0].label; - this.class = ExtensionEditorDropDownAction.EnabledClass; - } else { - this.label = this.initialLabel; - this.class = ExtensionEditorDropDownAction.EnabledDropDownClass; - } - } else { - this.class = ExtensionEditorDropDownAction.DisabledClass; - } - } - - public run(): Promise { - const enabledActions = this.actions.filter(a => a.enabled); - if (enabledActions.length === 1) { - enabledActions[0].run(); - } else { - return super.run({ actionGroups: [this.actions], disposeActionsOnHide: false }); - } - return Promise.resolve(); - } -} - -export class EnableDropDownAction extends ExtensionEditorDropDownAction { +export class EnableDropDownAction extends ActionWithDropDownAction { constructor( @IInstantiationService instantiationService: IInstantiationService @@ -1055,11 +1139,11 @@ export class EnableDropDownAction extends ExtensionEditorDropDownAction { super('extensions.enable', localize('enableAction', "Enable"), [ instantiationService.createInstance(EnableGloballyAction), instantiationService.createInstance(EnableForWorkspaceAction) - ], instantiationService); + ]); } } -export class DisableDropDownAction extends ExtensionEditorDropDownAction { +export class DisableDropDownAction extends ActionWithDropDownAction { constructor( runningExtensions: IExtensionDescription[], @@ -1068,8 +1152,9 @@ export class DisableDropDownAction extends ExtensionEditorDropDownAction { super('extensions.disable', localize('disableAction', "Disable"), [ instantiationService.createInstance(DisableGloballyAction, runningExtensions), instantiationService.createInstance(DisableForWorkspaceAction, runningExtensions) - ], instantiationService); + ]); } + } export class CheckForUpdatesAction extends Action { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index e6609df5c28..f1aede07b33 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -14,7 +14,7 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, SyncIgnoredIconAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, SyncIgnoredIconAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -24,6 +24,7 @@ import { isLanguagePackExtension } from 'vs/platform/extensions/common/extension import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { foreground, listActiveSelectionForeground, listActiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionBackground, listFocusForeground, listFocusBackground, listHoverForeground, listHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export interface IExtensionsViewState { onFocus: Event; @@ -61,6 +62,7 @@ export class Renderer implements IPagedRenderer { @IExtensionService private readonly extensionService: IExtensionService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { } get templateId() { return 'extension'; } @@ -86,6 +88,9 @@ export class Renderer implements IPagedRenderer { const actionbar = new ActionBar(footer, { animated: false, actionViewItemProvider: (action: IAction) => { + if (action instanceof ActionWithDropDownAction) { + return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + } if (action.id === ManageExtensionAction.ID) { return (action).createActionViewItem(); } @@ -101,7 +106,8 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(SyncIgnoredIconAction), this.instantiationService.createInstance(UpdateAction), reloadAction, - this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(InstallDropdownAction), + this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), @@ -197,13 +203,21 @@ export class Renderer implements IPagedRenderer { this.extensionViewState.onFocus(e => { if (areSameExtensions(extension.identifier, e.identifier)) { - data.actionbar.viewItems.forEach(item => (item).setFocus(true)); + data.actionbar.viewItems.forEach(item => { + if (item instanceof ExtensionActionViewItem || item instanceof ExtensionActionWithDropdownActionViewItem) { + item.setFocus(true); + } + }); } }, this, data.extensionDisposables); this.extensionViewState.onBlur(e => { if (areSameExtensions(extension.identifier, e.identifier)) { - data.actionbar.viewItems.forEach(item => (item).setFocus(false)); + data.actionbar.viewItems.forEach(item => { + if (item instanceof ExtensionActionViewItem || item instanceof ExtensionActionWithDropdownActionViewItem) { + item.setFocus(false); + } + }); } }, this, data.extensionDisposables); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 68731f3aed6..f89a61d48b6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -501,7 +501,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.root.classList.toggle('narrow', dimension.width <= 300); } if (this.searchBox) { - this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + this.searchBox.layout(new Dimension(dimension.width - 34, 20)); } super.layout(new Dimension(dimension.width, dimension.height - 41)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 56e4f952c1c..99ae55b72c7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -13,13 +13,13 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { - IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath + IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, + InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -33,13 +33,13 @@ import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IProductService } from 'vs/platform/product/common/productService'; -import { getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { isWeb } from 'vs/base/common/platform'; import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { FileAccess } from 'vs/base/common/network'; +import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; interface IExtensionStateProvider { (extension: Extension): T; @@ -511,7 +511,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, - @IExtensionManagementService private readonly extensionService: IExtensionManagementService, + @IWorkbenchExtensioManagementService private readonly extensionManagementService: IWorkbenchExtensioManagementService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -523,6 +523,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IStorageService private readonly storageService: IStorageService, @IModeService private readonly modeService: IModeService, + @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, + @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IProductService private readonly productService: IProductService ) { super(); @@ -629,7 +631,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1; const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2; options.text = options.text ? this.resolveQueryText(options.text) : options.text; - return this.extensionService.getExtensionsReport() + return this.extensionManagementService.getExtensionsReport() .then(report => { const maliciousSet = getMaliciousExtensionsSet(report); @@ -903,7 +905,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - install(extension: URI | IExtension): Promise { + install(extension: URI | IExtension, installOptions?: InstallOptions): Promise { if (extension instanceof URI) { return this.installWithProgress(() => this.installFromVSIX(extension)); } @@ -918,7 +920,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return Promise.reject(new Error('Missing gallery')); } - return this.installWithProgress(() => this.installFromGallery(extension, gallery), gallery.displayName); + return this.installWithProgress(() => this.installFromGallery(extension, gallery, installOptions), gallery.displayName); } setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise { @@ -937,7 +939,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension location: ProgressLocation.Extensions, title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), source: `${toUninstall.identifier.id}` - }, () => this.extensionService.uninstall(toUninstall).then(() => undefined)); + }, () => this.extensionManagementService.uninstall(toUninstall).then(() => undefined)); } installVersion(extension: IExtension, version: string): Promise { @@ -976,38 +978,44 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.progressService.withProgress({ location: ProgressLocation.Extensions, source: `${toReinstall.identifier.id}` - }, () => this.extensionService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0])); + }, () => this.extensionManagementService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0])); } isExtensionIgnoredToSync(extension: IExtension): boolean { - const localExtensions = (!isWeb && this.localExtensions ? this.localExtensions.local : this.local) - .filter(l => !!l.local) - .map(l => l.local!); - - const ignoredExtensions = getIgnoredExtensions(localExtensions, this.configurationService); - return ignoredExtensions.includes(extension.identifier.id.toLowerCase()); + return extension.local ? !this.isInstalledExtensionSynced(extension.local) + : this.extensionsSyncManagementService.hasToNeverSyncExtension(extension.identifier.id); } - toggleExtensionIgnoredToSync(extension: IExtension): Promise { + async toggleExtensionIgnoredToSync(extension: IExtension): Promise { const isIgnored = this.isExtensionIgnoredToSync(extension); - const isDefaultIgnored = extension.local?.isMachineScoped; - const id = extension.identifier.id.toLowerCase(); - - // first remove the extension completely from ignored extensions - let currentValue = [...this.configurationService.getValue('settingsSync.ignoredExtensions')].map(id => id.toLowerCase()); - currentValue = currentValue.filter(v => v !== id && v !== `-${id}`); - - // If ignored, then add only if it is ignored by default - if (isIgnored && isDefaultIgnored) { - currentValue.push(`-${id}`); + if (extension.local && isIgnored) { + (extension).local = await this.updateSynchronizingInstalledExtension(extension.local, true); + this._onChange.fire(extension); + } else { + this.extensionsSyncManagementService.updateIgnoredExtensions(extension.identifier.id, !isIgnored); } + await this.userDataAutoSyncService.triggerSync(['IgnoredExtensionsUpdated'], false, false); + } - // If asked not to sync, then add only if it is not ignored by default - if (!isIgnored && !isDefaultIgnored) { - currentValue.push(id); + private isInstalledExtensionSynced(extension: ILocalExtension): boolean { + if (extension.isMachineScoped) { + return false; } + if (this.extensionsSyncManagementService.hasToAlwaysSyncExtension(extension.identifier.id)) { + return true; + } + return !this.extensionsSyncManagementService.hasToNeverSyncExtension(extension.identifier.id); + } - return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + async updateSynchronizingInstalledExtension(extension: ILocalExtension, sync: boolean): Promise { + const isMachineScoped = !sync; + if (extension.isMachineScoped !== isMachineScoped) { + extension = await this.extensionManagementService.updateExtensionScope(extension, isMachineScoped); + } + if (sync) { + this.extensionsSyncManagementService.updateIgnoredExtensions(extension.identifier.id, false); + } + return extension; } private installWithProgress(installTask: () => Promise, extensionName?: string): Promise { @@ -1019,9 +1027,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async installFromVSIX(vsix: URI): Promise { - const manifest = await this.extensionService.getManifest(vsix); + const manifest = await this.extensionManagementService.getManifest(vsix); const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) })); - const { identifier } = await this.extensionService.install(vsix); + const { identifier } = await this.extensionManagementService.install(vsix); if (existingExtension && existingExtension.latestVersion !== manifest.version) { this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(identifier, manifest.version)); @@ -1030,12 +1038,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0]; } - private async installFromGallery(extension: IExtension, gallery: IGalleryExtension): Promise { + private async installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { this.installing.push(extension); this._onChange.fire(extension); try { - const extensionService = extension.server && extension.local && !isLanguagePackExtension(extension.local.manifest) ? extension.server.extensionManagementService : this.extensionService; - await extensionService.installFromGallery(gallery); + if (extension.state === ExtensionState.Installed && extension.local) { + await this.extensionManagementService.updateFromGallery(gallery, extension.local); + } else { + await this.extensionManagementService.installFromGallery(gallery, installOptions); + } const ids: string[] | undefined = extension.identifier.uuid ? [extension.identifier.uuid] : undefined; const names: string[] | undefined = extension.identifier.uuid ? undefined : [extension.identifier.id]; this.queryGallery({ names, ids, pageSize: 1 }, CancellationToken.None); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index 7695202bb36..340a9ad241a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -10,13 +10,15 @@ text-overflow: ellipsis; } -.monaco-action-bar .action-item .action-label.extension-action.label { +.monaco-action-bar .action-item > .action-label.extension-action.label, +.monaco-action-bar .action-dropdown-item > .action-label.extension-action.label { padding: 0 5px; outline-offset: 2px; } .monaco-action-bar .action-item .action-label.extension-action.text, -.monaco-action-bar .action-item .action-label.extension-action.label { +.monaco-action-bar .action-item .action-label.extension-action.label, +.monaco-action-bar .action-dropdown-item .action-label.extension-action.label { line-height: 14px; margin-top: 2px; } @@ -27,8 +29,7 @@ } .monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after, -.monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after, -.monaco-action-bar .action-item .action-label.extension-action.extension-editor-dropdown-action.dropdown:after { +.monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after { content: '▼'; padding-left: 2px; font-size: 80%; @@ -41,7 +42,8 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling), .monaco-action-bar .action-item.disabled .action-label.extension-action.update, .monaco-action-bar .action-item.disabled .action-label.extension-action.theme, -.monaco-action-bar .action-item.disabled .action-label.extension-action.extension-editor-dropdown-action, +.monaco-action-bar .action-item.action-dropdown-item.disabled, +.monaco-action-bar .action-item.action-dropdown-item .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.reload, .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, .monaco-action-bar .action-item.disabled .action-label.system-disable.hide, diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 3270a34f18e..2acadd17b0f 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -153,16 +153,26 @@ justify-content: flex-start; } -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action { +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item:not(.action-dropdown-item) > .extension-action, +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.action-label.dropdown, +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item > .extension-action.action-label.empty-dropdown { margin-right: 8px; } -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label { +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.label { font-weight: 600; - padding: 1px 6px; max-width: 300px; } +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label { + padding: 1px 6px; +} + +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.dropdown.label { + padding: 1px 0px; +} + .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.system-disable { margin-right: 0; } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 110c26969b5..f4586ae311e 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; -import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -80,7 +80,7 @@ export interface IExtensionsWorkbenchService { queryGallery(options: IQueryOptions, token: CancellationToken): Promise>; canInstall(extension: IExtension): boolean; install(vsix: URI): Promise; - install(extension: IExtension, promptToInstallDependencies?: boolean): Promise; + install(extension: IExtension, installOptins?: InstallOptions): Promise; uninstall(extension: IExtension): Promise; installVersion(extension: IExtension, version: string): Promise; reinstall(extension: IExtension): Promise; diff --git a/src/vs/workbench/contrib/extensions/common/extensionsUtils.ts b/src/vs/workbench/contrib/extensions/common/extensionsUtils.ts index 6caca15aa7b..7887fc066ba 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsUtils.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsUtils.ts @@ -11,7 +11,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index d491483b338..34cdcea7efa 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -82,7 +82,8 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { - text: '$(sync~spin) ' + nls.localize('profilingExtensionHost', "Profiling Extension Host"), + text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), + showProgress: true, ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."), command: 'workbench.action.extensionHostProfilder.stop' @@ -91,7 +92,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio const timeStarted = Date.now(); const handle = setInterval(() => { if (this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator.update({ ...indicator, text: '$(sync~spin) ' + nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); + this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); } }, 1000); this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 21d2f7844d3..f7fb0e3bda3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -13,7 +13,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { RuntimeExtensionsEditor, ShowRuntimeExtensionsAction, IExtensionHostProfileService, DebugExtensionHostAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, SaveExtensionHostProfileAction, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { EditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ActiveEditorContext } from 'vs/workbench/common/editor'; import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/extensionProfileService'; 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 94fc4af4425..3c38a6761d5 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 @@ -37,7 +37,7 @@ import { TestExtensionEnablementService } from 'vs/workbench/services/extensionM import { IURLService } from 'vs/platform/url/common/url'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification'; import { NativeURLService } from 'vs/platform/url/common/urlService'; import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; @@ -448,9 +448,9 @@ suite('ExtensionRecommendationsService Test', () => { await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); - assert.ok(recommendations['ms-python.python']); - assert.ok(!recommendations['mockpublisher2.mockextension2']); - assert.ok(!recommendations['ms-dotnettools.csharp']); + assert.ok(recommendations['ms-python.python'], 'ms-python.python extension shall exist'); + assert.ok(!recommendations['mockpublisher2.mockextension2'], 'mockpublisher2.mockextension2 extension shall not exist'); + assert.ok(!recommendations['ms-dotnettools.csharp'], 'ms-dotnettools.csharp extension shall not exist'); }); test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', async () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 39582fe948d..25259506d5d 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -47,11 +47,14 @@ import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -120,6 +123,9 @@ async function setupTest() { instantiationService.stub(IExtensionService, >{ getExtensions: () => Promise.resolve([]), onDidChangeExtensions: new Emitter().event, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false }); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + instantiationService.stub(IUserDataAutoSyncEnablementService, instantiationService.createInstance(UserDataAutoSyncEnablementService)); + instantiationService.stub(IUserDataSyncResourceEnablementService, instantiationService.createInstance(UserDataSyncResourceEnablementService)); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); } @@ -154,9 +160,9 @@ suite('ExtensionsActions', () => { }); }); - test('Test Install action when state is installing', () => { + test('Test InstallingLabelAction when state is installing', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); + const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallingLabelAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -304,111 +310,6 @@ suite('ExtensionsActions', () => { }); }); - test('Test CombinedInstallAction when there is no extension', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - - assert.ok(!testObject.enabled); - assert.equal('extension-action label prominent install no-extension', testObject.class); - }); - - test('Test CombinedInstallAction when extension is system extension', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const local = aLocalExtension('a', {}, { type: ExtensionType.System }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); - - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => { - testObject.extension = extensions[0]; - assert.ok(!testObject.enabled); - assert.equal('extension-action label prominent install no-extension', testObject.class); - }); - }); - - test('Test CombinedInstallAction when installAction is enabled', () => { - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const gallery = aGalleryExtension('a'); - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - - return workbenchService.queryGallery(CancellationToken.None) - .then((paged) => { - testObject.extension = paged.firstPage[0]; - assert.ok(testObject.enabled); - assert.equal('Install', testObject.label); - assert.equal('extension-action label prominent install', testObject.class); - }); - }); - - test('Test CombinedInstallAction when unInstallAction is enabled', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const local = aLocalExtension('a'); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); - - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => { - testObject.extension = extensions[0]; - assert.ok(testObject.enabled); - assert.equal('Uninstall', testObject.label); - assert.equal('extension-action label uninstall', testObject.class); - }); - }); - - test('Test CombinedInstallAction when state is installing', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - const gallery = aGalleryExtension('a'); - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - return workbenchService.queryGallery(CancellationToken.None) - .then((paged) => { - testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, gallery }); - - assert.ok(!testObject.enabled); - assert.equal('Installing', testObject.label); - assert.equal('extension-action label install installing', testObject.class); - }); - }); - - test('Test CombinedInstallAction when state is installing during update', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const local = aLocalExtension('a'); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); - - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => { - const gallery = aGalleryExtension('a'); - const extension = extensions[0]; - extension.gallery = gallery; - testObject.extension = extension; - installEvent.fire({ identifier: gallery.identifier, gallery }); - assert.ok(!testObject.enabled); - assert.equal('Installing', testObject.label); - assert.equal('extension-action label install installing', testObject.class); - }); - }); - - test('Test CombinedInstallAction when state is uninstalling', () => { - const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.createInstance(ExtensionContainers, [testObject]); - const local = aLocalExtension('a'); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); - - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => { - testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); - assert.ok(!testObject.enabled); - assert.equal('Uninstalling', testObject.label); - assert.equal('extension-action label uninstall uninstalling', testObject.class); - }); - }); - test('Test UpdateAction when there is no extension', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); instantiationService.createInstance(ExtensionContainers, [testObject]); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 74a838c980f..dc1e3e16768 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -41,7 +41,7 @@ import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedPr import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 3fd4c0fe013..7b38b373bea 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -25,7 +25,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as import { Disposable } from 'vs/base/common/lifecycle'; import { isWeb, isWindows } from 'vs/base/common/platform'; import { dirname, basename } from 'vs/base/common/path'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.contribution.ts b/src/vs/workbench/contrib/feedback/browser/feedback.contribution.ts index b6d1fa758f6..94485ad4969 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.contribution.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.contribution.ts @@ -6,6 +6,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { FeedbackStatusbarConribution } from 'vs/workbench/contrib/feedback/browser/feedbackStatusbarItem'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FeedbackStatusbarConribution, LifecyclePhase.Starting); \ No newline at end of file +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FeedbackStatusbarConribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts index 415393da1ef..b260fdf04b3 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts @@ -6,7 +6,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { URI } from 'vs/base/common/uri'; import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { IHostService } from 'vs/workbench/services/host/browser/host'; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 086fa8c123a..fd399160c73 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -22,7 +22,7 @@ import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfi import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; +import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, HasWebFileSystemAccess, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; @@ -80,7 +80,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace + primary: KeyMod.CtrlCmd | KeyCode.Backspace, + secondary: [KeyCode.Delete] }, handler: moveFileToTrashHandler }); @@ -222,7 +223,7 @@ appendToCommandPalette(COMPARE_WITH_SAVED_COMMAND_ID, { value: nls.localize('com appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, original: 'Save As...' }, category); appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'New File' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'New Folder' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); -appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); +appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download...' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); appendToCommandPalette(NEW_UNTITLED_FILE_COMMAND_ID, { value: NEW_UNTITLED_FILE_LABEL, original: 'New Untitled File' }, category); // Menu registration - open editors @@ -489,7 +490,14 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ id: DOWNLOAD_COMMAND_ID, title: DOWNLOAD_LABEL, }, - when: ContextKeyExpr.or(ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), IsWebContext.toNegated()), ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated())) + when: ContextKeyExpr.or( + // native: for any remote resource + ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)), + // web: for any files + ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()), + // web: for any folders if file system API support is provided + ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess) + ) })); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index b5437e7859a..c7f63c11d7a 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -11,10 +11,10 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Action } from 'vs/base/common/actions'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { BinarySize, IFileService, IFileStatWithMetadata, IFileStreamContent } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; @@ -39,7 +39,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { getErrorMessage } from 'vs/base/common/errors'; -import { triggerDownload } from 'vs/base/browser/dom'; +import { WebFileSystemAccess, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -49,6 +49,9 @@ import { once } from 'vs/base/common/functional'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { trim, rtrim } from 'vs/base/common/strings'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ILogService } from 'vs/platform/log/common/log'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -66,7 +69,7 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); -export const DOWNLOAD_LABEL = nls.localize('download', "Download"); +export const DOWNLOAD_LABEL = nls.localize('download', "Download..."); const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; @@ -997,49 +1000,213 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = (accessor: ServicesAccessor) => { + const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const workingCopyFileService = accessor.get(IWorkingCopyFileService); const fileDialogService = accessor.get(IFileDialogService); const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true); + const progressService = accessor.get(IProgressService); - let canceled = false; - sequence(stats.map(s => async () => { - if (canceled) { - return; - } + const context = explorerService.getContext(true); + const explorerItems = context.length ? context : explorerService.roots; - if (isWeb) { - if (!s.isDirectory) { - let bufferOrUri: Uint8Array | URI; - try { - bufferOrUri = (await fileService.readFile(s.resource, { limits: { size: 1024 * 1024 /* set a limit to reduce memory pressure */ } })).value.buffer; - } catch (error) { - bufferOrUri = FileAccess.asBrowserUri(s.resource); + const cts = new CancellationTokenSource(); + + const downloadPromise = progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: isWeb, + title: nls.localize('downloadingFiles', "Downloading") + }, async progress => { + return sequence(explorerItems.map(explorerItem => async () => { + if (cts.token.isCancellationRequested) { + return; + } + + // Web: use DOM APIs to download files with optional support + // for folders and large files + if (isWeb) { + const stat = await fileService.resolve(explorerItem.resource, { resolveMetadata: true }); + + if (cts.token.isCancellationRequested) { + return; } - triggerDownload(bufferOrUri, s.name); - } - } else { - let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath(Schemas.file) : fileDialogService.defaultFilePath(Schemas.file); - if (defaultUri) { - defaultUri = resources.joinPath(defaultUri, s.name); + const maxBlobDownloadSize = 32 * BinarySize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure + const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; + + // Folder: use FS APIs to download files and folders if available and preferred + if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { + + interface IDownloadOperation { + startTime: number, + + filesTotal: number; + filesDownloaded: number; + + totalBytesDownloaded: number; + fileBytesDownloaded: number; + } + + async function pipeContents(name: string, source: IFileStreamContent, target: WebFileSystemAccess.FileSystemWritableFileStream, operation: IDownloadOperation): Promise { + return new Promise((resolve, reject) => { + const sourceStream = source.value; + + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => target.close())); + + let disposed = false; + disposables.add(toDisposable(() => disposed = true)); + + disposables.add(once(cts.token.onCancellationRequested)(() => { + disposables.dispose(); + reject(); + })); + + sourceStream.on('data', data => { + if (!disposed) { + target.write(data.buffer); + reportProgress(name, source.size, data.byteLength, operation); + } + }); + + sourceStream.on('error', error => { + disposables.dispose(); + reject(error); + }); + + sourceStream.on('end', () => { + disposables.dispose(); + resolve(); + }); + }); + } + + async function downloadFile(targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, name: string, resource: URI, operation: IDownloadOperation): Promise { + + // Report progress + operation.filesDownloaded++; + operation.fileBytesDownloaded = 0; // reset for this file + reportProgress(name, 0, 0, operation); + + // Start to download + const targetFile = await targetFolder.getFileHandle(name, { create: true }); + const targetFileWriter = await targetFile.createWritable(); + + return pipeContents(name, await fileService.readFileStream(resource), targetFileWriter, operation); + } + + async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { + if (folder.children) { + operation.filesTotal += (folder.children.map(child => child.isFile)).length; + + for (const child of folder.children) { + if (cts.token.isCancellationRequested) { + return; + } + + if (child.isFile) { + await downloadFile(targetFolder, child.name, child.resource, operation); + } else { + const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); + const resolvedChildFolder = await fileService.resolve(child.resource, { resolveMetadata: true }); + + await downloadFolder(resolvedChildFolder, childFolder, operation); + } + } + } + } + + function reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { + operation.fileBytesDownloaded += bytesDownloaded; + operation.totalBytesDownloaded += bytesDownloaded; + + const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < BinarySize.MB) { + if (operation.filesTotal === 1) { + message = name; + } else { + message = nls.localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, BinarySize.formatSize(bytesDownloadedPerSecond)); + } + } + + // Large file + else { + message = nls.localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, BinarySize.formatSize(operation.fileBytesDownloaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesDownloadedPerSecond)); + } + + progress.report({ message }); + } + + try { + const parentFolder: WebFileSystemAccess.FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const operation: IDownloadOperation = { + startTime: Date.now(), + + filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method + filesDownloaded: 0, + + totalBytesDownloaded: 0, + fileBytesDownloaded: 0 + }; + + if (stat.isDirectory) { + const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); + await downloadFolder(stat, targetFolder, operation); + } else { + await downloadFile(parentFolder, stat.name, stat.resource, operation); + } + } catch (error) { + logService.warn(error); + cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels + } + } + + // File: use traditional download to circumvent browser limitations + else if (stat.isFile) { + let bufferOrUri: Uint8Array | URI; + try { + bufferOrUri = (await fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; + } catch (error) { + bufferOrUri = FileAccess.asBrowserUri(stat.resource); + } + + if (!cts.token.isCancellationRequested) { + triggerDownload(bufferOrUri, stat.name); + } + } } - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")), - title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), - defaultUri - }); - if (destination) { - await workingCopyFileService.copy([{ source: s.resource, target: destination }], { overwrite: true }); - } else { - // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 - canceled = true; + // Native: use working copy file service to get at the contents + else { + progress.report({ message: explorerItem.name }); + + let defaultUri = explorerItem.isDirectory ? fileDialogService.defaultFolderPath(Schemas.file) : fileDialogService.defaultFilePath(Schemas.file); + if (defaultUri) { + defaultUri = resources.joinPath(defaultUri, explorerItem.name); + } + + const destination = await fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), + title: explorerItem.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + defaultUri + }); + + if (destination) { + await workingCopyFileService.copy([{ source: explorerItem.resource, target: destination }], { overwrite: true }); + } else { + cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 + } } - } - })); + })); + }, () => cts.dispose(true)); + + // Also indicate progress in the files view + progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => downloadPromise); }; CommandsRegistry.registerCommand({ diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index d2e8299e07a..2c7af040d36 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -27,7 +27,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 874c92d571b..aa0a628b5d4 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -527,7 +527,7 @@ export class ExplorerView extends ViewPane { } else { arg = roots.length === 1 ? roots[0].resource : {}; } - disposables.add(createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService)); + disposables.add(createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions)); this.contextMenuService.showContextMenu({ getAnchor: () => anchor, diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 4ed199508e9..17a38497e9e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -999,22 +999,43 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (target.isReadonly) { return; } + const resolvedTarget = target; + if (!resolvedTarget) { + return; + } // Desktop DND (Import file) if (data instanceof NativeDragAndDropData) { - if (isWeb) { - this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } else { - this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const dropPromise = this.progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: isWeb ? localize('uploadingFiles', "Uploading") : localize('copyingFiles', "Copying") + }, async progress => { + try { + if (isWeb) { + await this.handleWebExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token); + } else { + await this.handleExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token); + } + } catch (error) { + this.notificationService.warn(error); + } + }, () => cts.dispose(true)); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => dropPromise); } // In-Explorer DND (Move/Copy file) else { - this.handleExplorerDrop(data as ElementsDragAndDropData, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent).then(undefined, e => this.notificationService.warn(e)); } } - private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; // Somehow the items thing is being modified at random, maybe as a security @@ -1026,45 +1047,38 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } const results: { isFile: boolean, resource: URI }[] = []; - const cts = new CancellationTokenSource(); const operation: IUploadOperation = { filesTotal: entries.length, filesUploaded: 0, startTime: Date.now(), bytesUploaded: 0 }; - // Start upload and report progress globally - const uploadPromise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: true, - title: localize('uploadingFiles', "Uploading") - }, async progress => { - for (let entry of entries) { + for (let entry of entries) { + if (token.isCancellationRequested) { + break; + } - // Confirm overwrite as needed - if (target && entry.name && target.getChild(entry.name)) { - const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); - if (!confirmed) { - continue; - } - - await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true }); + // Confirm overwrite as needed + if (target && entry.name && target.getChild(entry.name)) { + const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); + if (!confirmed) { + continue; } - // Upload entry - const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, cts.token); - if (result) { - results.push(result); + await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true }); + + if (token.isCancellationRequested) { + break; } } - }, () => cts.dispose(true)); - // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => uploadPromise); - - // Wait until upload is done - await uploadPromise; + // Upload entry + const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, token); + if (result) { + results.push(result); + } + } // Open uploaded file in editor only if we upload just one - if (!cts.token.isCancellationRequested && results.length === 1 && results[0].isFile) { - await this.editorService.openEditor({ resource: results[0].resource, options: { pinned: true } }); + const firstUploadedFile = results[0]; + if (!token.isCancellationRequested && firstUploadedFile?.isFile) { + await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); } } @@ -1081,15 +1095,19 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const bytesUploadedPerSecond = operation.bytesUploaded / ((Date.now() - operation.startTime) / 1000); + // Small file let message: string; - if (operation.filesTotal === 1 && entry.name) { - message = entry.name; - } else { - message = localize('uploadProgress', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, BinarySize.formatSize(bytesUploadedPerSecond)); + if (fileSize < BinarySize.MB) { + if (operation.filesTotal === 1) { + message = `${entry.name}`; + } else { + message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, BinarySize.formatSize(bytesUploadedPerSecond)); + } } - if (fileSize > BinarySize.MB) { - message = localize('uploadProgressDetail', "{0} ({1} of {2}, {3}/s)", message, BinarySize.formatSize(fileBytesUploaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesUploadedPerSecond)); + // Large file + else { + message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, BinarySize.formatSize(fileBytesUploaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesUploadedPerSecond)); } progress.report({ message }); @@ -1140,7 +1158,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } else { done = true; // an empty array is a signal that all entries have been read } - } while (!done); + } while (!done && !token.isCancellationRequested); // Update operation total based on new counts operation.filesTotal += childEntries.length; @@ -1227,12 +1245,16 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); } - private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { // Check for dropped external files to be folders const droppedResources = extractResources(originalEvent, true); const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); + if (token.isCancellationRequested) { + return; + } + // Pass focus to window this.hostService.focus(); @@ -1257,7 +1279,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return this.workspaceEditingService.addFolders(folders); } if (choice === buttons.length - 2) { - return this.addResources(target, droppedResources.map(res => res.resource)); + return this.addResources(target, droppedResources.map(res => res.resource), progress, token); } return undefined; @@ -1265,16 +1287,20 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Handle dropped files (only support FileStat as target) else if (target instanceof ExplorerItem) { - return this.addResources(target, droppedResources.map(res => res.resource)); + return this.addResources(target, droppedResources.map(res => res.resource), progress, token); } } - private async addResources(target: ExplorerItem, resources: URI[]): Promise { + private async addResources(target: ExplorerItem, resources: URI[], progress: IProgress, token: CancellationToken): Promise { if (resources && resources.length > 0) { // Resolve target to check for name collisions and ask user const targetStat = await this.fileService.resolve(target.resource); + if (token.isCancellationRequested) { + return; + } + // Check for name collisions const targetNames = new Set(); const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); @@ -1295,8 +1321,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } addPromisesFactory.push(async () => { + if (token.isCancellationRequested) { + return; + } + const sourceFile = resource; - const targetFile = joinPath(target.resource, basename(sourceFile)); + const sourceFileName = basename(sourceFile); + const targetFile = joinPath(target.resource, sourceFileName); + + progress.report({ message: sourceFileName }); const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], { overwrite: true }))[0]; // if we only add one file, just open it directly diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 61d3180faab..5c1e9e59b5f 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -379,7 +379,7 @@ export class OpenEditorsView extends ViewPane { const element = e.element; const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? EditorResourceAccessor.getOriginalUri(element.editor) : {} }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? EditorResourceAccessor.getOriginalUri(element.editor) : {} }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts b/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts index 94d0d2a6ef7..f947c9200be 100644 --- a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts +++ b/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index db5448308c4..05c0d93f9c2 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -19,7 +19,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -29,6 +29,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; +import { mergeSort } from 'vs/base/common/arrays'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -55,7 +56,20 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { } private async _updateConfigValues(): Promise { - const extensions = await this._extensionService.getExtensions(); + let extensions = await this._extensionService.getExtensions(); + + extensions = mergeSort(extensions, (a, b) => { + let boostA = a.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); + let boostB = b.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); + + if (boostA && !boostB) { + return -1; + } else if (!boostA && boostB) { + return 1; + } else { + return a.name.localeCompare(b.name); + } + }); DefaultFormatter.extensionIds.length = 0; DefaultFormatter.extensionDescriptions.length = 0; diff --git a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts index cac0f93652b..190c892525f 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { ICommandAction, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts index ef84c894e5a..5285dbdba8a 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueActions.ts @@ -27,7 +27,7 @@ export class OpenProcessExplorer extends Action { export class ReportPerformanceIssueUsingReporterAction extends Action { static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; - static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); + static readonly LABEL = nls.localize({ key: 'reportPerformanceIssue', comment: [`Here, 'issue' means problem or bug`] }, "Report Performance Issue"); constructor( id: string, diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index a03c99e966c..446ad446f07 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -11,7 +11,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import { ConfigureLocaleAction } from 'vs/workbench/contrib/localizations/browser/localizationsActions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { IExtensionManagementService, DidInstallExtensionEvent, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index ef517f03d49..5d04d1b973f 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -18,7 +18,7 @@ import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/se import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { dirname } from 'vs/base/common/resources'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; diff --git a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts index 4f7faa55ebe..bd51bcfb753 100644 --- a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts +++ b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts @@ -8,7 +8,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { basename, dirname } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class LogsDataCleaner extends Disposable { diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 2b89de3e117..e046732264e 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -20,7 +20,7 @@ import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IMarkersWorkbenchService, MarkersWorkbenchService, ActivityUpdater } from 'vs/workbench/contrib/markers/browser/markers'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; diff --git a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts index c6eea1a6b89..c82688d11ad 100644 --- a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts @@ -14,7 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; class MarkersDecorationsProvider implements IDecorationsProvider { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts index 6d7d9c8a64f..dd92f64d1fb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IMarkerListProvider, MarkerList, IMarkerNavigationService } from 'vs/editor/contrib/gotoError/markerNavigationService'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 2c2c937e6cf..7d7abed7f52 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -16,7 +16,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts index 44538a7d3f0..a53d3536553 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -116,7 +116,7 @@ class PropertyHeader extends Disposable { this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); return item; } @@ -969,7 +969,7 @@ export class ModifiedCell extends AbstractCellRenderer { this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); return item; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 920a3688e11..7cd9088308f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -20,7 +20,7 @@ import { IEditorOptions, ITextEditorOptions, IResourceEditorInput } from 'vs/pla import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index d11ec996443..25156780e59 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -186,7 +186,7 @@ abstract class AbstractCellRenderer { const toolbar = new ToolBar(container, this.contextMenuService, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); return item; } @@ -1059,7 +1059,7 @@ export class ListTopCellToolbar extends Disposable { const toolbar = new ToolBar(this.topCellToolbar, this.contextMenuService, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); return item; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts index ca8082fade0..4b5002ebc8d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts @@ -6,7 +6,6 @@ import * as DOM from 'vs/base/browser/dom'; import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { renderCodicons } from 'vs/base/browser/codicons'; @@ -16,9 +15,8 @@ export class CodiconActionViewItem extends MenuEntryActionViewItem { readonly _action: MenuItemAction, keybindingService: IKeybindingService, notificationService: INotificationService, - contextMenuService: IContextMenuService ) { - super(_action, keybindingService, notificationService, contextMenuService); + super(_action, keybindingService, notificationService); } updateLabel(): void { if (this.options.label && this.label) { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index a12cfb4a6a9..7b0283691df 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -18,7 +18,7 @@ import { IEditorRegistry, Extensions as EditorExtensions, EditorDescriptor } fro import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 7af6a57b013..f1f11105233 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -15,7 +15,7 @@ import { OutputLinkProvider } from 'vs/workbench/contrib/output/common/outputLin import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; import { ILogService } from 'vs/platform/log/common/log'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IOutputChannelModel, IOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel'; import { IViewsService } from 'vs/workbench/common/views'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 262c391623d..7e86eb96f38 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -36,6 +36,7 @@ import { groupBy } from 'vs/base/common/arrays'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { editorBackground, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Dimension } from 'vs/base/browser/dom'; export class OutputViewPane extends ViewPane { @@ -118,7 +119,7 @@ export class OutputViewPane extends ViewPane { layoutBody(height: number, width: number): void { super.layoutBody(height, width); - this.editor.layout({ height, width }); + this.editor.layout(new Dimension(width, height)); } getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -207,6 +208,7 @@ export class OutputEditor extends AbstractTextResourceEditor { options.renderLineHighlight = 'none'; options.minimap = { enabled: false }; options.renderValidationDecorations = 'editable'; + options.padding = undefined; const outputConfig = this.configurationService.getValue('[Log]'); if (outputConfig) { diff --git a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts index 1262e65ffd4..a1f68bc0f03 100644 --- a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index ec7143e8e40..4d0b7970c90 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; -import { ILifecycleService, LifecyclePhase, StartupKindToString } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase, StartupKindToString } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts index 5a2a4182404..27611018578 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { StartupProfiler } from './startupProfiler'; diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index 4023c6493c0..78832061e97 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -9,7 +9,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { PerfviewInput } from 'vs/workbench/contrib/performance/browser/perfviewEditor'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts index 99db644f386..52f82c64dab 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts @@ -9,7 +9,7 @@ import { promisify } from 'util'; import { onUnexpectedError } from 'vs/base/common/errors'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { ILifecycleService, StartupKind, StartupKindToString } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, StartupKind, StartupKindToString } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService } from 'vs/platform/update/common/update'; diff --git a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts index c8abae6ade6..cdf16c2451e 100644 --- a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts +++ b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IKeymapService, areKeyboardLayoutsEqual, parseKeyboardLayoutDescription, getKeyboardLayoutId, IKeyboardLayoutInfo } from 'vs/workbench/services/keybinding/common/keymapInfo'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 0e1aaa73bfd..8c59d791150 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -18,7 +18,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 068b01de440..941cd7b5b7c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -37,7 +37,7 @@ import { badgeBackground, badgeForeground, contrastBorder, editorForeground } fr import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; -import { IUserDataAutoSyncService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncEnablementService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor'; import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; @@ -175,7 +175,7 @@ export class SettingsEditor2 extends EditorPane { @IEditorGroupsService protected editorGroupService: IEditorGroupsService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, - @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); @@ -334,7 +334,7 @@ export class SettingsEditor2 extends EditorPane { const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right; // minus padding inside inputbox, countElement width, controls width, extra padding before countElement const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - this.controlsElement.clientWidth - 12; - this.searchWidget.layout({ height: 20, width: monacoWidth }); + this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20)); this.rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this.rootElement.classList.toggle('narrow-width', dimension.width < 600); @@ -491,7 +491,7 @@ export class SettingsEditor2 extends EditorPane { } })); - if (this.userDataSyncWorkbenchService.enabled && this.userDataAutoSyncService.canToggleEnablement()) { + if (this.userDataSyncWorkbenchService.enabled && this.userDataAutoSyncEnablementService.canToggleEnablement()) { const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; @@ -1416,7 +1416,7 @@ class SyncControls extends Disposable { container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IThemeService themeService: IThemeService, ) { super(); @@ -1449,7 +1449,7 @@ class SyncControls extends Disposable { this.update(); })); - this._register(this.userDataAutoSyncService.onDidChangeEnablement(() => { + this._register(this.userDataAutoSyncEnablementService.onDidChangeEnablement(() => { this.update(); })); } @@ -1473,7 +1473,7 @@ class SyncControls extends Disposable { return; } - if (this.userDataAutoSyncService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) { + if (this.userDataAutoSyncEnablementService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) { DOM.show(this.lastSyncedLabel); DOM.hide(this.turnOnSyncButton.element); } else { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index e5eb4697ce9..7c2dc4aa457 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -46,7 +46,7 @@ import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSetti import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; -import { getDefaultIgnoredSettings, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { getDefaultIgnoredSettings, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation'; import { Codicon } from 'vs/base/common/codicons'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; @@ -1525,7 +1525,7 @@ export class SettingTreeRenderers { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IContextViewService private readonly _contextViewService: IContextViewService, - @IUserDataAutoSyncService private readonly _userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataAutoSyncEnablementService private readonly _userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, ) { this.settingActions = [ new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, (context: SettingsTreeSettingElement) => { @@ -1569,7 +1569,7 @@ export class SettingTreeRenderers { } private getActionsForSetting(setting: ISetting): IAction[] { - const enableSync = this._userDataAutoSyncService.isEnabled(); + const enableSync = this._userDataAutoSyncEnablementService.isEnabled(); return enableSync && !setting.disallowSyncIgnore ? [ new Separator(), diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 4c0cd2c2370..9f8bede88ca 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -16,7 +16,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; import { isMacintosh, isNative, isLinux } from 'vs/base/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 2dc4800d78c..c6d98b601a3 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -38,7 +38,7 @@ import { ReconnectionWaitEvent, PersistentConnectionEventType } from 'vs/platfor import Severity from 'vs/base/common/severity'; import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { SwitchRemoteViewItem, SwitchRemoteAction } from 'vs/workbench/contrib/remote/browser/explorerViewItems'; import { Action, IActionViewItem, IAction } from 'vs/base/common/actions'; import { isStringArray } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 14528dafac9..5ccb9b03723 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Extensions, IViewDescriptorService, IViewsRegistry, IViewsService } from 'vs/workbench/common/views'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IRemoteExplorerService, MakeAddress, mapHasTunnelLocalhostOrAllInterfaces, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -24,7 +23,6 @@ export const VIEWLET_ID = 'workbench.view.remote'; export class ForwardedPortsView extends Disposable implements IWorkbenchContribution { private contextKeyListener?: IDisposable; - private _activityBadge?: IDisposable; private entryAccessor: IStatusbarEntryAccessor | undefined; constructor( @@ -32,7 +30,6 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, - @IActivityService private readonly activityService: IActivityService, @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); @@ -67,37 +64,21 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu } private enableBadgeAndStatusBar() { - this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => { - this.updateActivityBadge(); - this.updateStatusBar(); - })); - this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => { - this.updateActivityBadge(); - this.updateStatusBar(); - })); const disposable = Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { if (e.find(view => view.views.find(viewDescriptor => viewDescriptor.id === TUNNEL_VIEW_ID))) { - this.updateActivityBadge(); + this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => { + this.updateStatusBar(); + })); + this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => { + this.updateStatusBar(); + })); + this.updateStatusBar(); disposable.dispose(); } }); } - private updateActivityBadge() { - if (this._activityBadge) { - this._activityBadge.dispose(); - } - if (this.remoteExplorerService.tunnelModel.forwarded.size > 0) { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(TUNNEL_VIEW_ID); - if (viewContainer) { - this._activityBadge = this.activityService.showViewContainerActivity(viewContainer.id, { - badge: new NumberBadge(this.remoteExplorerService.tunnelModel.forwarded.size, n => n === 1 ? nls.localize('1forwardedPort', "1 forwarded port") : nls.localize('nForwardedPorts', "{0} forwarded ports", n)) - }); - } - } - } - private updateStatusBar() { if (!this.entryAccessor) { this._register(this.entryAccessor = this.statusbarService.addEntry(this.entry, 'status.forwardedPorts', nls.localize('status.forwardedPorts', "Forwarded Ports"), StatusbarAlignment.LEFT, 40)); @@ -108,15 +89,22 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu private get entry(): IStatusbarEntry { let text: string; - if (this.remoteExplorerService.tunnelModel.forwarded.size === 0) { + let tooltip: string; + const count = this.remoteExplorerService.tunnelModel.forwarded.size + this.remoteExplorerService.tunnelModel.detected.size; + if (count === 0) { text = nls.localize('remote.forwardedPorts.statusbarTextNone', "No Ports Available"); - } else if (this.remoteExplorerService.tunnelModel.forwarded.size === 1) { - text = nls.localize('remote.forwardedPorts.statusbarTextSingle', "1 Port Available"); + tooltip = text; } else { - text = nls.localize('remote.forwardedPorts.statusbarTextMultiple', "{0} Ports Available", this.remoteExplorerService.tunnelModel.forwarded.size); + if (count === 1) { + text = nls.localize('remote.forwardedPorts.statusbarTextSingle', "1 Port Available"); + } else { + text = nls.localize('remote.forwardedPorts.statusbarTextMultiple', "{0} Ports Available", count); + } + const allTunnels = Array.from(this.remoteExplorerService.tunnelModel.forwarded.values()); + allTunnels.push(...Array.from(this.remoteExplorerService.tunnelModel.detected.values())); + tooltip = nls.localize('remote.forwardedPorts.statusbarTooltip', "Available Ports: {0}", + allTunnels.map(forwarded => forwarded.remotePort).join(', ')); } - const tooltip = nls.localize('remote.forwardedPorts.statusbarTooltip', "Available Ports: {0}", - Array.from(this.remoteExplorerService.tunnelModel.forwarded.values()).map(forwarded => forwarded.remotePort).join(', ')); return { text: `$(radio-tower) ${text}`, ariaLabel: tooltip, diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 8a1a986c824..83e5499b936 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -197,7 +197,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.remoteAuthority) || this.remoteAuthority; switch (this.connectionState) { case 'initializing': - this.renderRemoteStatusIndicator(`$(sync~spin) ${nls.localize('host.open', "Opening Remote...")}`, nls.localize('host.open', "Opening Remote...")); + this.renderRemoteStatusIndicator(nls.localize('host.open', "Opening Remote..."), nls.localize('host.open', "Opening Remote..."), undefined, true /* progress */); break; case 'disconnected': this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", hostLabel)}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel)); @@ -219,7 +219,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } } - private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string): void { + private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string, showProgress?: boolean): void { const name = nls.localize('remoteHost', "Remote Host"); if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) { command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; @@ -230,6 +230,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), ariaLabel: name, text, + showProgress, tooltip, command }; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 92a5455dc2b..fde246bfd50 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -37,7 +37,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { URI } from 'vs/base/common/uri'; -import { isLocalhost, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -403,17 +403,21 @@ class TunnelItem implements ITunnelItem { get label(): string { if (this.name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); - } else if (this.localAddress && !isLocalhost(this.remoteHost)) { - return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress); } else if (this.localAddress) { - return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress); - } else if (!isLocalhost(this.remoteHost)) { - return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort); + return nls.localize('remote.tunnelsView.forwardedPortLabel1', "{0} \u2192 {1}", this.remotePort, TunnelItem.compactLongAddress(this.localAddress)); } else { - return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort); + return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}", this.remotePort); } } + private static compactLongAddress(address: string): string { + if (address.length < 15) { + return address; + } + const host = new URL(address).host; + return host.length > 0 ? host : address; + } + set description(description: string | undefined) { this._description = description; } @@ -422,7 +426,7 @@ class TunnelItem implements ITunnelItem { if (this._description) { return this._description; } else if (this.name) { - return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress); + return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} \u2192 {1}", this.remotePort, this.localAddress); } return undefined; } @@ -586,7 +590,8 @@ export class TunnelPanel extends ViewPane { } shouldShowWelcome(): boolean { - return this.viewModel.forwarded.length === 0 && this.viewModel.candidates.length === 0 && !this.isEditing; + return (this.viewModel.forwarded.length === 0) && (this.viewModel.candidates.length === 0) && + (this.viewModel.detected.length === 0) && !this.isEditing; } focus(): void { @@ -631,7 +636,7 @@ export class TunnelPanel extends ViewPane { } const actions: IAction[] = []; - this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService)); + this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions)); this.contextMenuService.showContextMenu({ getAnchor: () => treeEvent.anchor, diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index ca725098357..1728f76eab2 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -5,7 +5,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILabelService, ResourceLabelFormatting } from 'vs/platform/label/common/label'; import { OperatingSystem, isWeb } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; @@ -124,5 +124,15 @@ Registry.as(ConfigurationExtensions.Configuration) 'pub.name': ['ui'] } }, + 'remote.restoreForwardedPorts': { + type: 'boolean', + markdownDescription: localize('remote.restoreForwardedPorts', "Restores the ports you forwarded in a workspace."), + default: false + }, + 'remote.autoForwardPorts': { + type: 'boolean', + markdownDescription: localize('remote.autoForwardPorts', "When enabled, URLs with ports (ex. `http://127.0.0.1:3000`) that are printed to your terminals are automatically forwarded."), + default: true + } } }); diff --git a/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts index 039ec302a61..5a750342697 100644 --- a/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts @@ -11,7 +11,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILabelService } from 'vs/platform/label/common/label'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Schemas } from 'vs/base/common/network'; @@ -168,16 +168,6 @@ Registry.as(ConfigurationExtensions.Configuration) markdownDescription: nls.localize('remote.downloadExtensionsLocally', "When enabled extensions are downloaded locally and installed on remote."), default: false }, - 'remote.restoreForwardedPorts': { - type: 'boolean', - markdownDescription: nls.localize('remote.restoreForwardedPorts', "Restores the ports you forwarded in a workspace."), - default: false - }, - 'remote.autoForwardPorts': { - type: 'boolean', - markdownDescription: nls.localize('remote.autoForwardPorts', "When enabled, URLs with ports (ex. `http://127.0.0.1:3000`) that are printed to your terminals are automatically forwarded."), - default: true - } } }); diff --git a/src/vs/workbench/contrib/sash/browser/sash.contribution.ts b/src/vs/workbench/contrib/sash/browser/sash.contribution.ts index 1dfb6be879b..c4b13519c08 100644 --- a/src/vs/workbench/contrib/sash/browser/sash.contribution.ts +++ b/src/vs/workbench/contrib/sash/browser/sash.contribution.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/sash/browser/sash.ts b/src/vs/workbench/contrib/sash/browser/sash.ts index 1c76305bfcf..db5215c2fe1 100644 --- a/src/vs/workbench/contrib/sash/browser/sash.ts +++ b/src/vs/workbench/contrib/sash/browser/sash.ts @@ -34,15 +34,7 @@ export class SashSizeController extends Disposable implements IWorkbenchContribu private onDidChangeSizeConfiguration(): void { const size = clamp(this.configurationService.getValue(this.configurationName) ?? minSize, minSize, maxSize); - // Update styles - this.stylesheet.textContent = ` - .monaco-sash.vertical { cursor: ew-resize; top: 0; width: ${size}px; height: 100%; } - .monaco-sash.horizontal { cursor: ns-resize; left: 0; width: 100%; height: ${size}px; } - .monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after { content: ' '; height: ${size * 2}px; width: ${size * 2}px; z-index: 100; display: block; cursor: all-scroll; position: absolute; } - .monaco-sash.orthogonal-start.vertical::before { left: -${size / 2}px; top: -${size}px; } - .monaco-sash.orthogonal-end.vertical::after { left: -${size / 2}px; bottom: -${size}px; } - .monaco-sash.orthogonal-start.horizontal::before { top: -${size / 2}px; left: -${size}px; } - .monaco-sash.orthogonal-end.horizontal::after { top: -${size / 2}px; right: -${size}px; }`; + document.documentElement.style.setProperty('--sash-size', size + 'px'); // Update behavor setGlobalSashSize(size); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index bf905250784..4f8c578406d 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -11,7 +11,7 @@ import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider, IS import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMStatusController } from './activity'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -221,7 +221,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (!repository || !repository.provider.acceptInputCommand) { return Promise.resolve(null); } - const id = repository.provider.acceptInputCommand.id; const args = repository.provider.acceptInputCommand.arguments; @@ -230,6 +229,34 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.viewNextCommit', + description: { description: localize('scm view next commit', "SCM: View Next Commit"), args: [] }, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.has('scmInputIsInLastLine'), + primary: KeyCode.DownArrow, + handler: accessor => { + const contextKeyService = accessor.get(IContextKeyService); + const context = contextKeyService.getContext(document.activeElement); + const repository = context.getValue('scmRepository'); + repository?.input.showNextHistoryValue(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.viewPriorCommit', + description: { description: localize('scm view prior commit', "SCM: View Prior Commit"), args: [] }, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.has('scmInputIsInFirstLine'), + primary: KeyCode.UpArrow, + handler: accessor => { + const contextKeyService = accessor.get(IContextKeyService); + const context = contextKeyService.getContext(document.activeElement); + const repository = context.getValue('scmRepository'); + repository?.input.showPreviousHistoryValue(); + } +}); + CommandsRegistry.registerCommand('scm.openInTerminal', async (accessor, provider: ISCMProvider) => { if (!provider || !provider.rootUri) { return; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 503dd541418..1a5d3329ac4 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -151,7 +151,7 @@ export class SCMRepositoriesViewPane extends ViewPane { const provider = e.element.provider; const menus = this.scmViewService.menus.getRepositoryMenus(provider); const menu = menus.repositoryMenu; - const [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + const [actions, disposable] = collectContextMenuActions(menu); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 89ae5f47db2..1d8870217c4 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -77,42 +77,10 @@ import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmReposito import { IPosition } from 'vs/editor/common/core/position'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { LabelFuzzyScore } from 'vs/base/browser/ui/tree/abstractTree'; type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; -function splitMatches(uri: URI, filterData: FuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { - let matches: IMatch[] | undefined; - let descriptionMatches: IMatch[] | undefined; - - if (filterData) { - matches = []; - descriptionMatches = []; - - const fileName = basename(uri); - const allMatches = createMatches(filterData); - - for (const match of allMatches) { - if (match.start < fileName.length) { - matches!.push( - { - start: match.start, - end: Math.min(match.end, fileName.length) - } - ); - } else { - descriptionMatches!.push( - { - start: match.start - (fileName.length + 1), - end: match.end - (fileName.length + 1) - } - ); - } - } - } - - return [matches, descriptionMatches]; -} - interface ISCMLayout { height: number | undefined; width: number | undefined; @@ -343,7 +311,7 @@ class RepositoryPaneActionRunner extends ActionRunner { } } -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore | LabelFuzzyScore, ResourceTemplate> { static readonly TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -373,7 +341,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); @@ -382,12 +350,14 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); } - renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); @@ -451,12 +423,11 @@ class ResourceRenderer implements ICompressibleTreeRenderer e.name).join('/'); const fileKind = FileKind.FOLDER; - const [matches, descriptionMatches] = splitMatches(folder.uri, node.filterData); + const matches = createMatches(node.filterData as FuzzyScore | undefined); template.fileLabel.setResource({ resource: folder.uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind, - matches, - descriptionMatches + matches }); template.actionBar.clear(); @@ -474,7 +445,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + disposeCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); } @@ -482,6 +453,56 @@ class ResourceRenderer implements ICompressibleTreeRenderer pathLength) { + // Label match + labelMatches.push({ + start: match.start - pathLength, + end: match.end - pathLength + }); + } else if (match.end < pathLength) { + // Description match + descriptionMatches.push(match); + } else { + // Spanning match + labelMatches.push({ + start: 0, + end: match.end - pathLength + }); + descriptionMatches.push({ + start: match.start, + end: pathLength + }); + } + } + + return [labelMatches, descriptionMatches]; + } } class ListDelegate implements IListVirtualDelegate { @@ -596,9 +617,12 @@ export class SCMTreeSorter implements ITreeSorter { export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider { - constructor(@ILabelService private readonly labelService: ILabelService) { } + constructor( + private viewModelProvider: () => ViewModel, + @ILabelService private readonly labelService: ILabelService, + ) { } - getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { + getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | { toString(): string; }[] | undefined { if (ResourceTree.isResourceNode(element)) { return element.name; } else if (isSCMRepository(element)) { @@ -608,12 +632,20 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb } else if (isSCMResourceGroup(element)) { return element.label; } else { - // Since a match in the file name takes precedence over a match - // in the folder name we are returning the label as file/folder. - const fileName = basename(element.sourceUri); - const filePath = this.labelService.getUriLabel(dirname(element.sourceUri), { relative: true }); + const viewModel = this.viewModelProvider(); + if (viewModel.mode === ViewModelMode.List) { + // In List mode match using the file name and the path. + // Since we want to match both on the file name and the + // full path we return an array of labels. A match in the + // file name takes precedence over a match in the path. + const fileName = basename(element.sourceUri); + const filePath = this.labelService.getUriLabel(element.sourceUri, { relative: true }); - return filePath.length !== 0 ? `${fileName} ${filePath}` : fileName; + return [fileName, filePath]; + } else { + // In Tree mode only match using the file name + return basename(element.sourceUri); + } } } @@ -1307,14 +1339,14 @@ class SCMInputWidget extends Disposable { if (value === textModel.getValue()) { // circuit breaker return; } - textModel.setValue(input.value); + textModel.setValue(value); this.inputEditor.setPosition(textModel.getFullModelRange().getEndPosition()); })); // Keep API in sync with model, update placeholder visibility and validate const updatePlaceholderVisibility = () => this.placeholderTextContainer.classList.toggle('hidden', textModel.getValueLength() > 0); this.repositoryDisposables.add(textModel.onDidChangeContent(() => { - input.value = textModel.getValue(); + input.setValue(textModel.getValue(), true); updatePlaceholderVisibility(); triggerValidation(); })); @@ -1433,6 +1465,18 @@ class SCMInputWidget extends Disposable { this.validationDisposable.dispose(); })); + const firstLineKey = contextKeyService2.createKey('scmInputIsInFirstLine', false); + const lastLineKey = contextKeyService2.createKey('scmInputIsInLastLine', false); + + this._register(this.inputEditor.onDidChangeCursorPosition(({ position }) => { + const viewModel = this.inputEditor._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + + firstLineKey.set(viewPosition.lineNumber === 1); + lastLineKey.set(viewPosition.lineNumber === lastLineNumber); + })); + const onInputFontFamilyChanged = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.inputFontFamily')); this._register(onInputFontFamilyChanged(() => this.inputEditor.updateOptions({ fontFamily: this.getInputEditorFontFamily() }))); @@ -1446,10 +1490,7 @@ class SCMInputWidget extends Disposable { layout(): void { const editorHeight = this.getContentHeight(); - const dimension: Dimension = { - width: this.element.clientWidth - 2, - height: editorHeight, - }; + const dimension = new Dimension(this.element.clientWidth - 2, editorHeight); this.inputEditor.layout(dimension); this.renderValidation(); @@ -1619,7 +1660,7 @@ export class SCMViewPane extends ViewPane { this._register(actionRunner); this._register(actionRunner.onDidBeforeRun(() => this.tree.domFocus())); - const renderers: ICompressibleTreeRenderer[] = [ + const renderers: ICompressibleTreeRenderer[] = [ this.instantiationService.createInstance(RepositoryRenderer, actionViewItemProvider), this.inputRenderer, this.instantiationService.createInstance(ResourceGroupRenderer, actionViewItemProvider), @@ -1628,7 +1669,7 @@ export class SCMViewPane extends ViewPane { const filter = new SCMTreeFilter(); const sorter = new SCMTreeSorter(() => this.viewModel); - const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider); + const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewModel); const identityProvider = new SCMResourceIdentityProvider(); this.tree = this.instantiationService.createInstance( @@ -1835,27 +1876,27 @@ export class SCMViewPane extends ViewPane { const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); const menu = menus.repositoryMenu; context = element.provider; - [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + [actions, disposable] = collectContextMenuActions(menu); } else if (isSCMInput(element)) { // noop } else if (isSCMResourceGroup(element)) { const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); const menu = menus.getResourceGroupMenu(element); - [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + [actions, disposable] = collectContextMenuActions(menu); } else if (ResourceTree.isResourceNode(element)) { if (element.element) { const menus = this.scmViewService.menus.getRepositoryMenus(element.element.resourceGroup.provider); const menu = menus.getResourceMenu(element.element); - [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + [actions, disposable] = collectContextMenuActions(menu); } else { const menus = this.scmViewService.menus.getRepositoryMenus(element.context.provider); const menu = menus.getResourceFolderMenu(element.context); - [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + [actions, disposable] = collectContextMenuActions(menu); } } else { const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider); const menu = menus.getResourceMenu(element); - [actions, disposable] = collectContextMenuActions(menu, this.contextMenuService); + [actions, disposable] = collectContextMenuActions(menu); } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 3bf2e9eef45..37af180bdee 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -10,7 +10,6 @@ import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/ba import { Action, IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { equals } from 'vs/base/common/arrays'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { renderCodicons } from 'vs/base/browser/codicons'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -75,10 +74,10 @@ export function connectPrimaryMenuToInlineActionBar(menu: IMenu, actionBar: Acti }, g => /^inline/.test(g)); } -export function collectContextMenuActions(menu: IMenu, contextMenuService: IContextMenuService): [IAction[], IDisposable] { +export function collectContextMenuActions(menu: IMenu): [IAction[], IDisposable] { const primary: IAction[] = []; const actions: IAction[] = []; - const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, { primary, secondary: actions }, contextMenuService, g => /^inline/.test(g)); + const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, { primary, secondary: actions }, g => /^inline/.test(g)); return [actions, disposable]; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index aa3f57f7849..0f4640b971b 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -86,7 +86,8 @@ export interface IInputValidator { export interface ISCMInput { readonly repository: ISCMRepository; - value: string; + readonly value: string; + setValue(value: string, fromKeyboard: boolean): void; readonly onDidChange: Event; placeholder: string; @@ -97,6 +98,9 @@ export interface ISCMInput { visible: boolean; readonly onDidChangeVisibility: Event; + + showNextHistoryValue(): void; + showPreviousHistoryValue(): void; } export interface ISCMRepository extends IDisposable { diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 50282ded800..b6dcb82722f 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -8,7 +8,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { HistoryNavigator2 } from 'vs/base/common/history'; class SCMInput implements ISCMInput { @@ -18,21 +19,6 @@ class SCMInput implements ISCMInput { return this._value; } - set value(value: string) { - if (value === this._value) { - return; - } - - this._value = value; - - if (this.repository.provider.rootUri) { - const key = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri.path}`; - this.storageService.store(key, value, StorageScope.WORKSPACE); - } - - this._onDidChange.fire(value); - } - private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; @@ -79,14 +65,71 @@ class SCMInput implements ISCMInput { private readonly _onDidChangeValidateInput = new Emitter(); readonly onDidChangeValidateInput: Event = this._onDidChangeValidateInput.event; + private historyNavigator: HistoryNavigator2; + constructor( readonly repository: ISCMRepository, @IStorageService private storageService: IStorageService ) { - if (this.repository.provider.rootUri) { - const key = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri.path}`; - this._value = this.storageService.get(key, StorageScope.WORKSPACE, ''); + const historyKey = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri?.path}`; + let history: string[] | undefined; + let rawHistory = this.storageService.get(historyKey, StorageScope.WORKSPACE, ''); + + if (rawHistory) { + try { + history = JSON.parse(rawHistory); + } catch { + // noop + } } + + if (!history || history.length === 0) { + history = [this._value]; + } else { + this._value = history[history.length - 1]; + } + + this.historyNavigator = new HistoryNavigator2(history, 50); + + this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + if (this.historyNavigator.isAtEnd()) { + this.historyNavigator.replaceLast(this._value); + } + + if (this.repository.provider.rootUri) { + this.storageService.store(historyKey, JSON.stringify([...this.historyNavigator]), StorageScope.WORKSPACE); + } + } + }); + } + + setValue(value: string, transient: boolean) { + if (value === this._value) { + return; + } + + if (!transient) { + this.historyNavigator.replaceLast(this._value); + this.historyNavigator.add(value); + } + + this._value = value; + this._onDidChange.fire(value); + } + + showNextHistoryValue(): void { + const value = this.historyNavigator.next(); + this.setValue(value, true); + } + + showPreviousHistoryValue(): void { + if (this.historyNavigator.isAtEnd()) { + this.historyNavigator.replaceLast(this._value); + } + + const value = this.historyNavigator.previous(); + this.setValue(value, true); } } diff --git a/src/vs/workbench/contrib/search/browser/replaceContributions.ts b/src/vs/workbench/contrib/search/browser/replaceContributions.ts index 23077d8d20a..3c32a737f8b 100644 --- a/src/vs/workbench/contrib/search/browser/replaceContributions.ts +++ b/src/vs/workbench/contrib/search/browser/replaceContributions.ts @@ -7,7 +7,7 @@ import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { ReplaceService, ReplacePreviewContentProvider } from 'vs/workbench/contrib/search/browser/replaceService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export function registerContributions(): void { registerSingleton(IReplaceService, ReplaceService, true); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index c0b8a580b54..041daae2594 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IListService, WorkbenchListFocusContextKey, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { Registry } from 'vs/platform/registry/common/platform'; import { defaultQuickAccessContextKeyValue } from 'vs/workbench/browser/quickaccess'; @@ -571,12 +571,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ properties: { query: { 'type': 'string' }, replace: { 'type': 'string' }, + preserveCase: { 'type': 'boolean' }, triggerSearch: { 'type': 'boolean' }, filesToInclude: { 'type': 'string' }, filesToExclude: { 'type': 'string' }, isRegex: { 'type': 'boolean' }, isCaseSensitive: { 'type': 'boolean' }, matchWholeWord: { 'type': 'boolean' }, + useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, } } }, diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 23c915b19f6..d2f10840868 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -169,7 +169,7 @@ export interface IFindInFilesArgs { isRegex?: boolean; isCaseSensitive?: boolean; matchWholeWord?: boolean; - excludeSettingAndIgnoreFiles?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; } export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFilesArgs = {}) => { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 9afc71db8f5..71a2cae440a 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -780,7 +780,7 @@ export class SearchView extends ViewPane { e.browserEvent.stopPropagation(); const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -1197,8 +1197,8 @@ export class SearchView extends ViewPane { if (typeof args.preserveCase === 'boolean') { this.searchWidget.replaceInput.setPreserveCase(args.preserveCase); } - if (typeof args.excludeSettingAndIgnoreFiles === 'boolean') { - this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(args.excludeSettingAndIgnoreFiles); + if (typeof args.useExcludeSettingsAndIgnoreFiles === 'boolean') { + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(args.useExcludeSettingsAndIgnoreFiles); } } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index fb00b6d69e9..08518d3e33f 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -16,7 +16,7 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; @@ -228,6 +228,37 @@ CommandsRegistry.registerCommand( //#region Actions const category = { value: localize('search', "Search Editor"), original: 'Search Editor' }; +export type LegacySearchEditorArgs = Partial<{ + query: string, + includes: string, + excludes: string, + contextLines: number, + wholeWord: boolean, + caseSensitive: boolean, + regexp: boolean, + useIgnores: boolean, + showIncludesExcludes: boolean, + triggerSearch: boolean, + focusResults: boolean, + location: 'reuse' | 'new' +}>; + +const translateLegacyConfig = (legacyConfig: LegacySearchEditorArgs & OpenSearchEditorArgs = {}): OpenSearchEditorArgs => { + const config: OpenSearchEditorArgs = {}; + const overrides: { [K in keyof LegacySearchEditorArgs]: keyof OpenSearchEditorArgs } = { + includes: 'filesToInclude', + excludes: 'filesToExclude', + wholeWord: 'matchWholeWord', + caseSensitive: 'isCaseSensitive', + regexp: 'isRegexp', + useIgnores: 'useExcludeSettingsAndIgnoreFiles', + }; + Object.entries(legacyConfig).forEach(([key, value]) => { + (config as any)[(overrides as any)[key] ?? key] = value; + }); + return config; +}; + export type OpenSearchEditorArgs = Partial; const openArgDescription = { description: 'Open a new search editor. Arguments passed can include variables like ${relativeFileDirname}.', @@ -236,13 +267,13 @@ const openArgDescription = { schema: { properties: { query: { type: 'string' }, - includes: { type: 'string' }, - excludes: { type: 'string' }, + filesToInclude: { type: 'string' }, + filesToExclude: { type: 'string' }, contextLines: { type: 'number' }, - wholeWord: { type: 'boolean' }, - caseSensitive: { type: 'boolean' }, - regexp: { type: 'boolean' }, - useIgnores: { type: 'boolean' }, + matchWholeWord: { type: 'boolean' }, + isCaseSensitive: { type: 'boolean' }, + isRegexp: { type: 'boolean' }, + useExcludeSettingsAndIgnoreFiles: { type: 'boolean' }, showIncludesExcludes: { type: 'boolean' }, triggerSearch: { type: 'boolean' }, focusResults: { type: 'boolean' }, @@ -285,8 +316,8 @@ registerAction2(class extends Action2 { description: openArgDescription }); } - async run(accessor: ServicesAccessor, args: OpenSearchEditorArgs) { - await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, { ...args, location: 'new' }); + async run(accessor: ServicesAccessor, args: LegacySearchEditorArgs | OpenSearchEditorArgs) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, translateLegacyConfig({ ...args, location: 'new' })); } }); @@ -300,8 +331,8 @@ registerAction2(class extends Action2 { description: openArgDescription }); } - async run(accessor: ServicesAccessor, args: OpenSearchEditorArgs) { - await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, { ...args, location: 'reuse' }); + async run(accessor: ServicesAccessor, args: LegacySearchEditorArgs | OpenSearchEditorArgs) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, translateLegacyConfig({ ...args, location: 'reuse' })); } }); @@ -315,8 +346,8 @@ registerAction2(class extends Action2 { description: openArgDescription }); } - async run(accessor: ServicesAccessor, args: OpenSearchEditorArgs) { - await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args, true); + async run(accessor: ServicesAccessor, args: LegacySearchEditorArgs | OpenSearchEditorArgs) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, translateLegacyConfig(args), true); } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 1d575614d74..820f50030eb 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -439,16 +439,16 @@ export class SearchEditor extends BaseTextEditor { } } - private readConfigFromWidget() { + private readConfigFromWidget(): SearchConfiguration { return { - caseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(), + isCaseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(), contextLines: this.queryEditorWidget.getContextLines(), - excludes: this.inputPatternExcludes.getValue(), - includes: this.inputPatternIncludes.getValue(), + filesToExclude: this.inputPatternExcludes.getValue(), + filesToInclude: this.inputPatternIncludes.getValue(), query: this.queryEditorWidget.searchInput.getValue(), - regexp: this.queryEditorWidget.searchInput.getRegex(), - wholeWord: this.queryEditorWidget.searchInput.getWholeWords(), - useIgnores: this.inputPatternExcludes.useExcludesAndIgnoreFiles(), + isRegexp: this.queryEditorWidget.searchInput.getRegex(), + matchWholeWord: this.queryEditorWidget.searchInput.getWholeWords(), + useExcludeSettingsAndIgnoreFiles: this.inputPatternExcludes.useExcludesAndIgnoreFiles(), showIncludesExcludes: this.showingIncludesExcludes }; } @@ -464,25 +464,25 @@ export class SearchEditor extends BaseTextEditor { this.inputPatternIncludes.onSearchSubmit(); }); - const config: SearchConfiguration = this.readConfigFromWidget(); + const config = this.readConfigFromWidget(); if (!config.query) { return; } const content: IPatternInfo = { pattern: config.query, - isRegExp: config.regexp, - isCaseSensitive: config.caseSensitive, - isWordMatch: config.wholeWord, + isRegExp: config.isRegexp, + isCaseSensitive: config.isCaseSensitive, + isWordMatch: config.matchWholeWord, }; const options: ITextQueryBuilderOptions = { _reason: 'searchEditor', extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), maxResults: 10000, - disregardIgnoreFiles: !config.useIgnores || undefined, - disregardExcludeSettings: !config.useIgnores || undefined, - excludePattern: config.excludes, - includePattern: config.includes, + disregardIgnoreFiles: !config.useExcludeSettingsAndIgnoreFiles || undefined, + disregardExcludeSettings: !config.useExcludeSettingsAndIgnoreFiles || undefined, + excludePattern: config.filesToExclude, + includePattern: config.filesToInclude, previewOptions: { matchLines: 1, charsPerLine: 1000 @@ -527,7 +527,7 @@ export class SearchEditor extends BaseTextEditor { const controller = ReferencesController.get(this.searchResultEditor); controller.closeWidget(false); const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, sortOrder, exit?.limitHit); + const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.filesToInclude, config.filesToExclude, config.contextLines, labelFormatter, sortOrder, exit?.limitHit); const { body } = await input.getModels(); this.modelService.updateModel(body, results.text); input.config = config; @@ -582,13 +582,13 @@ export class SearchEditor extends BaseTextEditor { this.toggleRunAgainMessage(body.getLineCount() === 1 && body.getValue() === '' && config.query !== ''); this.queryEditorWidget.setValue(config.query); - this.queryEditorWidget.searchInput.setCaseSensitive(config.caseSensitive); - this.queryEditorWidget.searchInput.setRegex(config.regexp); - this.queryEditorWidget.searchInput.setWholeWords(config.wholeWord); + this.queryEditorWidget.searchInput.setCaseSensitive(config.isCaseSensitive); + this.queryEditorWidget.searchInput.setRegex(config.isRegexp); + this.queryEditorWidget.searchInput.setWholeWords(config.matchWholeWord); this.queryEditorWidget.setContextLines(config.contextLines); - this.inputPatternExcludes.setValue(config.excludes); - this.inputPatternIncludes.setValue(config.includes); - this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(config.useIgnores); + this.inputPatternExcludes.setValue(config.filesToExclude); + this.inputPatternIncludes.setValue(config.filesToInclude); + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(config.useExcludeSettingsAndIgnoreFiles); this.toggleIncludesExcludes(config.showIncludesExcludes); this.restoreViewState(); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 7a265a1f7a1..a75dbd61ed5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -32,13 +32,13 @@ import { CancellationToken } from 'vs/base/common/cancellation'; export type SearchConfiguration = { query: string, - includes: string, - excludes: string, + filesToInclude: string, + filesToExclude: string, contextLines: number, - wholeWord: boolean, - caseSensitive: boolean, - regexp: boolean, - useIgnores: boolean, + matchWholeWord: boolean, + isCaseSensitive: boolean, + isRegexp: boolean, + useExcludeSettingsAndIgnoreFiles: boolean, showIncludesExcludes: boolean, }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 91cf43f0323..951be05422f 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -108,12 +108,12 @@ function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: string, excludes: string, contextLines: number): SearchConfiguration => { return { query: pattern.contentPattern.pattern, - regexp: !!pattern.contentPattern.isRegExp, - caseSensitive: !!pattern.contentPattern.isCaseSensitive, - wholeWord: !!pattern.contentPattern.isWordMatch, - excludes, includes, + isRegexp: !!pattern.contentPattern.isRegExp, + isCaseSensitive: !!pattern.contentPattern.isCaseSensitive, + matchWholeWord: !!pattern.contentPattern.isWordMatch, + filesToExclude: excludes, filesToInclude: includes, showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles), - useIgnores: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles), + useExcludeSettingsAndIgnoreFiles: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles), contextLines, }; }; @@ -126,15 +126,15 @@ export const serializeSearchConfiguration = (config: Partial ({ query: '', - includes: '', - excludes: '', - regexp: false, - caseSensitive: false, - useIgnores: true, - wholeWord: false, + filesToInclude: '', + filesToExclude: '', + isRegexp: false, + isCaseSensitive: false, + useExcludeSettingsAndIgnoreFiles: true, + matchWholeWord: false, contextLines: 0, showIncludesExcludes: false, }); @@ -189,19 +189,19 @@ export const extractSearchQueryFromLines = (lines: string[]): SearchConfiguratio const [, key, value] = parsed; switch (key) { case 'Query': query.query = unescapeNewlines(value); break; - case 'Including': query.includes = value; break; - case 'Excluding': query.excludes = value; break; + case 'Including': query.filesToInclude = value; break; + case 'Excluding': query.filesToExclude = value; break; case 'ContextLines': query.contextLines = +value; break; case 'Flags': { - query.regexp = value.indexOf('RegExp') !== -1; - query.caseSensitive = value.indexOf('CaseSensitive') !== -1; - query.useIgnores = value.indexOf('IgnoreExcludeSettings') === -1; - query.wholeWord = value.indexOf('WordMatch') !== -1; + query.isRegexp = value.indexOf('RegExp') !== -1; + query.isCaseSensitive = value.indexOf('CaseSensitive') !== -1; + query.useExcludeSettingsAndIgnoreFiles = value.indexOf('IgnoreExcludeSettings') === -1; + query.matchWholeWord = value.indexOf('WordMatch') !== -1; } } } - query.showIncludesExcludes = !!(query.includes || query.excludes || !query.useIgnores); + query.showIncludesExcludes = !!(query.filesToInclude || query.filesToExclude || !query.useExcludeSettingsAndIgnoreFiles); return query; }; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 8bb1ad640f4..2573aae3b12 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -15,6 +15,7 @@ import { localize } from 'vs/nls'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { isPatternInWord } from 'vs/base/common/filters'; +import { StopWatch } from 'vs/base/common/stopwatch'; export class SnippetCompletion implements CompletionItem { @@ -31,11 +32,8 @@ export class SnippetCompletion implements CompletionItem { readonly snippet: Snippet, range: IRange | { insert: IRange, replace: IRange } ) { - this.label = { - name: snippet.prefix, - type: localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source) - }; - this.detail = this.label.type!; + this.label = { name: snippet.prefix, type: snippet.name }; + this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source); this.insertText = snippet.codeSnippet; this.range = range; this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`; @@ -71,6 +69,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { return { suggestions: [] }; } + const sw = new StopWatch(true); const languageId = this._getLanguageIdAtPosition(model, position); const snippets = await this._snippets.getSnippets(languageId); @@ -141,7 +140,10 @@ export class SnippetCompletionProvider implements CompletionItemProvider { } } - return { suggestions }; + return { + suggestions, + duration: sw.elapsed() + }; } resolveCompletionItem(item: CompletionItem): CompletionItem { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 93b7f9272f7..3976dd126b7 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -16,7 +16,7 @@ import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 91ad6206651..9c25777d89d 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -85,7 +85,7 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 1); assert.deepEqual(result.suggestions[0].label, { name: 'bar', - type: 'barTest ()' + type: 'barTest' }); assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); assert.equal(result.suggestions[0].insertText, 'barCodeSnippet'); @@ -120,13 +120,13 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 2); assert.deepEqual(result.suggestions[0].label, { name: 'bar', - type: 'barTest ()' + type: 'barTest' }); assert.equal(result.suggestions[0].insertText, 's1'); assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); assert.deepEqual(result.suggestions[1].label, { name: 'bar-bar', - type: 'name ()' + type: 'name' }); assert.equal(result.suggestions[1].insertText, 's2'); assert.equal((result.suggestions[1].range as any).insert.startColumn, 1); @@ -137,7 +137,7 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 1); assert.deepEqual(result.suggestions[0].label, { name: 'bar-bar', - type: 'name ()' + type: 'name' }); assert.equal(result.suggestions[0].insertText, 's2'); assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); @@ -148,13 +148,13 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 2); assert.deepEqual(result.suggestions[0].label, { name: 'bar', - type: 'barTest ()' + type: 'barTest' }); assert.equal(result.suggestions[0].insertText, 's1'); assert.equal((result.suggestions[0].range as any).insert.startColumn, 5); assert.deepEqual(result.suggestions[1].label, { name: 'bar-bar', - type: 'name ()' + type: 'name' }); assert.equal(result.suggestions[1].insertText, 's2'); assert.equal((result.suggestions[1].range as any).insert.startColumn, 1); @@ -245,11 +245,11 @@ suite('SnippetsService', function () { let [first, second] = result.suggestions; assert.deepEqual(first.label, { name: 'first', - type: 'first ()' + type: 'first' }); assert.deepEqual(second.label, { name: 'second', - type: 'second ()' + type: 'second' }); }); }); @@ -335,7 +335,7 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 1); assert.deepEqual(result.suggestions[0].label, { name: 'mytemplate', - type: 'mytemplate ()' + type: 'mytemplate' }); }); 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 1777327a092..dc2cdaeaf94 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -10,7 +10,7 @@ import { getTotalHeight, getTotalWidth } from 'vs/base/browser/dom'; import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { ColorIdentifier, editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { getThemeTypeSelector, IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index fe63c8a2b3d..273f4cc82a7 100644 --- a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -12,7 +12,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ISurveyData, IProductService } from 'vs/platform/product/common/productService'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IOpenerService } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts index 51ff6db0b40..7f8e32d2842 100644 --- a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts @@ -11,7 +11,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IProductService } from 'vs/platform/product/common/productService'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/tags/electron-browser/tags.contribution.ts b/src/vs/workbench/contrib/tags/electron-browser/tags.contribution.ts index 28995ce564e..209e2690dc9 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/tags.contribution.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/tags.contribution.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { WorkspaceTags } from 'vs/workbench/contrib/tags/electron-browser/workspaceTags'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; // Register Workspace Tags Contribution Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceTags, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/tags/electron-browser/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-browser/workspaceTags.ts index 357ede3af3e..df781b92ff2 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/workspaceTags.ts @@ -18,6 +18,7 @@ import { isWindows } from 'vs/base/common/platform'; import { getRemotes, AllowedSecondLevelDomains, getDomainsOfRemotes } from 'vs/platform/extensionManagement/common/configRemotes'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { IProductService } from 'vs/platform/product/common/productService'; export function getHashedRemotesFromConfig(text: string, stripEndingDotGit: boolean = false): string[] { return getRemotes(text, stripEndingDotGit).map(r => { @@ -35,6 +36,7 @@ export class WorkspaceTags implements IWorkbenchContribution { @ITextFileService private readonly textFileService: ITextFileService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, + @IProductService private readonly productService: IProductService, @INativeHostService private readonly nativeHostService: INativeHostService ) { if (this.telemetryService.isOptedIn) { @@ -219,7 +221,11 @@ export class WorkspaceTags implements IWorkbenchContribution { } private reportProxyStats() { - this.requestService.resolveProxy('https://www.example.com/') + const downloadUrl = this.productService.downloadUrl; + if (!downloadUrl) { + return; + } + this.requestService.resolveProxy(downloadUrl) .then(proxy => { let type = proxy ? String(proxy).trim().split(/\s+/, 1)[0] : 'EMPTY'; if (['DIRECT', 'PROXY', 'HTTPS', 'SOCKS', 'EMPTY'].indexOf(type) === -1) { diff --git a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts index 616f6e4255f..35b943f8750 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts @@ -7,16 +7,9 @@ import * as crypto from 'crypto'; import { IFileService, IResolveFileResult, IFileStat } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { INotificationService, NeverShowAgainScope, INeverShowAgainOptions } from 'vs/platform/notification/common/notification'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ITextFileService, ITextFileContent } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { localize } from 'vs/nls'; -import Severity from 'vs/base/common/severity'; -import { joinPath } from 'vs/base/common/resources'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { getHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/electron-browser/workspaceTags'; @@ -133,15 +126,12 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @IHostService private readonly hostService: IHostService, - @INotificationService private readonly notificationService: INotificationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, @ITextFileService private readonly textFileService: ITextFileService ) { } async getTags(): Promise { if (!this._tags) { - this._tags = await this.resolveWorkspaceTags(rootFiles => this.handleWorkspaceFiles(rootFiles)); + this._tags = await this.resolveWorkspaceTags(); } return this._tags; @@ -301,7 +291,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.playwright" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ - private resolveWorkspaceTags(participant?: (rootFiles: string[]) => void): Promise { + private resolveWorkspaceTags(): Promise { const tags: Tags = Object.create(null); const state = this.contextService.getWorkbenchState(); @@ -318,7 +308,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { tags['workspace.empty'] = isEmpty; const folders = !isEmpty ? workspace.folders.map(folder => folder.uri) : this.productService.quality !== 'stable' && this.findFolders(); - if (!folders || !folders.length || !this.fileService) { + if (!folders || !folders.length) { return Promise.resolve(tags); } @@ -326,10 +316,6 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { const names = ([]).concat(...files.map(result => result.success ? (result.stat!.children || []) : [])).map(c => c.name); const nameSet = names.reduce((s, n) => s.add(n.toLowerCase()), new Set()); - if (participant) { - participant(names); - } - tags['workspace.grunt'] = nameSet.has('gruntfile.js'); tags['workspace.gulp'] = nameSet.has('gulpfile.js'); tags['workspace.jake'] = nameSet.has('jakefile.js'); @@ -485,49 +471,6 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { }); } - private handleWorkspaceFiles(rootFiles: string[]): void { - const state = this.contextService.getWorkbenchState(); - const workspace = this.contextService.getWorkspace(); - - // Handle top-level workspace files for local single folder workspace - if (state === WorkbenchState.FOLDER) { - const workspaceFiles = rootFiles.filter(hasWorkspaceFileExtension); - if (workspaceFiles.length > 0) { - this.doHandleWorkspaceFiles(workspace.folders[0].uri, workspaceFiles); - } - } - } - - private doHandleWorkspaceFiles(folder: URI, workspaces: string[]): void { - const neverShowAgain: INeverShowAgainOptions = { id: 'workspaces.dontPromptToOpen', scope: NeverShowAgainScope.WORKSPACE, isSecondary: true }; - - // Prompt to open one workspace - if (workspaces.length === 1) { - const workspaceFile = workspaces[0]; - - this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ - label: localize('openWorkspace', "Open Workspace"), - run: () => this.hostService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) - }], { neverShowAgain }); - } - - // Prompt to select a workspace from many - else if (workspaces.length > 1) { - this.notificationService.prompt(Severity.Info, localize('workspacesFound', "This folder contains multiple workspace files. Do you want to open one? [Learn more]({0}) about workspace files.", 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ - label: localize('selectWorkspace', "Select Workspace"), - run: () => { - this.quickInputService.pick( - workspaces.map(workspace => ({ label: workspace } as IQuickPickItem)), - { placeHolder: localize('selectToOpen', "Select a workspace to open") }).then(pick => { - if (pick) { - this.hostService.openWindow([{ workspaceUri: joinPath(folder, pick.label) }]); - } - }); - } - }], { neverShowAgain }); - } - } - private findFolders(): URI[] | undefined { const folder = this.findFolder(); return folder && [folder]; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index c211f46c15a..54b59c57c23 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -20,7 +20,7 @@ import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; import { LRUCache, Touch } from 'vs/base/common/map'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -213,7 +213,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected _taskSystemInfos: Map; protected _workspaceTasksPromise?: Promise>; - protected _areJsonTasksSupportedPromise: Promise = Promise.resolve(false); protected _taskSystem?: ITaskSystem; protected _taskSystemListener?: IDisposable; @@ -543,7 +542,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public registerTaskSystem(key: string, info: TaskSystemInfo): void { - this._taskSystemInfos.set(key, info); + if (!this._taskSystemInfos.has(key) || info.platform !== Platform.Platform.Web) { + this._taskSystemInfos.set(key, info); + } + } + + private getTaskSystemInfo(key: string): TaskSystemInfo | undefined { + if (this.environmentService.remoteAuthority) { + return this._taskSystemInfos.get(key); + } + return undefined; } public extensionCallbackTaskComplete(task: Task, result: number): Promise { @@ -1588,7 +1596,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.pathService, this.viewDescriptorService, this.logService, (workspaceFolder: IWorkspaceFolder | undefined) => { if (workspaceFolder) { - return this._taskSystemInfos.get(workspaceFolder.uri.scheme); + return this.getTaskSystemInfo(workspaceFolder.uri.scheme); } else if (this._taskSystemInfos.size > 0) { return this._taskSystemInfos.values().next().value; } else { @@ -1883,8 +1891,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - public setJsonTasksSupported(areSupported: Promise) { - this._areJsonTasksSupportedPromise = areSupported; + private get jsonTasksSupported(): boolean { + return !!ShellExecutionSupportedContext.getValue(this.contextKeyService) && !!ProcessExecutionSupportedContext.getValue(this.contextKeyService); } private computeWorkspaceFolderTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise { @@ -1896,7 +1904,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return Promise.resolve({ workspaceFolder, set: undefined, configurations: undefined, hasErrors: workspaceFolderConfiguration ? workspaceFolderConfiguration.hasErrors : false }); } return ProblemMatcherRegistry.onReady().then(async (): Promise => { - let taskSystemInfo: TaskSystemInfo | undefined = this._taskSystemInfos.get(workspaceFolder.uri.scheme); + let taskSystemInfo: TaskSystemInfo | undefined = this.getTaskSystemInfo(workspaceFolder.uri.scheme); let problemReporter = new ProblemReporter(this._outputChannel); let parseResult = TaskConfig.parse(workspaceFolder, undefined, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, workspaceFolderConfiguration.config!, problemReporter, TaskConfig.TaskConfigSource.TasksJson, this.contextKeyService); let hasErrors = false; @@ -1917,10 +1925,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer customizedTasks.byIdentifier[task.configures._key] = task; } } - if (!(await this._areJsonTasksSupportedPromise) && (parseResult.custom.length > 0)) { + if (!this.jsonTasksSupported && (parseResult.custom.length > 0)) { console.warn('Custom workspace tasks are not supported.'); } - return { workspaceFolder, set: { tasks: await this._areJsonTasksSupportedPromise ? parseResult.custom : [] }, configurations: customizedTasks, hasErrors }; + return { workspaceFolder, set: { tasks: this.jsonTasksSupported ? parseResult.custom : [] }, configurations: customizedTasks, hasErrors }; }); }); } @@ -1993,7 +2001,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!config) { return false; } - let taskSystemInfo: TaskSystemInfo | undefined = workspaceFolder ? this._taskSystemInfos.get(workspaceFolder.uri.scheme) : undefined; + let taskSystemInfo: TaskSystemInfo | undefined = workspaceFolder ? this.getTaskSystemInfo(workspaceFolder.uri.scheme) : undefined; let problemReporter = new ProblemReporter(this._outputChannel); let parseResult = TaskConfig.parse(workspaceFolder, this._workspace, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, config, problemReporter, source, this.contextKeyService, isRecentTask); let hasErrors = false; @@ -2010,7 +2018,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer customized[task.configures._key] = task; } } - if (!(await this._areJsonTasksSupportedPromise) && (parseResult.custom.length > 0)) { + if (!this.jsonTasksSupported && (parseResult.custom.length > 0)) { console.warn('Custom workspace tasks are not supported.'); } else { for (let task of parseResult.custom) { diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 190091c80f2..d938abe46af 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ProblemMatcherRegistry } from 'vs/workbench/contrib/tasks/common/problemMatcher'; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index aa4865001ac..f9622690061 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1085,6 +1085,7 @@ export class TerminalTaskSystem implements ITaskSystem { if (options.env) { shellLaunchConfig.env = options.env; } + shellLaunchConfig.isFeatureTerminal = true; return shellLaunchConfig; } @@ -1118,7 +1119,8 @@ export class TerminalTaskSystem implements ITaskSystem { isExtensionTerminal: true, waitOnExit, name: this.createTerminalName(task), - initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined + initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined, + isFeatureTerminal: true }; } else { let resolvedResult: { command: CommandString, args: CommandString[] } = this.resolveCommandAndArgs(resolver, task.command); diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index 7f395bdf770..19e45d10594 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -96,7 +96,6 @@ export interface ITaskService { registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void; - setJsonTasksSupported(areSuppored: Promise): void; extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; } diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 2b418979187..004f9217c13 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase, ILifecycleService, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index c2a23641883..10c3aca946e 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -17,7 +17,7 @@ import type { Terminal, IViewportRange, ILinkProvider } from 'xterm'; import { Schemas } from 'vs/base/common/network'; import { posix, win32 } from 'vs/base/common/path'; import { ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { OperatingSystem, isMacintosh, OS } from 'vs/base/common/platform'; +import { OperatingSystem, isMacintosh, OS, isWindows } from 'vs/base/common/platform'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider'; import { TerminalValidatedLocalLinkProvider, lineAndColumnClause, unixLocalLinkClause, winLocalLinkClause, winDrivePrefix, winLineAndColumnMatchIndex, unixLineAndColumnMatchIndex, lineAndColumnClauseGroupCount } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider'; @@ -34,6 +34,7 @@ export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isVali interface IPath { join(...paths: string[]): string; normalize(path: string): string; + sep: '\\' | '/'; } /** @@ -192,7 +193,9 @@ export class TerminalLinkManager extends DisposableStore { // respect line/col attachment const uri = URI.parse(link); if (uri.scheme === Schemas.file) { - this._handleLocalLink(uri.fsPath); + // Just using fsPath here is unsafe: https://github.com/microsoft/vscode/issues/109076 + const fsPath = uri.fsPath; + this._handleLocalLink(((this.osPath.sep === posix.sep) && isWindows) ? fsPath.replace(/\\/g, posix.sep) : fsPath); return; } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 959f572ed2d..2fca8bf9288 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -8,24 +8,21 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IRemoteTerminalService, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteTerminalProcessExecCommandEvent, IShellLaunchConfigDto, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IProcessDataEvent, IRemoteTerminalAttachTarget, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class RemoteTerminalService extends Disposable implements IRemoteTerminalService { public _serviceBrand: undefined; private readonly _remoteTerminalChannel: RemoteTerminalChannelClient | null; - private _hasConnectedToRemote = false; constructor( - @ITerminalService _terminalService: ITerminalService, @ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @ILogService private readonly _logService: ILogService, @@ -46,34 +43,42 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal throw new Error(`Cannot create remote terminal when there is no remote!`); } - let isPreconnectionTerminal = false; - if (!this._hasConnectedToRemote) { - isPreconnectionTerminal = true; - this._remoteAgentService.getEnvironment().then(() => { - this._hasConnectedToRemote = true; - }); - } + return new RemoteTerminalProcess(terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper, this._remoteTerminalChannel, this._remoteAgentService, this._logService, this._commandService); + } - return new RemoteTerminalProcess(terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper, isPreconnectionTerminal, this._remoteTerminalChannel, this._remoteAgentService, this._logService, this._commandService); + public async listTerminals(): Promise { + const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listTerminals() : []; + return terms.map(termDto => { + return { + id: termDto.id, + pid: termDto.pid, + title: termDto.title, + cwd: termDto.cwd + }; + }); } } export class RemoteTerminalProcess extends Disposable implements ITerminalChildProcess { - public readonly _onProcessData = this._register(new Emitter()); - public readonly onProcessData: Event = this._onProcessData.event; + public readonly _onProcessData = this._register(new Emitter()); + public readonly onProcessData: Event = this._onProcessData.event; private readonly _onProcessExit = this._register(new Emitter()); public readonly onProcessExit: Event = this._onProcessExit.event; public readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = this._register(new Emitter()); public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; + private readonly _onProcessOverrideDimensions = this._register(new Emitter()); + public readonly onProcessOverrideDimensions: Event = this._onProcessOverrideDimensions.event; private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } private _startBarrier: Barrier; private _remoteTerminalId: number; + private _inReplay = false; + constructor( private readonly _terminalId: number, private readonly _shellLaunchConfig: IShellLaunchConfig, @@ -81,7 +86,6 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP private readonly _cols: number, private readonly _rows: number, private readonly _configHelper: ITerminalConfigHelper, - private readonly _isPreconnectionTerminal: boolean, private readonly _remoteTerminalChannel: RemoteTerminalChannelClient, private readonly _remoteAgentService: IRemoteAgentService, private readonly _logService: ILogService, @@ -94,11 +98,6 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP public async start(): Promise { - // Add a loading title only if this terminal is instantiated before a connection is up and running - if (this._isPreconnectionTerminal) { - setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); - } - // Fetch the environment to check shell permissions const env = await this._remoteAgentService.getEnvironment(); if (!env) { @@ -106,49 +105,46 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP throw new Error('Could not fetch remote environment'); } - const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(env.os); + if (!this._shellLaunchConfig.remoteAttach) { + const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(env.os); - const shellLaunchConfigDto: IShellLaunchConfigDto = { - name: this._shellLaunchConfig.name, - executable: this._shellLaunchConfig.executable, - args: this._shellLaunchConfig.args, - cwd: this._shellLaunchConfig.cwd, - env: this._shellLaunchConfig.env - }; + const shellLaunchConfigDto: IShellLaunchConfigDto = { + name: this._shellLaunchConfig.name, + executable: this._shellLaunchConfig.executable, + args: this._shellLaunchConfig.args, + cwd: this._shellLaunchConfig.cwd, + env: this._shellLaunchConfig.env + }; - this._logService.trace('Spawning remote agent process', { terminalId: this._terminalId, shellLaunchConfigDto }); + this._logService.trace('Spawning remote agent process', { terminalId: this._terminalId, shellLaunchConfigDto }); - const result = await this._remoteTerminalChannel.createTerminalProcess( - shellLaunchConfigDto, - this._activeWorkspaceRootUri, - this._cols, - this._rows, - isWorkspaceShellAllowed, - ); + const result = await this._remoteTerminalChannel.createTerminalProcess( + shellLaunchConfigDto, + this._activeWorkspaceRootUri, + !this._shellLaunchConfig.isFeatureTerminal, + this._cols, + this._rows, + isWorkspaceShellAllowed, + ); - this._remoteTerminalId = result.terminalId; - this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._remoteTerminalId)(event => { - switch (event.type) { - case 'ready': - return this._onProcessReady.fire({ pid: event.pid, cwd: event.cwd }); - case 'titleChanged': - return this._onProcessTitleChanged.fire(event.title); - case 'data': - return this._onProcessData.fire(event.data); - case 'exit': - return this._onProcessExit.fire(event.exitCode); - case 'execCommand': - return this._execCommand(event); + this._remoteTerminalId = result.terminalId; + this.setupTerminalEventListener(); + this._onProcessResolvedShellLaunchConfig.fire(reviveIShellLaunchConfig(result.resolvedShellLaunchConfig)); + + const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._remoteTerminalId); + + if (typeof startResult !== 'undefined') { + // An error occurred + return startResult; } - })); + } else { + this._remoteTerminalId = this._shellLaunchConfig.remoteAttach.id; + this._onProcessReady.fire({ pid: this._shellLaunchConfig.remoteAttach.pid, cwd: this._shellLaunchConfig.remoteAttach.cwd }); + this.setupTerminalEventListener(); - this._onProcessResolvedShellLaunchConfig.fire(reviveIShellLaunchConfig(result.resolvedShellLaunchConfig)); - - const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._remoteTerminalId); - - if (typeof startResult !== 'undefined') { - // An error occurred - return startResult; + setTimeout(() => { + this._onProcessTitleChanged.fire(this._shellLaunchConfig.remoteAttach!.title); + }, 0); } this._startBarrier.open(); @@ -162,13 +158,62 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP } public input(data: string): void { + if (this._inReplay) { + return; + } + this._startBarrier.wait().then(_ => { this._remoteTerminalChannel.sendInputToTerminalProcess(this._remoteTerminalId, data); }); } + private setupTerminalEventListener(): void { + this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._remoteTerminalId)(event => { + switch (event.type) { + case 'ready': + return this._onProcessReady.fire({ pid: event.pid, cwd: event.cwd }); + case 'titleChanged': + return this._onProcessTitleChanged.fire(event.title); + case 'data': + return this._onProcessData.fire({ data: event.data, sync: false }); + case 'replay': { + try { + this._inReplay = true; + + for (const e of event.events) { + if (e.cols !== 0 || e.rows !== 0) { + // never override with 0x0 as that is a marker for an unknown initial size + this._onProcessOverrideDimensions.fire({ cols: e.cols, rows: e.rows, forceExactSize: true }); + } + this._onProcessData.fire({ data: e.data, sync: true }); + } + } finally { + this._inReplay = false; + } + + // remove size override + this._onProcessOverrideDimensions.fire(undefined); + + return; + } + case 'exit': + return this._onProcessExit.fire(event.exitCode); + case 'execCommand': + return this._execCommand(event); + case 'orphan?': { + this._remoteTerminalChannel.orphanQuestionReply(this._remoteTerminalId); + return; + } + } + })); + } + public resize(cols: number, rows: number): void { + if (this._inReplay) { + return; + } this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.resizeTerminalProcess(this._remoteTerminalId, cols, rows); }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 1e8c415f41e..2ba4774be33 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -7,7 +7,7 @@ import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; -import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro, IRemoteTerminalAttachTarget } from 'vs/workbench/contrib/terminal/common/terminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; @@ -79,6 +79,7 @@ export interface ITerminalService { terminalTabs: ITerminalTab[]; isProcessSupportRegistered: boolean; + initializeTerminals(): Promise; onActiveTabChanged: Event; onTabDisposed: Event; onInstanceCreated: Event; @@ -89,7 +90,7 @@ export interface ITerminalService { onInstanceRequestSpawnExtHostProcess: Event; onInstanceRequestStartExtensionTerminal: Event; onInstancesChanged: Event; - onInstanceTitleChanged: Event; + onInstanceTitleChanged: Event; onActiveInstanceChanged: Event; onRequestAvailableShells: Event; onDidRegisterProcessSupport: Event; @@ -180,6 +181,7 @@ export interface IRemoteTerminalService { dispose(): void; + listTerminals(): Promise; createRemoteTerminalProcess(terminalId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, configHelper: ITerminalConfigHelper,): Promise; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 002bfbabf9c..23bf8cf8cd1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -79,23 +79,9 @@ export class ToggleTerminalAction extends ToggleViewAction { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITerminalService private readonly terminalService: ITerminalService ) { super(id, label, TERMINAL_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService); } - - async run() { - if (this.terminalService.isProcessSupportRegistered && this.terminalService.terminalInstances.length === 0) { - // If there is not yet an instance attempt to create it here so that we can suggest a - // new shell on Windows (and not do so when the panel is restored on reload). - const newTerminalInstance = this.terminalService.createTerminal(undefined); - const toDispose = newTerminalInstance.onProcessIdReady(() => { - newTerminalInstance.focus(); - toDispose.dispose(); - }); - } - return super.run(); - } } export class KillTerminalAction extends Action { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 35b90cbeaa0..6dc11347198 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -40,6 +40,9 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { private readonly _onWorkspacePermissionsChanged = new Emitter(); public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } + private readonly _onConfigChanged = new Emitter(); + public get onConfigChanged(): Event { return this._onConfigChanged.event; } + public constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @@ -71,6 +74,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { configValues.fontWeightBold = this._normalizeFontWeight(configValues.fontWeightBold, DEFAULT_BOLD_FONT_WEIGHT); this.config = configValues; + this._onConfigChanged.fire(); } public configFontIsMonospace(): boolean { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c781273cd58..544318a4085 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -25,7 +25,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { IShellLaunchConfig, ITerminalDimensions, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalLaunchError, IProcessDataEvent, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; @@ -44,7 +44,7 @@ import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { LatencyTelemetryAddon } from 'vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon'; +import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -94,7 +94,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _terminalA11yTreeFocusContextKey: IContextKey; private _cols: number = 0; private _rows: number = 0; - private _dimensionsOverride: ITerminalDimensions | undefined; + private _dimensionsOverride: ITerminalDimensionsOverride | undefined; private _windowsShellHelper: IWindowsShellHelper | undefined; private _xtermReadyPromise: Promise; private _titleReadyPromise: Promise; @@ -117,12 +117,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { public get id(): number { return this._id; } public get cols(): number { if (this._dimensionsOverride && this._dimensionsOverride.cols) { + if (this._dimensionsOverride.forceExactSize) { + return this._dimensionsOverride.cols; + } return Math.min(Math.max(this._dimensionsOverride.cols, 2), this._cols); } return this._cols; } public get rows(): number { if (this._dimensionsOverride && this._dimensionsOverride.rows) { + if (this._dimensionsOverride.forceExactSize) { + return this._dimensionsOverride.rows; + } return Math.min(Math.max(this._dimensionsOverride.rows, 2), this._rows); } return this._rows; @@ -415,7 +421,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._xterm.onKey(e => this._onKey(e.key, e.domEvent)); this._xterm.onSelectionChange(async () => this._onSelectionChange()); - this._processManager.onProcessData(data => this._onProcessData(data)); + this._processManager.onProcessData(e => this._onProcessData(e)); this._xterm.onData(data => this._processManager.write(data)); this.processReady.then(async () => { if (this._linkManager) { @@ -449,8 +455,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } })); - const latencyAddon = this._register(this._instantiationService.createInstance(LatencyTelemetryAddon, this._processManager)); - this._xterm.loadAddon(latencyAddon); + const typeaheadAddon = this._register(this._instantiationService.createInstance(TypeAheadAddon, this._processManager, this._configHelper)); + this._xterm.loadAddon(typeaheadAddon); return xterm; } @@ -882,11 +888,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); - this._processManager.onProcessData(data => { - this._initialDataEvents?.push(data); - this._onData.fire(data); + this._processManager.onProcessData(ev => { + this._initialDataEvents?.push(ev.data); + this._onData.fire(ev.data); }); - this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e)); + this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e, true)); this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)); @@ -959,9 +965,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - private _onProcessData(data: string): void { - const messageId = ++this._latestXtermWriteData; - this._xterm?.write(data, () => this._latestXtermParseData = messageId); + private _onProcessData(ev: IProcessDataEvent): void { + if (ev.sync) { + this._xtermCore?.writeSync(ev.data); + } else { + const messageId = ++this._latestXtermWriteData; + this._xterm?.write(ev.data, () => this._latestXtermParseData = messageId); + } } /** @@ -1349,6 +1359,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @debounce(50) private async _resize(): Promise { + this._resizeNow(false); + } + + private async _resizeNow(immediate: boolean): Promise { let cols = this.cols; let rows = this.rows; @@ -1398,8 +1412,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - await this._processManager.ptyProcessReady; - this._processManager.setDimensions(cols, rows); + if (immediate) { + // do not await, call setDimensions synchronously + this._processManager.setDimensions(cols, rows); + } else { + await this._processManager.ptyProcessReady; + this._processManager.setDimensions(cols, rows); + } } public setShellType(shellType: TerminalShellType) { @@ -1442,9 +1461,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return this._titleReadyPromise; } - public setDimensions(dimensions: ITerminalDimensions | undefined): void { + public setDimensions(dimensions: ITerminalDimensionsOverride | undefined, immediate: boolean = false): void { + if (this._dimensionsOverride && this._dimensionsOverride.forceExactSize && !dimensions && this._rows === 0 && this._cols === 0) { + // this terminal never had a real size => keep the last dimensions override exact size + this._cols = this._dimensionsOverride.cols; + this._rows = this._dimensionsOverride.rows; + } this._dimensionsOverride = dimensions; - this._resize(); + if (immediate) { + this._resizeNow(true); + } else { + this._resize(); + } } private _setResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts deleted file mode 100644 index ba307010967..00000000000 --- a/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IBeforeProcessDataEvent, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; -import type { ITerminalAddon, Terminal } from 'xterm'; - -interface ITypedChar { - char: string; - time: number; -} - -// Collect data in 5 minute chunks -const TELEMETRY_TIMEOUT = 1000 * 60 * 5; - -export class LatencyTelemetryAddon extends Disposable implements ITerminalAddon { - private _terminal!: Terminal; - private _typedQueue: ITypedChar[] = []; - private _activeTimer: any; - private _unprocessedLatencies: number[] = []; - - constructor( - private readonly _processManager: ITerminalProcessManager, - @ITelemetryService private readonly _telemetryService: ITelemetryService - ) { - super(); - } - - public activate(terminal: Terminal): void { - this._terminal = terminal; - this._register(terminal.onData(e => this._onData(e))); - this._register(this._processManager.onBeforeProcessData(e => this._onBeforeProcessData(e))); - } - - private async _triggerTelemetryReport(): Promise { - if (!this._activeTimer) { - this._activeTimer = setTimeout(() => { - this._sendTelemetryReport(); - this._activeTimer = undefined; - }, TELEMETRY_TIMEOUT); - } - } - - private _sendTelemetryReport(): void { - if (this._unprocessedLatencies.length < 10) { - return; - } - - /* __GDPR__ - "terminalLatencyStats" : { - "min" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "max" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "median" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - } - */ - const median = this._unprocessedLatencies.sort()[Math.floor(this._unprocessedLatencies.length / 2)]; - this._telemetryService.publicLog('terminalLatencyStats', { - min: Math.min(...this._unprocessedLatencies), - max: Math.max(...this._unprocessedLatencies), - median, - count: this._unprocessedLatencies.length - }); - this._unprocessedLatencies.length = 0; - } - - private _onData(data: string): void { - if (this._terminal.buffer.active.type === 'alternate') { - return; - } - - const code = data.charCodeAt(0); - if (data.length === 1 && code >= 32 && code <= 126) { - const typed: ITypedChar = { - char: data, - time: Date.now() - }; - this._typedQueue.push(typed); - } - } - - private _onBeforeProcessData(event: IBeforeProcessDataEvent): void { - if (!this._typedQueue.length) { - return; - } - - const cleanText = removeAnsiEscapeCodes(event.data); - for (let i = 0; i < cleanText.length; i++) { - if (this._typedQueue[0] && this._typedQueue[0].char === cleanText[i]) { - const success = this._typedQueue.shift()!; - const latency = Date.now() - success.time; - this._unprocessedLatencies.push(latency); - this._triggerTelemetryReport(); - } else { - this._typedQueue.length = 0; - break; - } - } - } -} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 84f18d3c998..c7069f89ff0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { ITerminalProcessExtHostProxy, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessExtHostProxy, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensions, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -23,8 +23,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = this._register(new Emitter()); public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } + private readonly _onProcessOverrideDimensions = this._register(new Emitter()); + public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 0dd449b36f4..d9bb4929c97 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -6,7 +6,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { env as processEnv } from 'vs/base/common/process'; -import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent, ITerminalEnvironment, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent, ITerminalEnvironment, ITerminalLaunchError, IProcessDataEvent, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -69,14 +69,14 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce public get onProcessReady(): Event { return this._onProcessReady.event; } private readonly _onBeforeProcessData = this._register(new Emitter()); public get onBeforeProcessData(): Event { return this._onBeforeProcessData.event; } - private readonly _onProcessData = this._register(new Emitter()); - public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessData = this._register(new Emitter()); + public get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessTitle = this._register(new Emitter()); public get onProcessTitle(): Event { return this._onProcessTitle.event; } private readonly _onProcessExit = this._register(new Emitter()); public get onProcessExit(): Event { return this._onProcessExit.event; } - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } + private readonly _onProcessOverrideDimensions = this._register(new Emitter()); + public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } private readonly _onProcessOverrideShellLaunchConfig = this._register(new Emitter()); public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessOverrideShellLaunchConfig.event; } private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter()); @@ -158,8 +158,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); - const enableRemoteAgentTerminals = this._workspaceConfigurationService.getValue('terminal.integrated.serverSpawn'); - if (enableRemoteAgentTerminals) { + const enableRemoteAgentTerminals = this._workspaceConfigurationService.getValue('terminal.integrated.serverSpawn'); + if (enableRemoteAgentTerminals !== false) { this._process = await this._remoteTerminalService.createRemoteTerminalProcess(this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); } else { this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); @@ -171,11 +171,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this.processState = ProcessState.LAUNCHING; - this._process.onProcessData(data => { + this._process.onProcessData(ev => { + const data = (typeof ev === 'string' ? ev : ev.data); + const sync = (typeof ev === 'string' ? false : ev.sync); const beforeProcessDataEvent: IBeforeProcessDataEvent = { data }; this._onBeforeProcessData.fire(beforeProcessDataEvent); if (beforeProcessDataEvent.data && beforeProcessDataEvent.data.length > 0) { - this._onProcessData.fire(beforeProcessDataEvent.data); + this._onProcessData.fire({ data: beforeProcessDataEvent.data, sync }); } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 92b03fff2ab..7bb6f9d1c59 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -3,32 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError, ITerminalNativeWindowsDelegate } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; -import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; -import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { Event, Emitter } from 'vs/base/common/event'; +import { timeout } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { basename } from 'vs/base/common/path'; +import { isMacintosh, isWeb, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import * as nls from 'vs/nls'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; +import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; +import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; +import { IAvailableShellsRequest, IShellDefinition, IShellLaunchConfig, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalLaunchError, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { isWindows, isMacintosh, OperatingSystem, isWeb } from 'vs/base/common/platform'; -import { basename } from 'vs/base/common/path'; -import { timeout } from 'vs/base/common/async'; -import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; interface IExtHostReadyEntry { promise: Promise; @@ -85,8 +86,8 @@ export class TerminalService implements ITerminalService { public get onInstanceMaximumDimensionsChanged(): Event { return this._onInstanceMaximumDimensionsChanged.event; } private readonly _onInstancesChanged = new Emitter(); public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } - private readonly _onInstanceTitleChanged = new Emitter(); - public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + private readonly _onInstanceTitleChanged = new Emitter(); + public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } private readonly _onActiveInstanceChanged = new Emitter(); public get onActiveInstanceChanged(): Event { return this._onActiveInstanceChanged.event; } private readonly _onTabDisposed = new Emitter(); @@ -108,7 +109,10 @@ export class TerminalService implements ITerminalService { @IConfigurationService private _configurationService: IConfigurationService, @IViewsService private _viewsService: IViewsService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + @IConfigurationService private readonly _workspaceConfigurationService: IConfigurationService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { this._activeTabIndex = 0; this._isShuttingDown = false; @@ -221,7 +225,7 @@ export class TerminalService implements ITerminalService { } public getTabLabels(): string[] { - return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`); + return this._terminalTabs.map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`); } public getFindState(): FindReplaceState { @@ -339,6 +343,39 @@ export class TerminalService implements ITerminalService { } } + public async initializeTerminals(): Promise { + const enableRemoteAgentTerminals = this._workspaceConfigurationService.getValue('terminal.integrated.serverSpawn'); + if (!!this._environmentService.remoteAuthority && enableRemoteAgentTerminals !== false) { + const emptyTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, undefined); + this._terminalTabs.push(emptyTab); + this._onInstanceTitleChanged.fire(undefined); + const remoteTerms = await this._remoteTerminalService.listTerminals(); + + /* __GDPR__ + "terminalReconnect" : { + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + const data = { + count: remoteTerms.length + }; + this._telemetryService.publicLog('terminalReconnection', data); + if (remoteTerms.length > 0) { + // Reattach to all remote terms + this.createTerminal({ remoteAttach: remoteTerms[0] }, emptyTab); + for (let term of remoteTerms.slice(1)) { + this.createTerminal({ remoteAttach: term }); + } + } else if (this.terminalInstances.length === 0) { + // Remote, no terminals to attach to + this.createTerminal(undefined, emptyTab); + } + } else if (this.terminalInstances.length === 0) { + // Local, just create a terminal + this.createTerminal(); + } + } + private _getInstanceFromGlobalInstanceIndex(index: number): { tab: ITerminalTab, tabIndex: number, instance: ITerminalInstance, localInstanceIndex: number } | null { let currentTabIndex = 0; while (index >= 0 && currentTabIndex < this._terminalTabs.length) { @@ -610,7 +647,7 @@ export class TerminalService implements ITerminalService { return instance; } - public createTerminal(shell: IShellLaunchConfig = {}): ITerminalInstance { + public createTerminal(shell: IShellLaunchConfig = {}, terminalTab?: TerminalTab): ITerminalInstance { if (!this.isProcessSupportRegistered) { throw new Error('Could not create terminal when process support is not registered'); } @@ -620,8 +657,14 @@ export class TerminalService implements ITerminalService { this._initInstanceListeners(instance); return instance; } - const terminalTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, shell); - this._terminalTabs.push(terminalTab); + + if (terminalTab) { + terminalTab.addInstance(shell); + } else { + terminalTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, shell); + this._terminalTabs.push(terminalTab); + } + const instance = terminalTab.terminalInstances[0]; terminalTab.addDisposable(terminalTab.onDisposed(this._onTabDisposed.fire, this._onTabDisposed)); terminalTab.addDisposable(terminalTab.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts index 360f0c4a8ec..a798660b2f7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IShellLaunchConfig, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; @@ -56,7 +57,7 @@ class SplitPaneContainer extends Disposable { (this.orientation === Orientation.VERTICAL && direction === Direction.Right)) { amount *= -1; } - this._layoutService.resizePart(Parts.PANEL_PART, amount); + this._layoutService.resizePart(Parts.PANEL_PART, amount, amount); return; } @@ -228,7 +229,7 @@ export class TerminalTab extends Disposable implements ITerminalTab { constructor( private _container: HTMLElement | undefined, - shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance, + shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance | undefined, @ITerminalService private readonly _terminalService: ITerminalService, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @@ -236,6 +237,18 @@ export class TerminalTab extends Disposable implements ITerminalTab { ) { super(); + if (shellLaunchConfigOrInstance) { + this.addInstance(shellLaunchConfigOrInstance); + } + + this._activeInstanceIndex = 0; + + if (this._container) { + this.attachToElement(this._container); + } + } + + public addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance): void { let instance: ITerminalInstance; if ('id' in shellLaunchConfigOrInstance) { instance = shellLaunchConfigOrInstance; @@ -244,11 +257,12 @@ export class TerminalTab extends Disposable implements ITerminalTab { } this._terminalInstances.push(instance); this._initInstanceListeners(instance); - this._activeInstanceIndex = 0; - if (this._container) { - this.attachToElement(this._container); + if (this._splitPaneContainer) { + this._splitPaneContainer!.split(instance); } + + this._onInstancesChanged.fire(); } public dispose(): void { @@ -358,6 +372,10 @@ export class TerminalTab extends Disposable implements ITerminalTab { } public get title(): string { + if (!this.terminalInstances.length) { + return nls.localize('terminal.integrated.starting', "Starting..."); + } + let title = this.terminalInstances[0].title; for (let i = 1; i < this.terminalInstances.length; i++) { if (this.terminalInstances[i].title) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts new file mode 100644 index 00000000000..c0551e32c31 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -0,0 +1,912 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Color } from 'vs/base/common/color'; +import { debounce } from 'vs/base/common/decorators'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { IBeforeProcessDataEvent, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import type { IBuffer, IBufferCell, ITerminalAddon, Terminal } from 'xterm'; + +const ESC = '\x1b'; +const CSI = `${ESC}[`; +const SHOW_CURSOR = `${CSI}?25h`; +const HIDE_CURSOR = `${CSI}?25l`; +const DELETE_CHAR = `${CSI}X`; +const CSI_STYLE_RE = /^\x1b\[[0-9;]*m/; +const CSI_MOVE_RE = /^\x1b\[([0-9]*)(;[35])?O?([DC])/; +const PASSWORD_INPUT_RE = /(password|passphrase|passwd).*:/i; +const NOT_WORD_RE = /\W/; + +const statsBufferSize = 24; +const statsSendTelemetryEvery = 1000 * 60 * 5; // how often to collect stats +const statsMinSamplesToTurnOn = 5; +const statsMinAccuracyToTurnOn = 0.3; +const statsToggleOffThreshold = 0.5; // if latency is less than `threshold * this`, turn off + +/** + * Codes that should be omitted from sending to the prediction engine and + * insted omitted directly: + * - cursor hide/show + * - mode set/reset + */ +const PREDICTION_OMIT_RE = /^(\x1b\[\??25[hl])+/; + +const enum CursorMoveDirection { + Back = 'D', + Forwards = 'C', +} + +const setCursorPos = (x: number, y: number) => `${CSI}${y + 1};${x + 1}H`; +const setCursorCoordinate = (buffer: IBuffer, c: ICoordinate) => setCursorPos(c.x, c.y + (c.baseY - buffer.baseY)); + +interface ICoordinate { + x: number; + y: number; + baseY: number; +} + +const getCellAtCoordinate = (b: IBuffer, c: ICoordinate) => b.getLine(c.y + c.baseY)?.getCell(c.x); + +const moveToWordBoundary = (b: IBuffer, cursor: ICoordinate, direction: -1 | 1) => { + let ateLeadingWhitespace = false; + if (direction < 0) { + cursor.x--; + } + + while (cursor.x >= 0) { + const cell = getCellAtCoordinate(b, cursor); + if (!cell?.getCode()) { + return; + } + + const chars = cell.getChars(); + if (NOT_WORD_RE.test(chars)) { + if (ateLeadingWhitespace) { + break; + } + } else { + ateLeadingWhitespace = true; + } + + cursor.x += direction; + } + + if (direction < 0) { + cursor.x++; // we want to place the cursor after the whitespace starting the word + } + + cursor.x = Math.max(0, cursor.x); +}; + +const enum MatchResult { + /** matched successfully */ + Success, + /** failed to match */ + Failure, + /** buffer data, it might match in the future one more data comes in */ + Buffer, +} + +export interface IPrediction { + /** + * Returns a sequence to apply the prediction. + * @param buffer to write to + * @param cursor position to write the data. Should advance the cursor. + * @returns a string to be written to the user terminal, or optionally a + * string for the user terminal and real pty. + */ + apply(buffer: IBuffer, cursor: ICoordinate): string; + + /** + * Returns a sequence to roll back a previous `apply()` call. If + * `rollForwards` is not given, then this is also called if a prediction + * is correct before show the user's data. + */ + rollback(buffer: IBuffer): string; + + /** + * If available, this will be called when the prediction is correct. + */ + rollForwards?(buffer: IBuffer, withInput: string): string; + + /** + * Returns whether the given input is one expected by this prediction. + */ + matches(input: StringReader): MatchResult; +} + +class StringReader { + public index = 0; + + public get remaining() { + return this.input.length - this.index; + } + + public get eof() { + return this.index === this.input.length; + } + + public get rest() { + return this.input.slice(this.index); + } + + constructor(private readonly input: string) { } + + /** + * Advances the reader and returns the character if it matches. + */ + public eatChar(char: string) { + if (this.input[this.index] !== char) { + return; + } + + this.index++; + return char; + } + + /** + * Advances the reader and returns the string if it matches. + */ + public eatStr(substr: string) { + if (this.input.slice(this.index, substr.length) !== substr) { + return; + } + + this.index += substr.length; + return substr; + } + + /** + * Matches and eats the substring character-by-character. If EOF is reached + * before the substring is consumed, it will buffer. Index is not moved + * if it's not a match. + */ + public eatGradually(substr: string): MatchResult { + let prevIndex = this.index; + for (let i = 0; i < substr.length; i++) { + if (i > 0 && this.eof) { + return MatchResult.Buffer; + } + + if (!this.eatChar(substr[i])) { + this.index = prevIndex; + return MatchResult.Failure; + } + } + + return MatchResult.Success; + } + + /** + * Advances the reader and returns the regex if it matches. + */ + public eatRe(re: RegExp) { + const match = re.exec(this.input.slice(this.index)); + if (!match) { + return; + } + + this.index += match[0].length; + return match; + } + + /** + * Advances the reader and returns the character if the code matches. + */ + public eatCharCode(min = 0, max = min + 1) { + const code = this.input.charCodeAt(this.index); + if (code < min || code >= max) { + return undefined; + } + + this.index++; + return code; + } +} + +/** + * Preidction which never tests true. Will always discard predictions made + * after it. + */ +class HardBoundary implements IPrediction { + public apply() { + return ''; + } + + public rollback() { + return ''; + } + + public matches() { + return MatchResult.Failure; + } +} + +/** + * Wraps another prediction. Does not apply the prediction, but will pass + * through its `matches` request. + */ +class TentativeBoundary implements IPrediction { + constructor(private readonly inner: IPrediction) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + this.inner.apply(buffer, cursor); + return ''; + } + + public rollback() { + return ''; + } + + public matches(input: StringReader) { + return this.inner.matches(input); + } +} + +/** + * Prediction for a single alphanumeric character. + */ +class CharacterPrediction implements IPrediction { + protected appliedAt?: ICoordinate & { + oldAttributes: string; + oldChar: string; + }; + + constructor(private readonly style: string, private readonly char: string) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + const cell = getCellAtCoordinate(buffer, cursor); + this.appliedAt = cell + ? { ...cursor, oldAttributes: getBufferCellAttributes(cell), oldChar: cell.getChars() } + : { ...cursor, oldAttributes: '', oldChar: '' }; + + cursor.x++; + return this.style + this.char + this.appliedAt.oldAttributes; + } + + public rollback(buffer: IBuffer) { + if (!this.appliedAt) { + return ''; // not applied + } + + const a = this.appliedAt; + this.appliedAt = undefined; + return setCursorCoordinate(buffer, a) + (a.oldChar ? `${a.oldAttributes}${a.oldChar}${setCursorCoordinate(buffer, a)}` : DELETE_CHAR); + } + + public matches(input: StringReader) { + let startIndex = input.index; + + // remove any styling CSI before checking the char + while (input.eatRe(CSI_STYLE_RE)) { } + + if (input.eof) { + return MatchResult.Buffer; + } + + if (input.eatChar(this.char)) { + return MatchResult.Success; + } + + input.index = startIndex; + return MatchResult.Failure; + } +} + +class BackspacePrediction extends CharacterPrediction { + constructor() { + super('', '\b'); + } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + const cell = getCellAtCoordinate(buffer, cursor); + this.appliedAt = cell + ? { ...cursor, oldAttributes: getBufferCellAttributes(cell), oldChar: cell.getChars() } + : { ...cursor, oldAttributes: '', oldChar: '' }; + + cursor.x--; + return setCursorCoordinate(buffer, cursor) + DELETE_CHAR; + } + + public rollForwards() { + return ''; + } + + public matches(input: StringReader) { + const isEOL = this.appliedAt?.oldChar === ''; + if (isEOL) { + const r1 = input.eatGradually(`\b${CSI}K`); + if (r1 !== MatchResult.Failure) { + return r1; + } + + const r2 = input.eatGradually(`\b \b`); + if (r2 !== MatchResult.Failure) { + return r2; + } + } + + return MatchResult.Failure; + } +} + +class NewlinePrediction implements IPrediction { + protected prevPosition?: ICoordinate; + + public apply(_: IBuffer, cursor: ICoordinate) { + this.prevPosition = { ...cursor }; + cursor.x = 0; + cursor.y++; + return '\r\n'; + } + + public rollback(buffer: IBuffer) { + if (!this.prevPosition) { + return ''; // not applied + } + + const p = this.prevPosition; + this.prevPosition = undefined; + return setCursorCoordinate(buffer, p) + DELETE_CHAR; + } + + public rollForwards() { + return ''; // does not need to rewrite + } + + public matches(input: StringReader) { + return input.eatGradually('\r\n'); + } +} + +class CursorMovePrediction implements IPrediction { + private applied?: { + rollForward: string; + rollBack: string; + amount: number; + }; + + constructor( + private readonly direction: CursorMoveDirection, + private readonly moveByWords: boolean, + private readonly amount: number, + ) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + let rollBack = setCursorCoordinate(buffer, cursor); + const currentCell = getCellAtCoordinate(buffer, cursor); + if (currentCell) { + rollBack += getBufferCellAttributes(currentCell); + } + + const { amount, direction, moveByWords } = this; + const delta = direction === CursorMoveDirection.Back ? -1 : 1; + const startX = cursor.x; + if (moveByWords) { + for (let i = 0; i < amount; i++) { + moveToWordBoundary(buffer, cursor, delta); + } + } else { + cursor.x += delta * amount; + } + + const rollForward = setCursorCoordinate(buffer, cursor); + this.applied = { amount: Math.abs(cursor.x - startX), rollBack, rollForward }; + return this.applied.rollForward; + } + + public rollback() { + return this.applied?.rollBack ?? ''; + } + + public rollForwards() { + return ''; // does not need to rewrite + } + + public matches(input: StringReader) { + if (!this.applied) { + return MatchResult.Failure; + } + + const direction = this.direction; + const { amount, rollForward } = this.applied; + + if (amount === 1) { + // arg can be omitted to move one character + const r = input.eatGradually(`${CSI}${direction}`); + if (r !== MatchResult.Failure) { + return r; + } + + // \b is the equivalent to moving one character back + const r2 = input.eatGradually(`\b`); + if (r2 !== MatchResult.Failure) { + return r2; + } + } + + // check if the cursor position is set absolutely + if (rollForward) { + const r = input.eatGradually(rollForward); + if (r !== MatchResult.Failure) { + return r; + } + } + + // check for a relative move in the direction + return input.eatGradually(`${CSI}${amount}${direction}`); + } +} + +export class PredictionStats extends Disposable { + private readonly stats: [latency: number, correct: boolean][] = []; + private index = 0; + private readonly addedAtTime = new WeakMap(); + private readonly changeEmitter = new Emitter(); + public readonly onChange = this.changeEmitter.event; + + /** + * Gets the percent (0-1) of predictions that were accurate. + */ + public get accuracy() { + let correctCount = 0; + for (const [, correct] of this.stats) { + if (correct) { + correctCount++; + } + } + + return correctCount / (this.stats.length || 1); + } + + /** + * Gets the number of recorded stats. + */ + public get sampleSize() { + return this.stats.length; + } + + /** + * Gets latency stats of successful predictions. + */ + public get latency() { + const latencies = this.stats.filter(([, correct]) => correct).map(([s]) => s).sort(); + + return { + count: latencies.length, + min: latencies[0], + median: latencies[Math.floor(latencies.length / 2)], + max: latencies[latencies.length - 1], + }; + } + + constructor(timeline: PredictionTimeline) { + super(); + this._register(timeline.onPredictionAdded(p => this.addedAtTime.set(p, Date.now()))); + this._register(timeline.onPredictionSucceeded(this.pushStat.bind(this, true))); + this._register(timeline.onPredictionFailed(this.pushStat.bind(this, false))); + } + + private pushStat(correct: boolean, prediction: IPrediction) { + const started = this.addedAtTime.get(prediction)!; + this.stats[this.index] = [Date.now() - started, correct]; + this.index = (this.index + 1) % statsBufferSize; + this.changeEmitter.fire(); + } +} + +export class PredictionTimeline { + /** + * Expected queue of events. Only predictions for the lowest are + * written into the terminal. + */ + private expected: ({ gen: number; p: IPrediction })[] = []; + + /** + * Current prediction generation. + */ + private currentGen = 0; + + /** + * Cursor position -- kept outside the buffer since it can be ahead if + * typing swiftly. + */ + private cursor: ICoordinate | undefined; + + /** + * Previously sent data that was buffered and should be prepended to the + * next input. + */ + private inputBuffer?: string; + + /** + * Whether predictions are echoed to the terminal. If false, predictions + * will still be computed internally for latency metrics, but input will + * never be adjusted. + */ + private showPredictions = false; + + private readonly addedEmitter = new Emitter(); + public readonly onPredictionAdded = this.addedEmitter.event; + private readonly failedEmitter = new Emitter(); + public readonly onPredictionFailed = this.failedEmitter.event; + private readonly succeededEmitter = new Emitter(); + public readonly onPredictionSucceeded = this.succeededEmitter.event; + + constructor(public readonly terminal: Terminal) { } + + public setShowPredictions(show: boolean) { + if (show === this.showPredictions) { + return; + } + + // console.log('set predictions:', show); + this.showPredictions = show; + + const buffer = this.getActiveBuffer(); + if (!buffer) { + return; + } + + const toApply = this.expected.filter(({ gen }) => gen === this.expected[0].gen).map(({ p }) => p); + if (show) { + this.cursor = undefined; + this.terminal.write(toApply.map(p => p.apply(buffer, this.getCursor(buffer))).join('')); + } else { + this.terminal.write(toApply.reverse().map(p => p.rollback(buffer)).join('')); + } + } + + /** + * Should be called when input is incoming to the temrinal. + */ + public beforeServerInput(input: string): string { + if (this.inputBuffer) { + input = this.inputBuffer + input; + this.inputBuffer = undefined; + } + + if (!this.expected.length) { + this.cursor = undefined; + return input; + } + + const buffer = this.getActiveBuffer(); + if (!buffer) { + this.cursor = undefined; + return input; + } + + let output = ''; + + const reader = new StringReader(input); + const startingGen = this.expected[0].gen; + const emitPredictionOmitted = () => { + const omit = reader.eatRe(PREDICTION_OMIT_RE); + if (omit) { + output += omit[0]; + } + }; + + ReadLoop: while (this.expected.length && reader.remaining > 0) { + emitPredictionOmitted(); + + const prediction = this.expected[0].p; + let beforeTestReaderIndex = reader.index; + switch (prediction.matches(reader)) { + case MatchResult.Success: + // if the input character matches what the next prediction expected, undo + // the prediction and write the real character out. + const eaten = input.slice(beforeTestReaderIndex, reader.index); + output += prediction.rollForwards?.(buffer, eaten) + ?? (prediction.rollback(buffer) + input.slice(beforeTestReaderIndex, reader.index)); + this.succeededEmitter.fire(prediction); + this.expected.shift(); + break; + case MatchResult.Buffer: + // on a buffer, store the remaining data and completely read data + // to be output as normal. + this.inputBuffer = input.slice(beforeTestReaderIndex); + reader.index = input.length; + break ReadLoop; + case MatchResult.Failure: + // on a failure, roll back all remaining items in this generation + // and clear predictions, since they are no longer valid + output += this.expected.filter(p => p.gen === startingGen) + .map(({ p }) => p.rollback(buffer)) + .reverse() + .join(''); + this.expected = []; + this.cursor = undefined; + this.failedEmitter.fire(prediction); + break ReadLoop; + } + } + + emitPredictionOmitted(); + + // Extra data (like the result of running a command) should cause us to + // reset the cursor + if (!reader.eof) { + output += reader.rest; + this.expected = []; + this.cursor = undefined; + } + + // If we passed a generation boundary, apply the current generation's predictions + if (this.expected.length && startingGen !== this.expected[0].gen) { + for (const { p, gen } of this.expected) { + if (gen !== this.expected[0].gen) { + break; + } + + output += p.apply(buffer, this.getCursor(buffer)); + } + } + + if (!this.showPredictions) { + return input; + } + + if (output.length === 0 || output === input) { + return output; + } + + if (this.cursor) { + output += setCursorCoordinate(buffer, this.cursor); + } + + // prevent cursor flickering while typing + output = HIDE_CURSOR + output + SHOW_CURSOR; + + return output; + } + + /** + * Appends a typeahead prediction. + */ + public addPrediction(buffer: IBuffer, prediction: IPrediction) { + this.expected.push({ gen: this.currentGen, p: prediction }); + this.addedEmitter.fire(prediction); + + if (this.currentGen === this.expected[0].gen) { + const text = prediction.apply(buffer, this.getCursor(buffer)); + if (this.showPredictions) { + this.terminal.write(text); + } + } + } + + /** + * Appends a prediction followed by a boundary. The predictions applied + * after this one will only be displayed after the give prediction matches + * pty output/ + */ + public addBoundary(buffer: IBuffer, prediction: IPrediction) { + this.addPrediction(buffer, prediction); + this.currentGen++; + } + + public getCursor(buffer: IBuffer) { + if (!this.cursor) { + this.cursor = { baseY: buffer.baseY, y: buffer.cursorY, x: buffer.cursorX }; + } + + return this.cursor; + } + + private getActiveBuffer() { + const buffer = this.terminal.buffer.active; + return buffer.type === 'normal' ? buffer : undefined; + } +} +/** + * Gets the escape sequence to restore state/appearence in the cell. + */ +const getBufferCellAttributes = (cell: IBufferCell) => cell.isAttributeDefault() + ? `${CSI}0m` + : [ + cell.isBold() && `${CSI}1m`, + cell.isDim() && `${CSI}2m`, + cell.isItalic() && `${CSI}3m`, + cell.isUnderline() && `${CSI}4m`, + cell.isBlink() && `${CSI}5m`, + cell.isInverse() && `${CSI}7m`, + cell.isInvisible() && `${CSI}8m`, + + cell.isFgRGB() && `${CSI}38;2;${cell.getFgColor() >>> 24};${(cell.getFgColor() >>> 16) & 0xFF};${cell.getFgColor() & 0xFF}m`, + cell.isFgPalette() && `${CSI}38;5;${cell.getFgColor()}m`, + cell.isFgDefault() && `${CSI}39m`, + + cell.isBgRGB() && `${CSI}48;2;${cell.getBgColor() >>> 24};${(cell.getBgColor() >>> 16) & 0xFF};${cell.getBgColor() & 0xFF}m`, + cell.isBgPalette() && `${CSI}48;5;${cell.getBgColor()}m`, + cell.isBgDefault() && `${CSI}49m`, + ].filter(seq => !!seq).join(''); + +const parseTypeheadStyle = (style: string | number) => { + if (typeof style === 'number') { + return `${CSI}${style}m`; + } + + const { r, g, b } = Color.fromHex(style).rgba; + return `${CSI}32;${r};${g};${b}m`; +}; + +export class TypeAheadAddon extends Disposable implements ITerminalAddon { + private typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle); + private typeaheadThreshold = this.config.config.typeaheadThreshold; + private lastRow?: { y: number; startingX: number }; + private timeline?: PredictionTimeline; + public stats?: PredictionStats; + + constructor( + private readonly processManager: ITerminalProcessManager, + private readonly config: TerminalConfigHelper, + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { + super(); + } + + public activate(terminal: Terminal): void { + const timeline = this.timeline = new PredictionTimeline(terminal); + const stats = this.stats = this._register(new PredictionStats(this.timeline)); + + timeline.setShowPredictions(this.typeaheadThreshold === 0); + this._register(terminal.onData(e => this.onUserData(e))); + this._register(this.config.onConfigChanged(() => { + this.typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle); + this.typeaheadThreshold = this.config.config.typeaheadThreshold; + this.reevaluatePredictorState(stats, timeline); + })); + this._register(this.processManager.onBeforeProcessData(e => this.onBeforeProcessData(e))); + + let nextStatsSend: any; + this._register(stats.onChange(() => { + if (!nextStatsSend) { + nextStatsSend = setTimeout(() => { + this.sendLatencyStats(stats); + nextStatsSend = undefined; + }, statsSendTelemetryEvery); + } + + this.reevaluatePredictorState(stats, timeline); + })); + } + + /** + * Note on debounce: + * + * We want to toggle the state only when the user has a pause in their + * typing. Otherwise, we could turn this on when the PTY sent data but the + * terminal cursor is not updated, causes issues. + */ + @debounce(100) + private reevaluatePredictorState(stats: PredictionStats, timeline: PredictionTimeline) { + if (this.typeaheadThreshold < 0) { + timeline.setShowPredictions(false); + } else if (this.typeaheadThreshold === 0) { + timeline.setShowPredictions(true); + } else if (stats.sampleSize > statsMinSamplesToTurnOn && stats.accuracy > statsMinAccuracyToTurnOn) { + const latency = stats.latency.median; + if (latency >= this.typeaheadThreshold) { + timeline.setShowPredictions(true); + } else if (latency < this.typeaheadThreshold / statsToggleOffThreshold) { + timeline.setShowPredictions(false); + } + } + } + + private sendLatencyStats(stats: PredictionStats) { + /* __GDPR__ + "terminalLatencyStats" : { + "min" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "max" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "median" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "predictionAccuracy" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('terminalLatencyStats', { + ...stats.latency, + predictionAccuracy: stats.accuracy, + }); + } + + private onUserData(data: string): void { + if (this.timeline?.terminal.buffer.active.type !== 'normal') { + return; + } + + // console.log('user data:', JSON.stringify(data)); + + const terminal = this.timeline.terminal; + const buffer = terminal.buffer.active; + + // the following code guards the terminal prompt to avoid being able to + // arrow or backspace-into the prompt. Record the lowest X value at which + // the user gave input, and mark all additions before that as tentative. + const actualY = buffer.baseY + buffer.cursorY; + if (actualY !== this.lastRow?.y) { + this.lastRow = { y: actualY, startingX: buffer.cursorX }; + } else { + this.lastRow.startingX = Math.min(this.lastRow.startingX, buffer.cursorX); + } + + const addLeftNavigating = (p: IPrediction) => + this.timeline!.getCursor(buffer).x <= this.lastRow!.startingX + ? this.timeline!.addBoundary(buffer, new TentativeBoundary(p)) + : this.timeline!.addPrediction(buffer, p); + + /** @see https://github.com/xtermjs/xterm.js/blob/1913e9512c048e3cf56bb5f5df51bfff6899c184/src/common/input/Keyboard.ts */ + const reader = new StringReader(data); + while (reader.remaining > 0) { + if (reader.eatCharCode(127)) { // backspace + addLeftNavigating(new BackspacePrediction()); + continue; + } + + if (reader.eatCharCode(32, 126)) { // alphanum + const char = data[reader.index - 1]; + this.timeline.addPrediction(buffer, new CharacterPrediction(this.typeheadStyle, char)); + if (this.timeline.getCursor(buffer).x === terminal.cols) { + this.timeline.addBoundary(buffer, new NewlinePrediction()); + } + continue; + } + + const cursorMv = reader.eatRe(CSI_MOVE_RE); + if (cursorMv) { + const direction = cursorMv[3] as CursorMoveDirection; + const p = new CursorMovePrediction(direction, !!cursorMv[2], Number(cursorMv[1]) || 1); + if (direction === CursorMoveDirection.Back) { + addLeftNavigating(p); + } else { + this.timeline.addPrediction(buffer, p); + } + continue; + } + + if (reader.eatStr(`${ESC}f`)) { + this.timeline.addPrediction(buffer, new CursorMovePrediction(CursorMoveDirection.Forwards, true, 1)); + continue; + } + + if (reader.eatStr(`${ESC}b`)) { + addLeftNavigating(new CursorMovePrediction(CursorMoveDirection.Back, true, 1)); + continue; + } + + if (reader.eatChar('\r') && buffer.cursorY < terminal.rows - 1) { + this.timeline.addPrediction(buffer, new NewlinePrediction()); + continue; + } + + // something else + this.timeline.addBoundary(buffer, new HardBoundary()); + break; + } + } + + private onBeforeProcessData(event: IBeforeProcessDataEvent): void { + if (!this.timeline) { + return; + } + + // console.log('incoming data:', JSON.stringify(event.data)); + event.data = this.timeline.beforeServerInput(event.data); + // console.log('emitted data:', JSON.stringify(event.data)); + + // If there's something that looks like a password prompt, omit giving + // input. This is approximate since there's no TTY "password here" code, + // but should be enough to cover common cases like sudo + if (PASSWORD_INPUT_RE.test(event.data)) { + const terminal = this.timeline.terminal; + this.timeline.addBoundary(terminal.buffer.active, new HardBoundary()); + } + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index ae87e42d990..8d9a84d5116 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -41,6 +41,7 @@ export class TerminalViewPane extends ViewPane { private _terminalContainer: HTMLElement | undefined; private _findWidget: TerminalFindWidget | undefined; private _splitTerminalAction: IAction | undefined; + private _terminalsInitialized = false; private _bodyDimensions: { width: number, height: number } = { width: 0, height: 0 }; constructor( @@ -109,14 +110,21 @@ export class TerminalViewPane extends ViewPane { this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { - const hadTerminals = this._terminalService.terminalInstances.length > 0; - if (!hadTerminals) { - this._terminalService.createTerminal(); + const hadTerminals = !!this._terminalService.terminalTabs.length; + if (this._terminalsInitialized) { + if (!hadTerminals) { + this._terminalService.createTerminal(); + } + } else { + this._terminalsInitialized = true; + this._terminalService.initializeTerminals(); } + this._updateTheme(); if (hadTerminals) { this._terminalService.getActiveTab()?.setVisible(visible); } else { + // TODO@Tyriar - this call seems unnecessary this.layoutBody(this._bodyDimensions.height, this._bodyDimensions.width); } } else { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index c559ff7b8db..3ae0210352d 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -26,6 +26,8 @@ export interface XTermCore { }; _onIntersectionChange: any; }; + + writeSync(data: string | Uint8Array): void; } export interface IEventEmitter { diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index c84ca3c0819..d904051e706 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -67,9 +67,11 @@ export interface ICreateTerminalProcessArguments { resolvedVariables: { [name: string]: string; }; envVariableCollections: ITerminalEnvironmentVariableCollections; shellLaunchConfig: IShellLaunchConfigDto; + workspaceId: string; workspaceFolders: IWorkspaceFolderData[]; activeWorkspaceFolder: IWorkspaceFolderData | null; activeFileResource: UriComponents | undefined; + shouldPersistTerminal: boolean; cols: number; rows: number; isWorkspaceShellAllowed: boolean; @@ -116,6 +118,25 @@ export interface ISendCommandResultToTerminalProcessArguments { payload: any; } +export interface IOrphanQuestionReplyArgs { + id: number; +} + +export interface IListTerminalsArgs { + workspaceId: string; +} + +export interface IRemoteTerminalDescriptionDto { + id: number; + pid: number; + title: string; + cwd: string; +} + +export interface ITriggerTerminalDataReplayArguments { + id: number; +} + export interface IRemoteTerminalProcessReadyEvent { type: 'ready'; pid: number; @@ -126,11 +147,16 @@ export interface IRemoteTerminalProcessTitleChangedEvent { title: string; } export interface IRemoteTerminalProcessDataEvent { - type: 'data' + type: 'data'; data: string; } +export interface ReplayEntry { cols: number; rows: number; data: string; } +export interface IRemoteTerminalProcessReplayEvent { + type: 'replay'; + events: ReplayEntry[]; +} export interface IRemoteTerminalProcessExitEvent { - type: 'exit' + type: 'exit'; exitCode: number | undefined; } export interface IRemoteTerminalProcessExecCommandEvent { @@ -139,12 +165,17 @@ export interface IRemoteTerminalProcessExecCommandEvent { commandId: string; commandArgs: any[]; } +export interface IRemoteTerminalProcessOrphanQuestionEvent { + type: 'orphan?'; +} export type IRemoteTerminalProcessEvent = ( IRemoteTerminalProcessReadyEvent | IRemoteTerminalProcessTitleChangedEvent | IRemoteTerminalProcessDataEvent + | IRemoteTerminalProcessReplayEvent | IRemoteTerminalProcessExitEvent | IRemoteTerminalProcessExecCommandEvent + | IRemoteTerminalProcessOrphanQuestionEvent ); export interface IOnTerminalProcessEventArguments { @@ -166,7 +197,7 @@ export class RemoteTerminalChannelClient { ) { } - private _readSingleTemrinalConfiguration(key: string): ISingleTerminalConfiguration { + private _readSingleTerminalConfiguration(key: string): ISingleTerminalConfiguration { const result = this._configurationService.inspect(key); return { userValue: result.userValue, @@ -175,21 +206,21 @@ export class RemoteTerminalChannelClient { }; } - public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { const terminalConfig = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); const configuration: ICompleteTerminalConfiguration = { - 'terminal.integrated.automationShell.windows': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.windows'), - 'terminal.integrated.automationShell.osx': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.osx'), - 'terminal.integrated.automationShell.linux': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.linux'), - 'terminal.integrated.shell.windows': this._readSingleTemrinalConfiguration('terminal.integrated.shell.windows'), - 'terminal.integrated.shell.osx': this._readSingleTemrinalConfiguration('terminal.integrated.shell.osx'), - 'terminal.integrated.shell.linux': this._readSingleTemrinalConfiguration('terminal.integrated.shell.linux'), - 'terminal.integrated.shellArgs.windows': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.windows'), - 'terminal.integrated.shellArgs.osx': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.osx'), - 'terminal.integrated.shellArgs.linux': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.linux'), - 'terminal.integrated.env.windows': this._readSingleTemrinalConfiguration('terminal.integrated.env.windows'), - 'terminal.integrated.env.osx': this._readSingleTemrinalConfiguration('terminal.integrated.env.osx'), - 'terminal.integrated.env.linux': this._readSingleTemrinalConfiguration('terminal.integrated.env.linux'), + 'terminal.integrated.automationShell.windows': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.windows'), + 'terminal.integrated.automationShell.osx': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.osx'), + 'terminal.integrated.automationShell.linux': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.linux'), + 'terminal.integrated.shell.windows': this._readSingleTerminalConfiguration('terminal.integrated.shell.windows'), + 'terminal.integrated.shell.osx': this._readSingleTerminalConfiguration('terminal.integrated.shell.osx'), + 'terminal.integrated.shell.linux': this._readSingleTerminalConfiguration('terminal.integrated.shell.linux'), + 'terminal.integrated.shellArgs.windows': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.windows'), + 'terminal.integrated.shellArgs.osx': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.osx'), + 'terminal.integrated.shellArgs.linux': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.linux'), + 'terminal.integrated.env.windows': this._readSingleTerminalConfiguration('terminal.integrated.env.windows'), + 'terminal.integrated.env.osx': this._readSingleTerminalConfiguration('terminal.integrated.env.osx'), + 'terminal.integrated.env.linux': this._readSingleTerminalConfiguration('terminal.integrated.env.linux'), 'terminal.integrated.inheritEnv': terminalConfig.inheritEnv, 'terminal.integrated.cwd': terminalConfig.cwd, 'terminal.integrated.detectLocale': terminalConfig.detectLocale, @@ -224,7 +255,8 @@ export class RemoteTerminalChannelClient { const resolverResult = await this._remoteAuthorityResolverService.resolveAuthority(this._remoteAuthority); const resolverEnv = resolverResult.options && resolverResult.options.extensionHostEnv; - const workspaceFolders = this._workspaceContextService.getWorkspace().folders; + const workspace = this._workspaceContextService.getWorkspace(); + const workspaceFolders = workspace.folders; const activeWorkspaceFolder = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null; const activeFileResource = EditorResourceAccessor.getOriginalUri(this._editorService.activeEditor, { @@ -237,9 +269,11 @@ export class RemoteTerminalChannelClient { resolvedVariables, envVariableCollections, shellLaunchConfig, + workspaceId: workspace.id, workspaceFolders, activeWorkspaceFolder, activeFileResource, + shouldPersistTerminal, cols, rows, isWorkspaceShellAllowed, @@ -306,4 +340,19 @@ export class RemoteTerminalChannelClient { }; return this._channel.call('$sendCommandResultToTerminalProcess', args); } + + public orphanQuestionReply(id: number): Promise { + const args: IOrphanQuestionReplyArgs = { + id + }; + return this._channel.call('$orphanQuestionReply', args); + } + + public listTerminals(): Promise { + const workspace = this._workspaceContextService.getWorkspace(); + const args: IListTerminalsArgs = { + workspaceId: workspace.id + }; + return this._channel.call('$listTerminals', args); + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 1af8a10e218..ad24b09b0df 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -135,6 +135,8 @@ export interface ITerminalConfiguration { enableFileLinks: boolean; unicodeVersion: '6' | '11'; experimentalLinkProvider: boolean; + typeaheadThreshold: number; + typeaheadStyle: number | string; } export interface ITerminalConfigHelper { @@ -163,6 +165,13 @@ export interface ITerminalEnvironment { [key: string]: string | null; } +export interface IRemoteTerminalAttachTarget { + id: number; + pid: number; + title: string; + cwd: string; +} + export interface IShellLaunchConfig { /** * The name of the terminal, if this is not set the name of the process will be used. @@ -215,6 +224,11 @@ export interface IShellLaunchConfig { */ isExtensionTerminal?: boolean; + /** + * This is a terminal that attaches to an already running remote terminal. + */ + remoteAttach?: { id: number; pid: number; title: string; cwd: string; }; + /** * Whether the terminal process environment should be exactly as provided in * `TerminalOptions.env`. When this is false (default), the environment will be based on the @@ -232,6 +246,12 @@ export interface IShellLaunchConfig { * as normal. */ hideFromUser?: boolean; + + /** + * Whether this terminal is not a terminal that the user directly created and uses, but rather + * a terminal used to drive some VS Code feature. + */ + isFeatureTerminal?: boolean; } /** @@ -266,6 +286,13 @@ export interface ITerminalDimensions { readonly rows: number; } +export interface ITerminalDimensionsOverride extends ITerminalDimensions { + /** + * indicate that xterm must receive these exact dimensions, even if they overflow the ui! + */ + forceExactSize?: boolean; +} + export interface ICommandTracker { scrollToPreviousCommand(): void; scrollToNextCommand(): void; @@ -289,6 +316,11 @@ export interface IBeforeProcessDataEvent { data: string; } +export interface IProcessDataEvent { + data: string; + sync: boolean; +} + export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; @@ -300,10 +332,10 @@ export interface ITerminalProcessManager extends IDisposable { readonly onProcessReady: Event; readonly onBeforeProcessData: Event; - readonly onProcessData: Event; + readonly onProcessData: Event; readonly onProcessTitle: Event; readonly onProcessExit: Event; - readonly onProcessOverrideDimensions: Event; + readonly onProcessOverrideDimensions: Event; readonly onProcessResolvedShellLaunchConfig: Event; readonly onEnvironmentVariableInfoChanged: Event; @@ -414,11 +446,11 @@ export interface ITerminalLaunchError { * child_process.ChildProcess node.js interface. */ export interface ITerminalChildProcess { - onProcessData: Event; + onProcessData: Event; onProcessExit: Event; onProcessReady: Event<{ pid: number, cwd: string }>; onProcessTitleChanged: Event; - onProcessOverrideDimensions?: Event; + onProcessOverrideDimensions?: Event; onProcessResolvedShellLaunchConfig?: Event; /** diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 985fde8246b..87c812cdcf0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -351,6 +351,36 @@ export const terminalConfiguration: IConfigurationNode = { description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."), type: 'boolean', default: true + }, + 'terminal.integrated.typeaheadThreshold': { + description: localize('terminal.integrated.typeaheadThreshold', "Experimental: length of time, in milliseconds, where typeahead will activate. If '0', typeahead will always be on, and if '-1' it will be disabled."), + type: 'integer', + minimum: -1, + default: -1, + }, + 'terminal.integrated.typeaheadStyle': { + description: localize('terminal.integrated.typeaheadStyle', "Experimental: terminal style of typeahead text, either a font style or an RGB color."), + default: 2, + oneOf: [ + { + type: 'integer', + default: 2, + enum: [0, 1, 2, 3, 4, 7], + enumDescriptions: [ + localize('terminal.integrated.typeaheadStyle.0', 'Normal'), + localize('terminal.integrated.typeaheadStyle.1', 'Bold'), + localize('terminal.integrated.typeaheadStyle.2', 'Dim'), + localize('terminal.integrated.typeaheadStyle.3', 'Italic'), + localize('terminal.integrated.typeaheadStyle.4', 'Underlined'), + localize('terminal.integrated.typeaheadStyle.7', 'Inverted'), + ] + }, + { + type: 'string', + format: 'color-hex', + default: '#ff0000', + } + ] } } }; diff --git a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts index 82f3fe6f1d0..db8c7f57d9b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; interface TerminalDataBuffer extends IDisposable { data: string[]; @@ -23,19 +24,20 @@ export class TerminalDataBufferer implements IDisposable { } } - startBuffering(id: number, event: Event, throttleBy: number = 5): IDisposable { + startBuffering(id: number, event: Event, throttleBy: number = 5): IDisposable { let disposable: IDisposable; - disposable = event((e: string) => { + disposable = event((e: string | IProcessDataEvent) => { + const data = (typeof e === 'string' ? e : e.data); let buffer = this._terminalBufferMap.get(id); if (buffer) { - buffer.data.push(e); + buffer.data.push(data); return; } const timeoutId = setTimeout(() => this._flushBuffer(id), throttleBy); buffer = { - data: [e], + data: [data], timeoutId: timeoutId, dispose: () => { clearTimeout(timeoutId); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts index 263c15cb666..ca107b15e38 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts @@ -12,7 +12,7 @@ import { TerminalNativeContribution } from 'vs/workbench/contrib/terminal/electr import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; // This file contains additional desktop-only contributions on top of those in browser/ diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts new file mode 100644 index 00000000000..6fdd2cbbc78 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { 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 { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +const CSI = `\x1b[`; + +suite('Workbench - Terminal Typeahead', () => { + suite('PredictionStats', () => { + let stats: PredictionStats; + const add = new Emitter(); + const succeed = new Emitter(); + const fail = new Emitter(); + + setup(() => { + stats = new PredictionStats({ + onPredictionAdded: add.event, + onPredictionSucceeded: succeed.event, + onPredictionFailed: fail.event, + } as any); + }); + + test('creates sane data', () => { + const stubs = createPredictionStubs(5); + const clock = useFakeTimers(); + try { + for (const s of stubs) { add.fire(s); } + + for (let i = 0; i < stubs.length; i++) { + clock.tick(100); + (i % 2 ? fail : succeed).fire(stubs[i]); + } + + assert.strictEqual(stats.accuracy, 3 / 5); + assert.strictEqual(stats.sampleSize, 5); + assert.deepStrictEqual(stats.latency, { + count: 3, + min: 100, + max: 500, + median: 300 + }); + } finally { + clock.restore(); + } + }); + + test('circular buffer', () => { + const bufferSize = 24; + const stubs = createPredictionStubs(bufferSize * 2); + + for (const s of stubs.slice(0, bufferSize)) { add.fire(s); succeed.fire(s); } + assert.strictEqual(stats.accuracy, 1); + + for (const s of stubs.slice(bufferSize, bufferSize * 3 / 2)) { add.fire(s); fail.fire(s); } + assert.strictEqual(stats.accuracy, 0.5); + + for (const s of stubs.slice(bufferSize * 3 / 2)) { add.fire(s); fail.fire(s); } + assert.strictEqual(stats.accuracy, 0); + }); + }); + + suite('timeline', () => { + const onBeforeProcessData = new Emitter(); + const onConfigChanged = new Emitter(); + let publicLog: SinonStub; + let config: ITerminalConfiguration; + let addon: TypeAheadAddon; + + + const predictedHelloo = [ + `${CSI}?25l`, // hide cursor + `${CSI}2;7H`, // move cursor cursor + `${CSI}X`, // delete character + 'o', // new character + `${CSI}2;8H`, // place cursor back at end of line + `${CSI}?25h`, // show cursor + ].join(''); + + const expectProcessed = (input: string, output: string) => { + const evt = { data: input }; + onBeforeProcessData.fire(evt); + assert.strictEqual(JSON.stringify(evt.data), JSON.stringify(output)); + }; + + setup(() => { + config = upcastPartial({ + typeaheadStyle: 3, + typeaheadThreshold: 0 + }); + publicLog = stub(); + addon = new TypeAheadAddon( + upcastPartial({ onBeforeProcessData: onBeforeProcessData.event }), + upcastPartial({ config, onConfigChanged: onConfigChanged.event }), + upcastPartial({ publicLog }) + ); + }); + + teardown(() => { + addon.dispose(); + }); + + test('predicts a single character', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('o'); + t.expectWritten(`${CSI}3mo`); + }); + + test('validates character prediction', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('o'); + expectProcessed('o', predictedHelloo); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('rolls back character prediction', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('o'); + + expectProcessed('q', [ + `${CSI}?25l`, // hide cursor + `${CSI}2;7H`, // move cursor cursor + `${CSI}X`, // delete character + 'q', // new character + `${CSI}?25h`, // show cursor + ].join('')); + assert.strictEqual(addon.stats?.accuracy, 0); + }); + + test('validates against and applies graphics mode on predicted', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('o'); + expectProcessed(`${CSI}4mo`, [ + `${CSI}?25l`, // hide cursor + `${CSI}2;7H`, // move cursor cursor + `${CSI}X`, // delete character + `${CSI}4m`, // PTY's style + 'o', // new character + `${CSI}2;8H`, // place cursor back at end of line + `${CSI}?25h`, // show cursor + ].join('')); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('ignores cursor hides or shows', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('o'); + expectProcessed(`${CSI}?25lo${CSI}?25h`, [ + `${CSI}?25l`, // hide cursor from PTY + `${CSI}?25l`, // hide cursor + `${CSI}2;7H`, // move cursor cursor + `${CSI}X`, // delete character + 'o', // new character + `${CSI}?25h`, // show cursor from PTY + `${CSI}2;8H`, // place cursor back at end of line + `${CSI}?25h`, // show cursor + ].join('')); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('matches backspace at EOL (bash style)', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('\x7F'); + expectProcessed(`\b${CSI}K`, `\b${CSI}K`); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('matches backspace at EOL (zsh style)', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('\x7F'); + expectProcessed('\b \b', '\b \b'); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('gradually matches backspace', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + t.onData('\x7F'); + expectProcessed('\b', ''); + expectProcessed(' \b', '\b \b'); + assert.strictEqual(addon.stats?.accuracy, 1); + }); + + test('waits for validation before deleting to left of cursor', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + + // initially should not backspace (until the server confirms it) + t.onData('\x7F'); + t.expectWritten(''); + expectProcessed('\b \b', '\b \b'); + t.cursor.x--; + + // enter input on the column... + t.onData('o'); + onBeforeProcessData.fire({ data: 'o' }); + t.cursor.x++; + t.clearWritten(); + + // now that the column is 'unlocked', we should be able to predict backspace on it + t.onData('\x7F'); + t.expectWritten(`${CSI}2;6H${CSI}X`); + }); + + test('avoids predicting password input', () => { + const t = createMockTerminal('hello|'); + addon.activate(t.terminal); + expectProcessed('Your password: ', 'Your password: '); + + t.onData('mellon\r\n'); + t.expectWritten(''); + expectProcessed('\r\n', '\r\n'); + + t.onData('o'); // back to normal mode + t.expectWritten(`${CSI}3mo`); + }); + }); +}); + +function upcastPartial(v: Partial): T { + return v as T; +} + +function createPredictionStubs(n: number) { + return new Array(n).fill(0).map(stubPrediction); +} + +function stubPrediction(): IPrediction { + return { + apply: () => '', + rollback: () => '', + matches: () => 0, + }; +} + +function createMockTerminal(...lines: string[]) { + const written: string[] = []; + const cursor = { y: 1, x: 1 }; + const onData = new Emitter(); + + for (let y = 0; y < lines.length; y++) { + const line = lines[y]; + if (line.includes('|')) { + cursor.y = y + 1; + cursor.x = line.indexOf('|') + 1; + lines[y] = line.replace('|', ''); + break; + } + } + + return { + written, + cursor, + expectWritten: (s: string) => { + assert.strictEqual(JSON.stringify(written.join('')), JSON.stringify(s)); + written.splice(0, written.length); + }, + clearWritten: () => written.splice(0, written.length), + onData: (s: string) => onData.fire(s), + terminal: { + cols: 80, + rows: 5, + onData: onData.event, + write(line: string) { + written.push(line); + }, + buffer: { + active: { + type: 'normal', + baseY: 0, + get cursorY() { return cursor.y; }, + get cursorX() { return cursor.x; }, + getLine(y: number) { + const s = lines[y - 1] || ''; + return { + length: s.length, + getCell: (x: number) => mockCell(s[x - 1] || ''), + }; + }, + } + } + } as unknown as Terminal + }; +} + +function mockCell(char: string) { + return new Proxy({}, { + get(_, prop) { + switch (prop) { + case 'getWidth': + return () => 1; + case 'getChars': + return () => char; + case 'getCode': + return () => char.charCodeAt(0) || 0; + default: + return String(prop).startsWith('is') ? (() => false) : (() => 0); + } + }, + }); +} diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index ac903c8b576..78ae88936df 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -1159,7 +1159,6 @@ class TimelinePaneCommands extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService ) { super(); @@ -1233,7 +1232,7 @@ class TimelinePaneCommands extends Disposable { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); menu.dispose(); scoped.dispose(); diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 8c3430fbd32..0cf1e65ecba 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -133,7 +133,19 @@ export class ReleaseNotesManager { return resolvedKeybindings[0].getLabel() || unassigned; }; + const kbCode = (match: string, binding: string) => { + const resolved = kb(match, binding); + return resolved ? `${resolved}` : resolved; + }; + + const kbstyleCode = (match: string, binding: string) => { + const resolved = kbstyle(match, binding); + return resolved ? `${resolved}` : resolved; + }; + return text + .replace(/`kb\(([a-z.\d\-]+)\)`/gi, kbCode) + .replace(/`kbstyle\(([^\)]+)\)`/gi, kbstyleCode) .replace(/kb\(([a-z.\d\-]+)\)/gi, kb) .replace(/kbstyle\(([^\)]+)\)/gi, kbstyle); }; diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index f842ef50965..d10d7d865a1 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -10,7 +10,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, CheckForVSCodeUpdateAction, CONTEXT_UPDATE_STATE, SwitchProductQualityContribution } from 'vs/workbench/contrib/update/browser/update'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import product from 'vs/platform/product/common/product'; import { StateType } from 'vs/platform/update/common/update'; diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts index 3e9d479633b..81ec10aea95 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -174,14 +174,44 @@ async function getRemotes(fileService: IFileService, textFileService: ITextFileS return [...set]; } -export async function readTrustedDomains(accessor: ServicesAccessor) { +export interface IStaticTrustedDomains { + readonly defaultTrustedDomains: string[]; + readonly trustedDomains: string[]; +} - const storageService = accessor.get(IStorageService); - const productService = accessor.get(IProductService); - const authenticationService = accessor.get(IAuthenticationService); +export interface ITrustedDomains extends IStaticTrustedDomains { + readonly userDomains: string[]; + readonly workspaceDomains: string[]; +} + +export async function readTrustedDomains(accessor: ServicesAccessor): Promise { + const { defaultTrustedDomains, trustedDomains } = readStaticTrustedDomains(accessor); + const [workspaceDomains, userDomains] = await Promise.all([readWorkspaceTrustedDomains(accessor), readAuthenticationTrustedDomains(accessor)]); + return { + workspaceDomains, + userDomains, + defaultTrustedDomains, + trustedDomains, + }; +} + +export async function readWorkspaceTrustedDomains(accessor: ServicesAccessor): Promise { const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); const workspaceContextService = accessor.get(IWorkspaceContextService); + return getRemotes(fileService, textFileService, workspaceContextService); +} + +export async function readAuthenticationTrustedDomains(accessor: ServicesAccessor): Promise { + const authenticationService = accessor.get(IAuthenticationService); + return authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 + ? [`https://github.com`] + : []; +} + +export function readStaticTrustedDomains(accessor: ServicesAccessor): IStaticTrustedDomains { + const storageService = accessor.get(IStorageService); + const productService = accessor.get(IProductService); const defaultTrustedDomains: string[] = productService.linkProtectionTrustedDomains ? [...productService.linkProtectionTrustedDomains] @@ -195,17 +225,8 @@ export async function readTrustedDomains(accessor: ServicesAccessor) { } } catch (err) { } - const userDomains = - authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 - ? [`https://github.com`] - : []; - - const workspaceDomains = await getRemotes(fileService, textFileService, workspaceContextService); - return { defaultTrustedDomains, trustedDomains, - userDomains, - workspaceDomains }; } diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 235931c0287..2c8428a6300 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -13,21 +13,25 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { - configureOpenerTrustedDomainsHandler, - readTrustedDomains -} from 'vs/workbench/contrib/url/browser/trustedDomains'; +import { configureOpenerTrustedDomainsHandler, readAuthenticationTrustedDomains, readStaticTrustedDomains, readWorkspaceTrustedDomains } from 'vs/workbench/contrib/url/browser/trustedDomains'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IdleValue } from 'vs/base/common/async'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; type TrustedDomainsDialogActionClassification = { action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; export class OpenerValidatorContributions implements IWorkbenchContribution { + + private _readWorkspaceTrustedDomainsResult: IdleValue>; + private _readAuthenticationTrustedDomainsResult: IdleValue>; + constructor( @IOpenerService private readonly _openerService: IOpenerService, @IStorageService private readonly _storageService: IStorageService, @@ -39,8 +43,26 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { @ITelemetryService private readonly _telemetryService: ITelemetryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + + this._readAuthenticationTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readAuthenticationTrustedDomains)); + this._authenticationService.onDidRegisterAuthenticationProvider(() => { + this._readAuthenticationTrustedDomainsResult?.dispose(); + this._readAuthenticationTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readAuthenticationTrustedDomains)); + }); + + this._readWorkspaceTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readWorkspaceTrustedDomains)); + this._workspaceContextService.onDidChangeWorkspaceFolders(() => { + this._readWorkspaceTrustedDomainsResult?.dispose(); + this._readWorkspaceTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readWorkspaceTrustedDomains)); + }); } async validateLink(resource: URI | string): Promise { @@ -54,7 +76,8 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { const { scheme, authority, path, query, fragment } = resource; const domainToOpen = `${scheme}://${authority}`; - const { defaultTrustedDomains, trustedDomains, userDomains, workspaceDomains } = await this._instantiationService.invokeFunction(readTrustedDomains); + const [workspaceDomains, userDomains] = await Promise.all([this._readWorkspaceTrustedDomainsResult.value, this._readAuthenticationTrustedDomainsResult.value]); + const { defaultTrustedDomains, trustedDomains, } = this._instantiationService.invokeFunction(readStaticTrustedDomains); const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains, ...userDomains, ...workspaceDomains]; if (isURLDomainTrusted(resource, allTrustedDomains)) { diff --git a/src/vs/workbench/contrib/url/browser/url.contribution.ts b/src/vs/workbench/contrib/url/browser/url.contribution.ts index d52dd4fa67f..949c9980357 100644 --- a/src/vs/workbench/contrib/url/browser/url.contribution.ts +++ b/src/vs/workbench/contrib/url/browser/url.contribution.ts @@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { IURLService } from 'vs/platform/url/common/url'; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 8701765c53c..51517c3e562 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -5,7 +5,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; import { IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index caeba8ce960..f41dbb1c2cd 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncResourceEnablementService, - getSyncResourceFromLocalPreview, IResourcePreview, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStore + getSyncResourceFromLocalPreview, IResourcePreview, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStore, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -110,6 +110,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IInstantiationService private readonly instantiationService: IInstantiationService, @IOutputService private readonly outputService: IOutputService, @IUserDataSyncAccountService readonly authTokenService: IUserDataSyncAccountService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @ITextModelService textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @@ -135,14 +136,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this._register(Event.any( Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500), - this.userDataAutoSyncService.onDidChangeEnablement, + this.userDataAutoSyncEnablementService.onDidChangeEnablement, this.userDataSyncWorkbenchService.onDidChangeAccountStatus )(() => { this.updateAccountBadge(); this.updateGlobalActivityBadge(); })); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); - this._register(userDataAutoSyncService.onDidChangeEnablement(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); + this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSynchronizerErrors(errors))); this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error))); @@ -152,7 +153,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); - this._register(Event.any(userDataSyncService.onDidChangeStatus, userDataAutoSyncService.onDidChangeEnablement)(() => this.turningOnSync = !userDataAutoSyncService.isEnabled() && userDataSyncService.status !== SyncStatus.Idle)); + this._register(Event.any(userDataSyncService.onDidChangeStatus, userDataAutoSyncEnablementService.onDidChangeEnablement)(() => this.turningOnSync = !userDataAutoSyncEnablementService.isEnabled() && userDataSyncService.status !== SyncStatus.Idle)); } } @@ -167,7 +168,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private readonly conflictsDisposables = new Map(); private onDidChangeConflicts(conflicts: [SyncResource, IResourcePreview[]][]) { - if (!this.userDataAutoSyncService.isEnabled()) { + if (!this.userDataAutoSyncEnablementService.isEnabled()) { return; } this.updateGlobalActivityBadge(); @@ -255,7 +256,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async acceptRemote(syncResource: SyncResource, conflicts: IResourcePreview[]) { try { for (const conflict of conflicts) { - await this.userDataSyncService.accept(syncResource, conflict.remoteResource, undefined, this.userDataAutoSyncService.isEnabled()); + await this.userDataSyncService.accept(syncResource, conflict.remoteResource, undefined, this.userDataAutoSyncEnablementService.isEnabled()); } } catch (e) { this.notificationService.error(e); @@ -265,7 +266,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async acceptLocal(syncResource: SyncResource, conflicts: IResourcePreview[]): Promise { try { for (const conflict of conflicts) { - await this.userDataSyncService.accept(syncResource, conflict.localResource, undefined, this.userDataAutoSyncService.isEnabled()); + await this.userDataSyncService.accept(syncResource, conflict.localResource, undefined, this.userDataAutoSyncEnablementService.isEnabled()); } } catch (e) { this.notificationService.error(e); @@ -401,7 +402,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.conflicts.length && this.userDataAutoSyncService.isEnabled()) { + if (this.userDataSyncService.conflicts.length && this.userDataAutoSyncEnablementService.isEnabled()) { badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, [, conflicts]) => { return result + conflicts.length; }, 0), () => localize('has conflicts', "{0}: Conflicts Detected", SYNC_TITLE)); } else if (this.turningOnSync) { badge = new ProgressBadge(() => localize('turning on syncing', "Turning on Settings Sync...")); @@ -419,7 +420,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let badge: IBadge | undefined = undefined; - if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataAutoSyncService.isEnabled() && this.userDataSyncWorkbenchService.accountStatus === AccountStatus.Unavailable) { + if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncWorkbenchService.accountStatus === AccountStatus.Unavailable) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync Settings")); } @@ -713,7 +714,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerActions(): void { - if (this.userDataAutoSyncService.canToggleEnablement()) { + if (this.userDataAutoSyncEnablementService.canToggleEnablement()) { this.registerTurnOnSyncAction(); this.registerTurnOffSyncAction(); } @@ -966,7 +967,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: showSyncedDataCommand.id, label: showSyncedDataCommand.title }); items.push({ type: 'separator' }); items.push({ id: syncNowCommand.id, label: syncNowCommand.title, description: syncNowCommand.description(that.userDataSyncService) }); - if (that.userDataAutoSyncService.canToggleEnablement()) { + if (that.userDataAutoSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; items.push({ id: turnOffSyncCommand.id, label: turnOffSyncCommand.title, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); } @@ -1166,7 +1167,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio @IDialogService private readonly dialogService: IDialogService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, ) { super(); @@ -1195,7 +1196,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; // we need a model } - if (!this.userDataAutoSyncService.isEnabled()) { + if (!this.userDataAutoSyncEnablementService.isEnabled()) { return false; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 8b801947f8d..1bf03ec3a5b 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncResourceEnablementService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncResourceEnablementService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; @@ -79,7 +79,7 @@ export class UserDataSyncDataViews extends Disposable { constructor( container: ViewContainer, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -194,7 +194,7 @@ export class UserDataSyncDataViews extends Disposable { } }); this._register(Event.any(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, - this.userDataAutoSyncService.onDidChangeEnablement, + this.userDataAutoSyncEnablementService.onDidChangeEnablement, this.userDataSyncService.onDidResetLocal, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts index 1f199d19b0a..bad65d90f44 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IUserDataSyncUtilService, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { UserDataSycnUtilServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index f11e9d412a5..fc7d8c587bd 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -38,11 +38,11 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { IHoverService, IHoverOptions, IHoverTarget } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { isMacintosh } from 'vs/base/common/platform'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; class Root implements ITreeItem { label = { label: 'root' }; @@ -724,6 +724,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { + return this.hoverService.showHover(options); + } + }; } get templateId(): string { @@ -752,7 +758,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer | undefined { + if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) { + if (resource) { + return undefined; + } else if (!node.tooltip) { + return label; + } else if (!isString(node.tooltip)) { + return Promise.resolve(node.tooltip); + } else { + return node.tooltip; + } + } + + return new Promise(async (resolve) => { + await node.resolve(); + resolve(node.tooltip); + }); + } + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { templateData.elementDisposable.dispose(); const node = element.element; @@ -787,12 +812,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); @@ -805,7 +828,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); @@ -841,62 +862,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer this.setAlignment(templateData.container, node))); - this.setupHovers(node, templateData.resourceLabel.element.firstElementChild!, disposableStore, fallbackHover); - this.setupHovers(node, templateData.icon, disposableStore, fallbackHover); - } - - private setupHovers(node: ITreeItem, htmlElement: HTMLElement, disposableStore: DisposableStore, label: string | undefined): void { - if (!(node instanceof ResolvableTreeItem) || (node.tooltip && isString(node.tooltip)) || (!node.tooltip && !node.hasResolve)) { - return; - } - const resolvableNode: ResolvableTreeItem = node; - - const hoverService = this.hoverService; - // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. - // On Mac, the delay is 1500. - const hoverDelay = isMacintosh ? 1500 : 500; - let hoverOptions: IHoverOptions | undefined; - let mouseX: number | undefined; - function mouseOver(this: HTMLElement, e: MouseEvent): any { - let isHovering = true; - function mouseMove(this: HTMLElement, e: MouseEvent): any { - mouseX = e.x; - } - function mouseLeave(this: HTMLElement, e: MouseEvent): any { - isHovering = false; - } - this.addEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave, { passive: true }); - this.addEventListener(DOM.EventType.MOUSE_MOVE, mouseMove, { passive: true }); - setTimeout(async () => { - await resolvableNode.resolve(); - const tooltip = resolvableNode.tooltip ?? label; - if (isHovering && tooltip) { - if (!hoverOptions) { - const target: IHoverTarget = { - targetElements: [this], - dispose: () => { } - }; - hoverOptions = { text: tooltip, target, anchorPosition: AnchorPosition.BELOW }; - } - if (mouseX !== undefined) { - (hoverOptions.target).x = mouseX + 10; - } - hoverService.showHover(hoverOptions); - } - this.removeEventListener(DOM.EventType.MOUSE_MOVE, mouseMove); - this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave); - }, hoverDelay); - } - htmlElement.addEventListener(DOM.EventType.MOUSE_OVER, mouseOver, { passive: true }); - disposableStore.add({ - dispose: () => { - htmlElement.removeEventListener(DOM.EventType.MOUSE_OVER, mouseOver); - } - }); } private setAlignment(container: HTMLElement, treeItem: ITreeItem) { - DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); + container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { @@ -1014,8 +983,7 @@ class TreeMenus extends Disposable implements IDisposable { constructor( private id: string, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IMenuService private readonly menuService: IMenuService ) { super(); } @@ -1037,7 +1005,7 @@ class TreeMenus extends Disposable implements IDisposable { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); menu.dispose(); contextKeyService.dispose(); diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index ba50cde5456..30340ca4663 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { OpenFolderAction, OpenFileFolderAction, OpenFileAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ShowAllCommandsAction } from 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess'; diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 6f19e4548e7..6b84fbcb776 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addClass } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -103,9 +102,7 @@ export abstract class BaseWebview extends Disposable { const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => { this._logService.debug(`Webview(${this.id}): webview ready`); - if (this.element) { - addClass(this.element, 'ready'); - } + this.element?.classList.add('ready'); if (this._state.type === WebviewState.Type.Initializing) { this._state.pendingMessages.forEach(({ channel, data }) => this.doPostMessage(channel, data)); diff --git a/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts index cf1f762e488..ba0c8561448 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewIconManager.ts @@ -6,7 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; export class WebviewIconManager { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index b7cd695aa46..6ac6a86d729 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -312,8 +312,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme if (!this.isFocused || !this.element) { return; } - - if (document.activeElement?.tagName === 'INPUT') { + if (document.activeElement && document.activeElement?.tagName !== 'BODY') { return; } try { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 4c0849a8821..41e915c4ecf 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { setImmediate } from 'vs/base/common/platform'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,7 +35,8 @@ const storageKeys = { export class WebviewViewPane extends ViewPane { - private _webview?: WebviewOverlay; + private readonly _webview = this._register(new MutableDisposable()); + private readonly _webviewDisposables = this._register(new DisposableStore()); private _activated = false; private _container?: HTMLElement; @@ -71,6 +73,14 @@ export class WebviewViewPane extends ViewPane { this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + + this._register(this.webviewViewService.onNewResolverRegistered(e => { + if (e.viewType === this.id) { + // Potentially re-activate if we have a new resolver + this.updateTreeVisibility(); + } + })); + this.updateTreeVisibility(); } @@ -83,14 +93,12 @@ export class WebviewViewPane extends ViewPane { dispose() { this._onDispose.fire(); - this._webview?.dispose(); - super.dispose(); } focus(): void { super.focus(); - this._webview?.focus(); + this._webview.value?.focus(); } renderBody(container: HTMLElement): void { @@ -102,7 +110,7 @@ export class WebviewViewPane extends ViewPane { this._resizeObserver = new ResizeObserver(() => { setImmediate(() => { if (this._container) { - this._webview?.layoutWebviewOverElement(this._container); + this._webview.value?.layoutWebviewOverElement(this._container); } }); }); @@ -115,8 +123,8 @@ export class WebviewViewPane extends ViewPane { } public saveState() { - if (this._webview) { - this.viewState[storageKeys.webviewState] = this._webview.state; + if (this._webview.value) { + this.viewState[storageKeys.webviewState] = this._webview.value.state; } this.memento.saveMemento(); @@ -126,21 +134,21 @@ export class WebviewViewPane extends ViewPane { protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); - if (!this._webview) { + if (!this._webview.value) { return; } if (this._container) { - this._webview.layoutWebviewOverElement(this._container, { width, height }); + this._webview.value.layoutWebviewOverElement(this._container, new Dimension(width, height)); } } private updateTreeVisibility() { if (this.isBodyVisible()) { this.activate(); - this._webview?.claim(this); + this._webview.value?.claim(this); } else { - this._webview?.release(this); + this._webview.value?.release(this); } } @@ -151,17 +159,20 @@ export class WebviewViewPane extends ViewPane { const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); webview.state = this.viewState[storageKeys.webviewState]; - this._webview = webview; + this._webview.value = webview; - this._register(toDisposable(() => { - this._webview?.release(this); + if (this._container) { + this._webview.value?.layoutWebviewOverElement(this._container); + } + + this._webviewDisposables.add(toDisposable(() => { + this._webview.value?.release(this); })); - this._register(webview.onDidUpdateState(() => { + this._webviewDisposables.add(webview.onDidUpdateState(() => { this.viewState[storageKeys.webviewState] = webview.state; })); - - const source = this._register(new CancellationTokenSource()); + const source = this._webviewDisposables.add(new CancellationTokenSource()); this.withProgress(async () => { await this.extensionService.activateByEvent(`onView:${this.id}`); @@ -178,6 +189,13 @@ export class WebviewViewPane extends ViewPane { get description(): string | undefined { return self.titleDescription; }, set description(value: string | undefined) { self.updateTitleDescription(value); }, + dispose: () => { + // Only reset and clear the webview itself. Don't dispose of the view container + this._activated = false; + this._webview.clear(); + this._webviewDisposables.clear(); + }, + show: (preserveFocus) => { this.viewService.openView(this.id, !preserveFocus); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts index 68196b0f01f..663e359f7cf 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; @@ -20,6 +20,8 @@ export interface WebviewView { readonly onDidChangeVisibility: Event; readonly onDispose: Event; + dispose(): void; + show(preserveFocus: boolean): void; } @@ -31,6 +33,8 @@ export interface IWebviewViewService { readonly _serviceBrand: undefined; + readonly onNewResolverRegistered: Event<{ readonly viewType: string }>; + register(type: string, resolver: IWebviewViewResolver): IDisposable; resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise; @@ -40,16 +44,20 @@ export class WebviewViewService extends Disposable implements IWebviewViewServic readonly _serviceBrand: undefined; - private readonly _views = new Map(); + private readonly _resolvers = new Map(); private readonly _awaitingRevival = new Map void }>(); + private readonly _onNewResolverRegistered = this._register(new Emitter<{ readonly viewType: string }>()); + public readonly onNewResolverRegistered = this._onNewResolverRegistered.event; + register(viewType: string, resolver: IWebviewViewResolver): IDisposable { - if (this._views.has(viewType)) { + if (this._resolvers.has(viewType)) { throw new Error(`View resolver already registered for ${viewType}`); } - this._views.set(viewType, resolver); + this._resolvers.set(viewType, resolver); + this._onNewResolverRegistered.fire({ viewType: viewType }); const pending = this._awaitingRevival.get(viewType); if (pending) { @@ -60,12 +68,12 @@ export class WebviewViewService extends Disposable implements IWebviewViewServic } return toDisposable(() => { - this._views.delete(viewType); + this._resolvers.delete(viewType); }); } resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise { - const resolver = this._views.get(viewType); + const resolver = this._resolvers.get(viewType); if (!resolver) { if (this._awaitingRevival.has(viewType)) { throw new Error('View already awaiting revival'); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts index 9153e3c1a4d..3f3d30665f6 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ViewsWelcomeContribution } from 'vs/workbench/contrib/welcome/common/viewsWelcomeContribution'; diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts index e2a76c54ed0..396871dc396 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts @@ -11,7 +11,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; Registry.as(ConfigurationExtensions.Configuration) diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 6f909ab86ae..4a61a79fe44 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -25,7 +25,7 @@ import { getInstalledExtensions, IExtensionStatus, onExtensionChanged, isKeymapE import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -import { ILifecycleService, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle'; import { splitName } from 'vs/base/common/labels'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.contribution.ts b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.contribution.ts index d0440e20402..ac2b9b11c09 100644 --- a/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.contribution.ts +++ b/src/vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.contribution.ts @@ -6,6 +6,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { BrowserTelemetryOptOut } from 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BrowserTelemetryOptOut, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.contribution.ts b/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.contribution.ts index 5c4f1ccf06e..9551df300cc 100644 --- a/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.contribution.ts +++ b/src/vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut.contribution.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { NativeTelemetryOptOut } from 'vs/workbench/contrib/welcome/telemetryOptOut/electron-sandbox/telemetryOptOut'; Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeTelemetryOptOut, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.ts index b729c1ba840..cd2d8c70aa6 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.ts @@ -56,7 +56,7 @@ app.listen(3000); ### Line Actions Since it's very common to work with the entire text in a line we provide a set of useful shortcuts to help with this. -1. Copy a line and insert it above or below the current position with kb(editor.action.copyLinesDownAction) or kb(editor.action.copyLinesUpAction) respectively. +1. Copy a line and insert it above or below the current position with kb(editor.action.copyLinesDownAction) or kb(editor.action.copyLinesUpAction) respectively.Copy the entire current line when no text is selected with kb(editor.action.clipboardCopyAction). 2. Move an entire line or selection of lines up or down with kb(editor.action.moveLinesUpAction) and kb(editor.action.moveLinesDownAction) respectively. 3. Delete the entire line with kb(editor.action.deleteLines). diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts index 24fc64ae597..d1bdbc2db74 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution.ts @@ -16,7 +16,7 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IEditorRegistry, Extensions as EditorExtensions, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; Registry.as(EditorExtensions.Editors) diff --git a/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts b/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts new file mode 100644 index 00000000000..eee904f92fa --- /dev/null +++ b/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { INeverShowAgainOptions, INotificationService, NeverShowAgainScope, Severity } from 'vs/platform/notification/common/notification'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; + +/** + * A workbench contribution that will look for `.code-workspace` files in the root of the + * workspace folder and open a notification to suggest to open one of the workspaces. + */ +export class WorkspacesFinderContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @INotificationService private readonly notificationService: INotificationService, + @IFileService private readonly fileService: IFileService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IHostService private readonly hostService: IHostService + ) { + super(); + + this.findWorkspaces(); + } + + private async findWorkspaces(): Promise { + const folder = this.contextService.getWorkspace().folders[0]; + if (!folder || this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER) { + return; // require a single root folder + } + + const rootFileNames = (await this.fileService.resolve(folder.uri)).children?.map(child => child.name); + if (Array.isArray(rootFileNames)) { + const workspaceFiles = rootFileNames.filter(hasWorkspaceFileExtension); + if (workspaceFiles.length > 0) { + this.doHandleWorkspaceFiles(folder.uri, workspaceFiles); + } + } + } + + private doHandleWorkspaceFiles(folder: URI, workspaces: string[]): void { + const neverShowAgain: INeverShowAgainOptions = { id: 'workspaces.dontPromptToOpen', scope: NeverShowAgainScope.WORKSPACE, isSecondary: true }; + + // Prompt to open one workspace + if (workspaces.length === 1) { + const workspaceFile = workspaces[0]; + + this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ + label: localize('openWorkspace', "Open Workspace"), + run: () => this.hostService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) + }], { neverShowAgain }); + } + + // Prompt to select a workspace from many + else if (workspaces.length > 1) { + this.notificationService.prompt(Severity.Info, localize('workspacesFound', "This folder contains multiple workspace files. Do you want to open one? [Learn more]({0}) about workspace files.", 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ + label: localize('selectWorkspace', "Select Workspace"), + run: () => { + this.quickInputService.pick( + workspaces.map(workspace => ({ label: workspace } as IQuickPickItem)), + { placeHolder: localize('selectToOpen', "Select a workspace to open") }).then(pick => { + if (pick) { + this.hostService.openWindow([{ workspaceUri: joinPath(folder, pick.label) }]); + } + }); + } + }], { neverShowAgain }); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspacesFinderContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 42115bba46d..9edf976b57c 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -45,8 +45,8 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file import { basename } from 'vs/base/common/resources'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; -import { NativeResourceIdentityService } from 'vs/platform/resource/node/resourceIdentityServiceImpl'; -import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { NativeResourceIdentityService } from 'vs/workbench/services/resourceIdentity/node/resourceIdentityServiceImpl'; +import { IResourceIdentityService } from 'vs/workbench/services/resourceIdentity/common/resourceIdentityService'; import { NativeLogService } from 'vs/workbench/services/log/electron-browser/logService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index a5536518ca6..75a6eaa0c77 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -29,7 +29,7 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; -import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IResourceIdentityService } from 'vs/workbench/services/resourceIdentity/common/resourceIdentityService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; import { SimpleConfigurationService, simpleFileSystemProvider, SimpleLogService, SimpleRemoteAgentService, SimpleResourceIdentityService, SimpleSignService, SimpleStorageService, SimpleNativeWorkbenchEnvironmentService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index 502e6437e6f..629a8dd39e5 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -7,7 +7,7 @@ /* eslint-disable code-import-patterns */ import { ConsoleLogService } from 'vs/platform/log/common/log'; -import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IResourceIdentityService } from 'vs/workbench/services/resourceIdentity/common/resourceIdentityService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { hash } from 'vs/base/common/hash'; import { URI } from 'vs/base/common/uri'; @@ -61,6 +61,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IExtensionHostDebugParams } from 'vs/platform/environment/common/environment'; import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import { Schemas } from 'vs/base/common/network'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; //#region Environment @@ -714,6 +715,23 @@ registerSingleton(IUserDataSyncStoreManagementService, SimpleIUserDataSyncStoreM //#endregion +//#region IStorageKeysSyncRegistryService + +class SimpleIStorageKeysSyncRegistryService implements IStorageKeysSyncRegistryService { + + declare readonly _serviceBrand: undefined; + + onDidChangeStorageKeys = Event.None; + + storageKeys = []; + + registerStorageKey(): void { } +} + +registerSingleton(IStorageKeysSyncRegistryService, SimpleIStorageKeysSyncRegistryService); + +//#endregion + //#region Task diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 49bb6f5b93e..c847975f9da 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -27,7 +27,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts index bc250461016..cdf58500913 100644 --- a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts @@ -14,7 +14,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; interface AccessibilityMetrics { diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 0392acfd564..74fa84fa6d6 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -123,7 +123,7 @@ const authenticationDefinitionSchema: IJSONSchema = { const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'authentication', jsonSchema: { - description: nls.localize('authenticationExtensionPoint', 'Contributes authentication'), + description: nls.localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), type: 'array', items: authenticationDefinitionSchema } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index a2c5b9f443b..2e23db80ae7 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -29,7 +29,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { toErrorMessage } from 'vs/base/common/errorMessage'; diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 7692bb61260..57a0b059141 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -53,6 +53,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import product from 'vs/platform/product/common/product'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { Event } from 'vs/base/common/event'; class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -1218,15 +1219,12 @@ suite('WorkspaceConfigurationService - Folder', () => { const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json')); await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); - await new Promise(async (c) => { - const disposable = testObject.onDidChangeConfiguration(e => { - assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); - assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); - disposable.dispose(); - c(); - }); + const e = await new Promise(async (c) => { + Event.once(testObject.onDidChangeConfiguration)(c); await fileService.del(workspaceSettingsResource); }); + assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); + assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); }); }); diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index cbea77efa04..a40d034ec7f 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -10,7 +10,7 @@ import { Schemas } from 'vs/base/common/network'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -146,8 +146,9 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR // get all "inputs" let inputs: ConfiguredInput[] = []; - if (folder && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - let result = this.configurationService.inspect(section, { resource: folder.uri }); + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { + const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; + let result = this.configurationService.inspect(section, overrides); if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { switch (target) { case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; @@ -155,7 +156,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR default: inputs = (result.workspaceFolderValue)?.inputs; } } else { - const valueResult = this.configurationService.getValue(section, { resource: folder.uri }); + const valueResult = this.configurationService.getValue(section, overrides); if (valueResult) { inputs = valueResult.inputs; } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 80818c1f705..3e3de85fb43 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -448,6 +448,7 @@ suite('Configuration Resolver Service', () => { assert.equal(1, mockCommandService.callCount); }); }); + test('a single prompt input variable', () => { const configuration = { @@ -475,6 +476,7 @@ suite('Configuration Resolver Service', () => { assert.equal(0, mockCommandService.callCount); }); }); + test('a single pick input variable', () => { const configuration = { @@ -502,6 +504,7 @@ suite('Configuration Resolver Service', () => { assert.equal(0, mockCommandService.callCount); }); }); + test('a single command input variable', () => { const configuration = { @@ -529,6 +532,7 @@ suite('Configuration Resolver Service', () => { assert.equal(1, mockCommandService.callCount); }); }); + test('several input variables and command', () => { const configuration = { @@ -558,6 +562,35 @@ suite('Configuration Resolver Service', () => { assert.equal(2, mockCommandService.callCount); }); }); + + test('input variable with undefined workspace folder', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${input:input1}', + 'port': 5858, + 'sourceMaps': false, + 'outDir': null + }; + + return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, 'tasks').then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'resolvedEnterinput1', + 'port': 5858, + 'sourceMaps': false, + 'outDir': null + }); + + assert.equal(0, mockCommandService.callCount); + }); + }); + test('contributed variable', () => { const buildTask = 'npm: compile'; const variable = 'defaultBuildTask'; diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 2ea6e902612..a2005fe6954 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -10,7 +10,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { getZoomFactor } from 'vs/base/browser/browser'; import { unmnemonicLabel } from 'vs/base/common/labels'; -import { Event, Emitter } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IContextMenuDelegate, IContextMenuEvent } from 'vs/base/browser/contextmenu'; import { once } from 'vs/base/common/functional'; @@ -31,8 +30,6 @@ export class ContextMenuService extends Disposable implements IContextMenuServic declare readonly _serviceBrand: undefined; - get onDidContextMenu(): Event { return this.impl.onDidContextMenu; } - private impl: IContextMenuService; constructor( @@ -66,9 +63,6 @@ class NativeContextMenuService extends Disposable implements IContextMenuService declare readonly _serviceBrand: undefined; - private _onDidContextMenu = this._register(new Emitter()); - readonly onDidContextMenu: Event = this._onDidContextMenu.event; - constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -85,7 +79,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService delegate.onHide(false); } - this._onDidContextMenu.fire(); + dom.ModifierKeyEmitter.getInstance().resetKeyStatus(); }); const menu = this.createMenu(delegate, actions, onHide); diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 301187f332b..fda885169d6 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -17,7 +17,6 @@ import { localize } from 'vs/nls'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; import { hash } from 'vs/base/common/hash'; class DecorationRule { @@ -327,7 +326,6 @@ export class DecorationsService implements IDecorationsService { constructor( @IThemeService themeService: IThemeService, - @ILogService private readonly _logService: ILogService, ) { this._decorationStyles = new DecorationStyles(themeService); } @@ -369,7 +367,6 @@ export class DecorationsService implements IDecorationsService { if (!isChild || deco.bubble) { data.push(deco); containsChildren = isChild || containsChildren; - this._logService.trace('DecorationsService#getDecoration#getOrRetrieve', wrapper.provider.label, deco, isChild, uri); } }); } 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 b129b381291..9cbdbb2a632 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -10,7 +10,6 @@ import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ConsoleLogService } from 'vs/platform/log/common/log'; suite('DecorationsService', function () { @@ -20,7 +19,7 @@ suite('DecorationsService', function () { if (service) { service.dispose(); } - service = new DecorationsService(new TestThemeService(), new ConsoleLogService()); + service = new DecorationsService(new TestThemeService()); }); test('Async provider, async/evented result', function () { diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/dialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/dialogService.ts index d28b6b5e923..c6886ea84e7 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/dialogService.ts @@ -215,7 +215,7 @@ class NativeDialogService implements IDialogService { const osProps = await this.nativeHostService.getOSProperties(); const detailString = (useAgo: boolean): string => { - return nls.localize('aboutDetail', + return nls.localize({ key: 'aboutDetail', comment: ['Electron, Chrome, Node.js and V8 are product names that need no translation'] }, "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", version, this.productService.commit || 'Unknown', diff --git a/src/vs/workbench/services/encryption/common/encryptionService.ts b/src/vs/workbench/services/encryption/common/encryptionService.ts index ed4d0c23b4e..d264a3b9dc0 100644 --- a/src/vs/workbench/services/encryption/common/encryptionService.ts +++ b/src/vs/workbench/services/encryption/common/encryptionService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICommonEncryptionService } from 'vs/platform/encryption/electron-main/common/encryptionService'; +import { ICommonEncryptionService } from 'vs/platform/encryption/common/encryptionService'; export const IEncryptionService = createDecorator('encryptionService'); diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index cfc85669558..d67f433f8c9 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -20,9 +20,11 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -44,11 +46,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService private readonly productService: IProductService, - @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @INotificationService private readonly notificationService: INotificationService, - // @IHostService private readonly hostService: IHostService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); this.storageManger = this._register(new StorageManager(storageService)); @@ -60,9 +62,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench this.lifecycleService.when(LifecyclePhase.Restored).then(() => { this.notificationService.prompt(Severity.Info, localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{ label: localize('Reload', "Reload"), - run: () => { - //this.hostService.reload(); - } + // Using ReloadWindowAction because depending on IHostService causes cyclic dependency - #108522 + run: () => instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL).run() }]); }); } @@ -104,7 +105,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench throw new Error(localize('cannot disable language pack extension', "Cannot change enablement of {0} extension because it contributes language packs.", extension.manifest.displayName || extension.identifier.id)); } - if (this.userDataAutoSyncService.isEnabled() && this.userDataSyncAccountService.account && + if (this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account && isAuthenticaionProviderExtension(extension.manifest) && extension.manifest.contributes!.authentication!.some(a => a.id === this.userDataSyncAccountService.account!.authenticationProviderId)) { throw new Error(localize('cannot disable auth extension', "Cannot change enablement {0} extension because Settings Sync depends on it.", extension.manifest.displayName || extension.identifier.id)); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 64197db0d5c..adf1279b705 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,17 +6,16 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; -export const IExtensionManagementServerService = createDecorator('extensionManagementServerService'); - export interface IExtensionManagementServer { id: string; label: string; extensionManagementService: IExtensionManagementService; } +export const IExtensionManagementServerService = createDecorator('extensionManagementServerService'); export interface IExtensionManagementServerService { readonly _serviceBrand: undefined; readonly localExtensionManagementServer: IExtensionManagementServer | null; @@ -25,6 +24,12 @@ export interface IExtensionManagementServerService { getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null; } +export const IWorkbenchExtensioManagementService = createDecorator('extensionManagementService'); +export interface IWorkbenchExtensioManagementService extends IExtensionManagementService { + readonly _serviceBrand: undefined; + updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise; +} + export const enum EnablementState { DisabledByExtensionKind, DisabledByEnvironemt, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 10009a8b260..2f987d73934 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,9 +5,9 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED + ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -21,7 +21,7 @@ import { Schemas } from 'vs/base/common/network'; import { IDownloadService } from 'vs/platform/download/common/download'; import { flatten } from 'vs/base/common/arrays'; -export class ExtensionManagementService extends Disposable implements IExtensionManagementService { +export class ExtensionManagementService extends Disposable implements IWorkbenchExtensioManagementService { declare readonly _serviceBrand: undefined; @@ -136,6 +136,14 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject(`Invalid location ${extension.location.toString()}`); } + updateExtensionScope(extension: ILocalExtension, isMachineScoped: boolean): Promise { + const server = this.getServer(extension); + if (server) { + return server.extensionManagementService.updateExtensionScope(extension, isMachineScoped); + } + return Promise.reject(`Invalid location ${extension.location.toString()}`); + } + zip(extension: ILocalExtension): Promise { const server = this.getServer(extension); if (server) { @@ -198,51 +206,50 @@ export class ExtensionManagementService extends Disposable implements IExtension return false; } - async installFromGallery(gallery: IGalleryExtension): Promise { - - // Only local server, install without any checks - if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + async updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise { + const server = this.getServer(extension); + if (!server) { + return Promise.reject(`Invalid location ${extension.location.toString()}`); } + const servers: IExtensionManagementServer[] = []; + + // Update Language pack on all servers + if (isLanguagePackExtension(extension.manifest)) { + servers.push(...this.servers); + } else { + servers.push(server); + } + + return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); + } + + async installFromGallery(gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { + const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); if (!manifest) { return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); } + const servers: IExtensionManagementServer[] = []; + // Install Language pack on all servers if (isLanguagePackExtension(manifest)) { - return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); + servers.push(...this.servers); + } else { + const server = this.getExtensionManagementServerToInstall(manifest); + if (server) { + servers.push(server); + } } - // 1. Install on preferred location - - // Install UI preferred extension on local server - if (prefersExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - // Install Workspace preferred extension on remote server - if (prefersExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - // Install Web preferred extension on web server - if (prefersExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) { - return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - - // 2. Install on supported location - - // Install UI supported extension on local server - if (canExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - // Install Workspace supported extension on remote server - if (canExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - // Install Web supported extension on web server - if (canExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) { - return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + if (servers.length) { + if (!installOptions?.isMachineScoped) { + if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer)) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + } + return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); } if (this.extensionManagementServerService.remoteExtensionManagementServer) { @@ -256,6 +263,46 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject(error); } + private getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | undefined { + + // Only local server + if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer; + } + + // 1. Install on preferred location + + // Install UI preferred extension on local server + if (prefersExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer; + } + // Install Workspace preferred extension on remote server + if (prefersExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer; + } + // Install Web preferred extension on web server + if (prefersExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) { + return this.extensionManagementServerService.webExtensionManagementServer; + } + + // 2. Install on supported location + + // Install UI supported extension on local server + if (canExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer; + } + // Install Workspace supported extension on remote server + if (canExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer; + } + // Install Web supported extension on web server + if (canExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) { + return this.extensionManagementServerService.webExtensionManagementServer; + } + + return undefined; + } + getExtensionsReport(): Promise { if (this.extensionManagementServerService.localExtensionManagementServer) { return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getExtensionsReport(); diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c9c957ce313..6a9b150be2c 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -107,5 +107,5 @@ export class WebExtensionManagementService extends Disposable implements IExtens install(vsix: URI): Promise { throw new Error('unsupported'); } reinstallFromGallery(extension: ILocalExtension): Promise { throw new Error('unsupported'); } getExtensionsReport(): Promise { throw new Error('unsupported'); } - + updateExtensionScope(): Promise { throw new Error('unsupported'); } } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index e005befccac..2d61870275f 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { generateUuid } from 'vs/base/common/uuid'; -import { ILocalExtension, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionManagementService as BaseExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDownloadService } from 'vs/platform/download/common/download'; @@ -39,4 +39,4 @@ export class ExtensionManagementService extends BaseExtensionManagementService { } } -registerSingleton(IExtensionManagementService, ExtensionManagementService); +registerSingleton(IWorkbenchExtensioManagementService, ExtensionManagementService); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 46f638667c0..3a1ab6031d6 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -45,19 +45,19 @@ export class NativeRemoteExtensionManagementService extends WebRemoteExtensionMa return local; } - async installFromGallery(extension: IGalleryExtension): Promise { - const local = await this.doInstallFromGallery(extension); + async installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + const local = await this.doInstallFromGallery(extension, installOptions); await this.installUIDependenciesAndPackedExtensions(local); return local; } - private async doInstallFromGallery(extension: IGalleryExtension): Promise { + private async doInstallFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { if (this.configurationService.getValue('remote.downloadExtensionsLocally')) { this.logService.trace(`Download '${extension.identifier.id}' extension locally and install`); return this.downloadCompatibleAndInstall(extension); } try { - const local = await super.installFromGallery(extension); + const local = await super.installFromGallery(extension, installOptions); return local; } catch (error) { try { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 8fecb410ea6..8080ace1e78 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -22,9 +22,9 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { productService, TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; // import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -56,11 +56,11 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(IConfigurationService), extensionManagementServerService, productService, - instantiationService.get(IUserDataAutoSyncService) || instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return false; } }), + instantiationService.get(IUserDataAutoSyncEnablementService) || instantiationService.stub(IUserDataAutoSyncEnablementService, >{ isEnabled() { return false; } }), instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService), instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()), instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), - // instantiationService.get(IHostService), + instantiationService, ); } @@ -401,7 +401,7 @@ suite('ExtensionEnablementService Test', () => { }); test('test canChangeEnablement return false for auth extension and user data sync account depends on it and auto sync is on', () => { - instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return true; } }); + instantiationService.stub(IUserDataAutoSyncEnablementService, >{ isEnabled() { return true; } }); instantiationService.stub(IUserDataSyncAccountService, >{ account: { authenticationProviderId: 'a' } }); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 5eaec3499a3..1dff19bf177 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -9,7 +9,7 @@ import { IWorkbenchExtensionEnablementService, IWebExtensionsScannerService } fr import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionService, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IExtensionHost, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -23,9 +23,11 @@ import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browse import { Schemas } from 'vs/base/common/network'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; +import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -79,6 +81,20 @@ export class ExtensionService extends AbstractExtensionService implements IExten super.dispose(); } + protected _onExtensionHostCrashed(extensionHost: ExtensionHostManager, code: number, signal: string | null): void { + super._onExtensionHostCrashed(extensionHost, code, signal); + if (extensionHost.kind === ExtensionHostKind.LocalWebWorker) { + if (code === ExtensionHostExitCode.StartTimeout10s) { + this._notificationService.prompt( + Severity.Error, + nls.localize('extensionService.startTimeout', "The Web Worker Extension Host did not start in 10s."), + [] + ); + return; + } + } + } + protected async _scanSingleExtension(extension: IExtension): Promise { if (extension.location.scheme === Schemas.vscodeRemote) { return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index ef4ffab918f..207a43ec779 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -21,7 +21,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index f0f7cb8f676..cffb140791c 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { VSBuffer } from 'vs/base/common/buffer'; -import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { createMessageOfType, MessageType, isMessageOfType, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IInitData, UIKind } from 'vs/workbench/api/common/extHost.protocol'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -16,7 +16,6 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; -import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; @@ -28,9 +27,9 @@ import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output import { localize } from 'vs/nls'; import { generateUuid } from 'vs/base/common/uuid'; import { canceled, onUnexpectedError } from 'vs/base/common/errors'; -import { WEB_WORKER_IFRAME } from 'vs/workbench/services/extensions/common/webWorkerIframe'; import { Barrier } from 'vs/base/common/async'; import { FileAccess } from 'vs/base/common/network'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; @@ -64,6 +63,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost @ILogService private readonly _logService: ILogService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, + @ILayoutService private readonly _layoutService: ILayoutService, ) { super(); this._isTerminating = false; @@ -81,10 +81,38 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost return true; } + private _webWorkerExtensionHostIframeSrc(): string | null { + if (this._environmentService.options && this._environmentService.options.webWorkerExtensionHostIframeSrc) { + return this._environmentService.options.webWorkerExtensionHostIframeSrc; + } + if (this._productService.webEndpointUrl) { + const forceHTTPS = (location.protocol === 'https:'); + let baseUrl = this._productService.webEndpointUrl; + if (this._productService.quality) { + baseUrl += `/${this._productService.quality}`; + } + if (this._productService.commit) { + baseUrl += `/${this._productService.commit}`; + } + return ( + forceHTTPS + ? `${baseUrl}/out/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html` + : `${baseUrl}/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` + ); + } + return null; + } + public async start(): Promise { if (!this._protocolPromise) { - if (platform.isWeb && !browser.isSafari && this._wrapInIframe()) { - this._protocolPromise = this._startInsideIframe(); + if (platform.isWeb) { + const webWorkerExtensionHostIframeSrc = this._webWorkerExtensionHostIframeSrc(); + if (webWorkerExtensionHostIframeSrc && this._wrapInIframe()) { + this._protocolPromise = this._startInsideIframe(webWorkerExtensionHostIframeSrc); + } else { + console.warn(`The web worker extension host is started without an iframe sandbox!`); + this._protocolPromise = this._startOutsideIframe(); + } } else { this._protocolPromise = this._startOutsideIframe(); } @@ -93,37 +121,41 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost return this._protocolPromise; } - private async _startInsideIframe(): Promise { + private async _startInsideIframe(webWorkerExtensionHostIframeSrc: string): Promise { const emitter = this._register(new Emitter()); const iframe = document.createElement('iframe'); iframe.setAttribute('class', 'web-worker-ext-host-iframe'); - iframe.setAttribute('sandbox', 'allow-scripts'); + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); iframe.style.display = 'none'; const vscodeWebWorkerExtHostId = generateUuid(); - const workerUrl = FileAccess.asBrowserUri('../worker/extensionHostWorkerMain.js', require).toString(true); - const workerSrc = getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true); - const escapeAttribute = (value: string): string => { - return value.replace(/"/g, '"'); - }; - const forceHTTPS = (location.protocol === 'https:'); - const html = ` - - - - - - - - - -`; - const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; - iframe.setAttribute('src', iframeContent); + iframe.setAttribute('src', `${webWorkerExtensionHostIframeSrc}?vscodeWebWorkerExtHostId=${vscodeWebWorkerExtHostId}`); const barrier = new Barrier(); let port!: MessagePort; + let barrierError: Error | null = null; + let barrierHasError = false; + let startTimeout: any = null; + + const rejectBarrier = (exitCode: number, error: Error) => { + barrierError = error; + barrierHasError = true; + onUnexpectedError(barrierError); + clearTimeout(startTimeout); + this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, barrierError.message]); + barrier.open(); + }; + + const resolveBarrier = (messagePort: MessagePort) => { + port = messagePort; + clearTimeout(startTimeout); + barrier.open(); + }; + + startTimeout = setTimeout(() => { + rejectBarrier(ExtensionHostExitCode.StartTimeout10s, new Error('The Web Worker Extension Host did not start in 10s')); + }, 10000); this._register(dom.addDisposableListener(window, 'message', (event) => { if (event.source !== iframe.contentWindow) { @@ -138,27 +170,28 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost err.message = message; err.name = name; err.stack = stack; - onUnexpectedError(err); - this._onDidExit.fire([18, err.message]); - return; + return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err); } const { data } = event.data; if (barrier.isOpen() || !(data instanceof MessagePort)) { console.warn('UNEXPECTED message', event); - this._onDidExit.fire([81, 'UNEXPECTED message']); - return; + const err = new Error('UNEXPECTED message'); + return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err); } - port = data; - barrier.open(); + resolveBarrier(data); })); - document.body.appendChild(iframe); + this._layoutService.container.appendChild(iframe); this._register(toDisposable(() => iframe.remove())); // await MessagePort and use it to directly communicate // with the worker extension host await barrier.wait(); + if (barrierHasError) { + throw barrierError; + } + port.onmessage = (event) => { const { data } = event; if (!(data instanceof ArrayBuffer)) { @@ -193,7 +226,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost const { data } = event; if (barrier.isOpen() || !(data instanceof MessagePort)) { console.warn('UNEXPECTED message', event); - this._onDidExit.fire([81, 'UNEXPECTED message']); + this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, 'UNEXPECTED message']); return; } port = data; @@ -217,7 +250,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost worker.onerror = (event) => { console.error(event.message, event.error); - this._onDidExit.fire([81, event.message || event.error]); + this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, event.message || event.error]); }; // keep for cleanup diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index 1889832dd7b..c60299a9a42 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -5,6 +5,13 @@ import { VSBuffer } from 'vs/base/common/buffer'; +export const enum ExtensionHostExitCode { + // nodejs uses codes 1-13 and exit codes >128 are signal exits + VersionMismatch = 55, + StartTimeout10s = 56, + UnexpectedError = 81, +} + export interface IExtHostReadyMessage { type: 'VSCODE_EXTHOST_IPC_READY'; } diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 1d26bf01091..9f03d2be9e6 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -19,7 +19,7 @@ import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platf import * as platform from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { VSBuffer } from 'vs/base/common/buffer'; diff --git a/src/vs/workbench/services/extensions/common/webWorkerIframe.ts b/src/vs/workbench/services/extensions/common/webWorkerIframe.ts deleted file mode 100644 index 7b354788d46..00000000000 --- a/src/vs/workbench/services/extensions/common/webWorkerIframe.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export const WEB_WORKER_IFRAME = { - sha: 'sha256-r24mDVsMuFEo8ChaY9ppVJKbY3CUM4I12Aw/yscWZbg=', - js: ` -(function() { - const workerSrc = document.getElementById('vscode-worker-src').getAttribute('data-value'); - const worker = new Worker(workerSrc, { name: 'WorkerExtensionHost' }); - const vscodeWebWorkerExtHostId = document.getElementById('vscode-web-worker-ext-host-id').getAttribute('data-value'); - - worker.onmessage = (event) => { - const { data } = event; - if (!(data instanceof MessagePort)) { - console.warn('Unknown data received', event); - window.parent.postMessage({ - vscodeWebWorkerExtHostId, - error: { - name: 'Error', - message: 'Unknown data received', - stack: [] - } - }, '*'); - return; - } - window.parent.postMessage({ - vscodeWebWorkerExtHostId, - data: data - }, '*', [data]); - }; - - worker.onerror = (event) => { - console.error(event.message, event.error); - window.parent.postMessage({ - vscodeWebWorkerExtHostId, - error: { - name: event.error ? event.error.name : '', - message: event.error ? event.error.message : '', - stack: event.error ? event.error.stack : [] - } - }, '*'); - }; -})(); -` -}; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index f940a3610f1..e76e7793de5 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -17,7 +17,7 @@ import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtension import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -39,6 +39,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ILogService } from 'vs/platform/log/common/log'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Schemas } from 'vs/base/common/network'; +import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -199,7 +200,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten super._onExtensionHostCrashed(extensionHost, code, signal); if (extensionHost.kind === ExtensionHostKind.LocalProcess) { - if (code === 55) { + if (code === ExtensionHostExitCode.VersionMismatch) { this._notificationService.prompt( Severity.Error, nls.localize('extensionService.versionMismatchCrash', "Extension host cannot start: version mismatch."), diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index a41bafbb44a..5ec4c4bbae8 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -23,7 +23,7 @@ import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ILifecycleService, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 4eb8204bf5a..e39d131fe7b 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -13,7 +13,7 @@ import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/ import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import product from 'vs/platform/product/common/product'; import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; @@ -225,7 +225,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise console.trace(`'addEventListener' has been blocked (self)['webkitResolveLocalFileSystemSyncURL'] = undefined; (self)['webkitResolveLocalFileSystemURL'] = undefined; -if (location.protocol === 'data:') { +if ((self).Worker) { // make sure new Worker(...) always uses data: - const _Worker = Worker; + const _Worker = (self).Worker; Worker = function (stringUrl: string | URL, options?: WorkerOptions) { const js = `importScripts('${stringUrl}');`; options = options || {}; diff --git a/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html b/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html new file mode 100644 index 00000000000..a99d3df29df --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/src/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html b/src/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html new file mode 100644 index 00000000000..27b6114a13b --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index dea4121181d..0f92adfe6bd 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -13,7 +13,7 @@ import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, i import { pathsToEditors } from 'vs/workbench/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; -import { trackFocus } from 'vs/base/browser/dom'; +import { IModifierKeyStatus, ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -23,7 +23,7 @@ import { parseLineAndColumnAware } from 'vs/base/common/extpath'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { BeforeShutdownEvent, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { BeforeShutdownEvent, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; /** * A workspace to open in the workbench can either be: @@ -58,13 +58,31 @@ export interface IWorkspaceProvider { open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise; } +enum HostShutdownReason { + + /** + * An unknown shutdown reason. + */ + Unknown = 1, + + /** + * A shutdown that was potentially triggered by keyboard use. + */ + Keyboard = 2, + + /** + * An explicit shutdown via code. + */ + Api = 3 +} + export class BrowserHostService extends Disposable implements IHostService { declare readonly _serviceBrand: undefined; private workspaceProvider: IWorkspaceProvider; - private signalExpectedShutdown = false; + private shutdownReason = HostShutdownReason.Unknown; constructor( @ILayoutService private readonly layoutService: ILayoutService, @@ -91,20 +109,37 @@ export class BrowserHostService extends Disposable implements IHostService { } private registerListeners(): void { + + // Veto shutdown depending on `window.confirmBeforeClose` setting this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e))); + + // Track modifier keys to detect keybinding usage + this._register(ModifierKeyEmitter.getInstance().event(e => this.updateShutdownReasonFromEvent(e))); } private onBeforeShutdown(e: BeforeShutdownEvent): void { - // Veto is setting is configured as such and we are not - // expecting a navigation that was triggered by the user - if (!this.signalExpectedShutdown && this.configurationService.getValue('window.confirmBeforeClose')) { - console.warn('Unload prevented: window.confirmBeforeClose=true'); + // Veto the shutdown depending on `window.confirmBeforeClose` setting + const confirmBeforeClose = this.configurationService.getValue<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose'); + if (confirmBeforeClose === 'always' || (this.shutdownReason === HostShutdownReason.Keyboard && confirmBeforeClose === 'keyboardOnly')) { + console.warn('Unload veto: window.confirmBeforeClose=true'); e.veto(true); } // Unset for next shutdown - this.signalExpectedShutdown = false; + this.shutdownReason = HostShutdownReason.Unknown; + } + + private updateShutdownReasonFromEvent(e: IModifierKeyStatus): void { + if (this.shutdownReason === HostShutdownReason.Api) { + return; // do not overwrite any explicitly set shutdown reason + } + + if (ModifierKeyEmitter.getInstance().isModifierPressed) { + this.shutdownReason = HostShutdownReason.Keyboard; + } else { + this.shutdownReason = HostShutdownReason.Unknown; + } } //#region Focus @@ -317,8 +352,11 @@ export class BrowserHostService extends Disposable implements IHostService { } private doOpen(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { + + // We know that `workspaceProvider.open` will trigger a shutdown + // with `options.reuse` so we update `shutdownReason` to reflect that if (options?.reuse) { - this.signalExpectedShutdown = true; + this.shutdownReason = HostShutdownReason.Api; } return this.workspaceProvider.open(workspace, options); @@ -367,9 +405,23 @@ export class BrowserHostService extends Disposable implements IHostService { } async reload(): Promise { - this.signalExpectedShutdown = true; + this.withExpectedShutdown(() => { + window.location.reload(); + }); + } - window.location.reload(); + async close(): Promise { + this.withExpectedShutdown(() => { + window.close(); + }); + } + + private withExpectedShutdown(callback: () => void): void { + + // Update shutdown reason in a way that we do not show a dialog + this.shutdownReason = HostShutdownReason.Api; + + callback(); } //#endregion diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 222da33fcde..db8a05c4262 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -83,5 +83,10 @@ export interface IHostService { */ reload(): Promise; + /** + * Attempt to close the active window. + */ + close(): Promise; + //#endregion } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index cef9b4614e8..005f5d42629 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -106,6 +106,10 @@ export class NativeHostService extends Disposable implements IHostService { return this.nativeHostService.reload(); } + close(): Promise { + return this.nativeHostService.closeWindow(); + } + //#endregion } diff --git a/src/vs/workbench/services/integrity/node/integrityService.ts b/src/vs/workbench/services/integrity/node/integrityService.ts index 7d50386ce6f..516484cb126 100644 --- a/src/vs/workbench/services/integrity/node/integrityService.ts +++ b/src/vs/workbench/services/integrity/node/integrityService.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { ChecksumPair, IIntegrityService, IntegrityTestResult } from 'vs/workbench/services/integrity/common/integrity'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index 86f63f04b56..c1b995107de 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -956,6 +956,12 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { } } + // See https://github.com/microsoft/vscode/issues/108880 + if (this._OS === OperatingSystem.Macintosh && binding.ctrlKey && !binding.metaKey && !binding.altKey && constantKeyCode === KeyCode.US_MINUS) { + // ctrl+- and ctrl+shift+- render very similarly in native macOS menus, leading to confusion + return null; + } + if (constantKeyCode !== -1) { return this._getElectronLabelForKeyCode(constantKeyCode); } diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 30c1951cf4e..9efe8d8bf18 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -27,7 +27,7 @@ import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybindin import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/mac_en_us.txt b/src/vs/workbench/services/keybinding/test/electron-browser/mac_en_us.txt index 7a83477293f..5881abce1cf 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/mac_en_us.txt +++ b/src/vs/workbench/services/keybinding/test/electron-browser/mac_en_us.txt @@ -345,9 +345,9 @@ isUSStandard: true | HW Code combination | Key | KeyCode combination | Pri | UI label | User settings | Electron accelerator | Dispatching string | WYSIWYG | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Minus | - | - | | - | - | - | [Minus] | | -| Ctrl+Minus | - | Ctrl+- | | Ctrl+- | ctrl+- | Ctrl+- | ctrl+[Minus] | | +| Ctrl+Minus | - | Ctrl+- | | Ctrl+- | ctrl+- | null | ctrl+[Minus] | | | Shift+Minus | _ | Shift+- | | Shift+- | shift+- | Shift+- | shift+[Minus] | | -| Ctrl+Shift+Minus | _ | Ctrl+Shift+- | | Ctrl+Shift+- | ctrl+shift+- | Ctrl+Shift+- | ctrl+shift+[Minus] | | +| Ctrl+Shift+Minus | _ | Ctrl+Shift+- | | Ctrl+Shift+- | ctrl+shift+- | null | ctrl+shift+[Minus] | | | Alt+Minus | - | Alt+- | | Alt+- | alt+- | Alt+- | alt+[Minus] | | | Ctrl+Alt+Minus | – | Ctrl+Alt+- | | Ctrl+Alt+- | ctrl+alt+- | Ctrl+Alt+- | ctrl+alt+[Minus] | | | Shift+Alt+Minus | _ | Shift+Alt+- | | Shift+Alt+- | shift+alt+- | Shift+Alt+- | shift+alt+[Minus] | | diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 66b354a5a71..74dc864bf51 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -18,7 +18,7 @@ import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderW import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { match } from 'vs/base/common/glob'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 75434b06573..a405e8973c6 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -232,7 +232,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Resizes currently focused part on main access */ - resizePart(part: Parts, sizeChange: number): void; + resizePart(part: Parts, sizeChangeWidth: number, sizeChangeHeight: number): void; /** * Register a part to participate in the layout. diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index af642f25aee..d9e5aed88ce 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -3,16 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { AbstractLifecycleService } from 'vs/platform/lifecycle/common/lifecycleService'; +import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { localize } from 'vs/nls'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { addDisposableListener } from 'vs/base/browser/dom'; export class BrowserLifecycleService extends AbstractLifecycleService { declare readonly _serviceBrand: undefined; + private beforeUnloadDisposable: IDisposable | undefined = undefined; + constructor( @ILogService readonly logService: ILogService ) { @@ -22,32 +26,54 @@ export class BrowserLifecycleService extends AbstractLifecycleService { } private registerListeners(): void { - window.addEventListener('beforeunload', e => this.onBeforeUnload(e)); + + // beforeUnload + this.beforeUnloadDisposable = addDisposableListener(window, 'beforeunload', (e: BeforeUnloadEvent) => this.onBeforeUnload(e)); } private onBeforeUnload(event: BeforeUnloadEvent): void { + this.logService.info('[lifecycle] onBeforeUnload triggered'); + + this.doShutdown(() => { + // Veto handling + event.preventDefault(); + event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again."); + }); + } + + shutdown(): void { + this.logService.info('[lifecycle] shutdown triggered'); + + // Remove beforeunload listener that would prevent shutdown + this.beforeUnloadDisposable?.dispose(); + + // Handle shutdown without veto support + this.doShutdown(); + } + + private doShutdown(handleVeto?: () => void): void { const logService = this.logService; - logService.info('[lifecycle] onBeforeUnload triggered'); let veto = false; // Before Shutdown this._onBeforeShutdown.fire({ veto(value) { - if (value === true) { - veto = true; - } else if (value instanceof Promise && !veto) { - logService.error('[lifecycle] Long running onBeforeShutdown currently not supported in the web'); - veto = true; + if (typeof handleVeto === 'function') { + if (value === true) { + veto = true; + } else if (value instanceof Promise && !veto) { + logService.error('[lifecycle] Long running onBeforeShutdown currently not supported in the web'); + veto = true; + } } }, reason: ShutdownReason.QUIT }); - // Veto: signal back to browser by returning a non-falsify return value - if (veto) { - event.preventDefault(); - event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again."); + // Veto: handle if provided + if (veto && typeof handleVeto === 'function') { + handleVeto(); return; } diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts new file mode 100644 index 00000000000..876edba7db3 --- /dev/null +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ILifecycleService = createDecorator('lifecycleService'); + +/** + * An event that is send out when the window is about to close. Clients have a chance to veto + * the closing by either calling veto with a boolean "true" directly or with a promise that + * resolves to a boolean. Returning a promise is useful in cases of long running operations + * on shutdown. + * + * Note: It is absolutely important to avoid long running promises if possible. Please try hard + * to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence! + */ +export interface BeforeShutdownEvent { + + /** + * Allows to veto the shutdown. The veto can be a long running operation but it + * will block the application from closing. + */ + veto(value: boolean | Promise): void; + + /** + * The reason why the application will be shutting down. + */ + readonly reason: ShutdownReason; +} + +/** + * An event that is send out when the window closes. Clients have a chance to join the closing + * by providing a promise from the join method. Returning a promise is useful in cases of long + * running operations on shutdown. + * + * Note: It is absolutely important to avoid long running promises if possible. Please try hard + * to return a boolean directly. Returning a promise has quite an impact on the shutdown sequence! + */ +export interface WillShutdownEvent { + + /** + * Allows to join the shutdown. The promise can be a long running operation but it + * will block the application from closing. + */ + join(promise: Promise): void; + + /** + * The reason why the application is shutting down. + */ + readonly reason: ShutdownReason; +} + +export const enum ShutdownReason { + + /** Window is closed */ + CLOSE = 1, + + /** Application is quit */ + QUIT = 2, + + /** Window is reloaded */ + RELOAD = 3, + + /** Other configuration loaded into window */ + LOAD = 4 +} + +export const enum StartupKind { + NewWindow = 1, + ReloadedWindow = 3, + ReopenedWindow = 4, +} + +export function StartupKindToString(startupKind: StartupKind): string { + switch (startupKind) { + case StartupKind.NewWindow: return 'NewWindow'; + case StartupKind.ReloadedWindow: return 'ReloadedWindow'; + case StartupKind.ReopenedWindow: return 'ReopenedWindow'; + } +} + +export const enum LifecyclePhase { + + /** + * The first phase signals that we are about to startup getting ready. + */ + Starting = 1, + + /** + * Services are ready and the view is about to restore its state. + */ + Ready = 2, + + /** + * Views, panels and editors have restored. For editors this means, that + * they show their contents fully. + */ + Restored = 3, + + /** + * The last phase after views, panels and editors have restored and + * some time has passed (few seconds). + */ + Eventually = 4 +} + +export function LifecyclePhaseToString(phase: LifecyclePhase) { + switch (phase) { + case LifecyclePhase.Starting: return 'Starting'; + case LifecyclePhase.Ready: return 'Ready'; + case LifecyclePhase.Restored: return 'Restored'; + case LifecyclePhase.Eventually: return 'Eventually'; + } +} + +/** + * A lifecycle service informs about lifecycle events of the + * application, such as shutdown. + */ +export interface ILifecycleService { + + readonly _serviceBrand: undefined; + + /** + * Value indicates how this window got loaded. + */ + readonly startupKind: StartupKind; + + /** + * A flag indicating in what phase of the lifecycle we currently are. + */ + phase: LifecyclePhase; + + /** + * Fired before shutdown happens. Allows listeners to veto against the + * shutdown to prevent it from happening. + * + * The event carries a shutdown reason that indicates how the shutdown was triggered. + */ + readonly onBeforeShutdown: Event; + + /** + * Fired when no client is preventing the shutdown from happening (from onBeforeShutdown). + * Can be used to save UI state even if that is long running through the WillShutdownEvent#join() + * method. + * + * The event carries a shutdown reason that indicates how the shutdown was triggered. + */ + readonly onWillShutdown: Event; + + /** + * Fired when the shutdown is about to happen after long running shutdown operations + * have finished (from onWillShutdown). This is the right place to dispose resources. + */ + readonly onShutdown: Event; + + /** + * Returns a promise that resolves when a certain lifecycle phase + * has started. + */ + when(phase: LifecyclePhase): Promise; + + /** + * Triggers a shutdown of the workbench. Depending on native or web, this can have + * different implementations and behaviour. + * + * **Note:** this should normally not be called. See related methods in `IHostService` + * and `INativeHostService` to close a window or quit the application. + */ + shutdown(): void; +} + +export const NullLifecycleService: ILifecycleService = { + + _serviceBrand: undefined, + + onBeforeShutdown: Event.None, + onWillShutdown: Event.None, + onShutdown: Event.None, + + phase: LifecyclePhase.Restored, + startupKind: StartupKind.NewWindow, + + async when() { }, + shutdown() { } +}; diff --git a/src/vs/platform/lifecycle/common/lifecycleService.ts b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts similarity index 94% rename from src/vs/platform/lifecycle/common/lifecycleService.ts rename to src/vs/workbench/services/lifecycle/common/lifecycleService.ts index fed0b6580e5..a7e52e488bc 100644 --- a/src/vs/platform/lifecycle/common/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { Barrier } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ILifecycleService, BeforeShutdownEvent, WillShutdownEvent, StartupKind, LifecyclePhase, LifecyclePhaseToString } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, BeforeShutdownEvent, WillShutdownEvent, StartupKind, LifecyclePhase, LifecyclePhaseToString } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { mark } from 'vs/base/common/performance'; @@ -71,4 +71,9 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi await barrier.wait(); } + + /** + * Subclasses to implement the explicit shutdown method. + */ + abstract shutdown(): void; } diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index 1d92faacfc8..2cfbb367437 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -5,13 +5,14 @@ import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { ShutdownReason, StartupKind, handleVetos, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; +import { ShutdownReason, StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { AbstractLifecycleService } from 'vs/platform/lifecycle/common/lifecycleService'; +import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import Severity from 'vs/base/common/severity'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -156,6 +157,10 @@ export class NativeLifecycleService extends AbstractLifecycleService { onUnexpectedError(error); } + + shutdown(): void { + this.nativeHostService.closeWindow(); + } } registerSingleton(ILifecycleService, NativeLifecycleService); diff --git a/src/vs/workbench/services/log/electron-browser/logService.ts b/src/vs/workbench/services/log/electron-browser/logService.ts index 6d896159197..c230f8920bf 100644 --- a/src/vs/workbench/services/log/electron-browser/logService.ts +++ b/src/vs/workbench/services/log/electron-browser/logService.ts @@ -12,7 +12,7 @@ import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class NativeLogService extends DelegatedLogService { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 597aaf8c80c..481205112be 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -151,7 +151,8 @@ export class ProgressService extends Disposable implements IProgressService { } const statusEntryProperties: IStatusbarEntry = { - text: `$(sync~spin) ${text}`, + text, + showProgress: true, ariaLabel: text, tooltip: title, command: progressCommand diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 8f2b82d7c7a..ec221545d26 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -11,7 +11,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 7d2f38f30d0..8b587b11673 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -83,8 +83,8 @@ export function mapHasTunnelLocalhostOrAllInterfaces(map: Map, h export class TunnelModel extends Disposable { readonly forwarded: Map; readonly detected: Map; - private _onForwardPort: Emitter = new Emitter(); - public onForwardPort: Event = this._onForwardPort.event; + private _onForwardPort: Emitter = new Emitter(); + public onForwardPort: Event = this._onForwardPort.event; private _onClosePort: Emitter<{ host: string, port: number }> = new Emitter(); public onClosePort: Event<{ host: string, port: number }> = this._onClosePort.event; private _onPortName: Emitter<{ host: string, port: number }> = new Emitter(); @@ -214,6 +214,7 @@ export class TunnelModel extends Disposable { closeable: false }); }); + this._onForwardPort.fire(); } registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { diff --git a/src/vs/platform/resource/common/resourceIdentityService.ts b/src/vs/workbench/services/resourceIdentity/common/resourceIdentityService.ts similarity index 100% rename from src/vs/platform/resource/common/resourceIdentityService.ts rename to src/vs/workbench/services/resourceIdentity/common/resourceIdentityService.ts diff --git a/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts b/src/vs/workbench/services/resourceIdentity/node/resourceIdentityServiceImpl.ts similarity index 95% rename from src/vs/platform/resource/node/resourceIdentityServiceImpl.ts rename to src/vs/workbench/services/resourceIdentity/node/resourceIdentityServiceImpl.ts index fe7cd857a5f..6ce05e46225 100644 --- a/src/vs/platform/resource/node/resourceIdentityServiceImpl.ts +++ b/src/vs/workbench/services/resourceIdentity/node/resourceIdentityServiceImpl.ts @@ -8,7 +8,7 @@ import { stat } from 'vs/base/node/pfs'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IResourceIdentityService } from 'vs/workbench/services/resourceIdentity/common/resourceIdentityService'; import { Disposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 14a52e61d68..3d65b4fbaa1 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -63,6 +63,11 @@ export interface IStatusbarEntry { * Whether to show a beak above the status bar entry. */ readonly showBeak?: boolean; + + /** + * Will enable a spinning icon in front of the text to indicate progress. + */ + readonly showProgress?: boolean; } export interface IStatusbarService { diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index e53764eb2ef..f498d7ba281 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -6,7 +6,7 @@ import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class BrowserTextFileService extends AbstractTextFileService { @@ -19,7 +19,7 @@ export class BrowserTextFileService extends AbstractTextFileService { protected onBeforeShutdown(reason: ShutdownReason): boolean { if (this.files.models.some(model => model.hasState(TextFileEditorModelState.PENDING_SAVE))) { - console.warn('Unload prevented: pending file saves'); + console.warn('Unload veto: pending file saves'); return true; // files are pending to be saved: veto } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 83974e2c424..39a25cb2b11 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding, stringToSnapshot, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, IFileStreamContent } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 9e84791aaf5..7899cdab6da 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelLoadOrCreateOptions, ITextFileLoadEvent, ITextFileSaveEvent, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; import { IFileService, FileChangesEvent, FileOperation, FileChangeType } from 'vs/platform/files/common/files'; diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index f7980b5848a..d1f83007990 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -16,7 +16,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { UTF8, UTF8_with_bom } from 'vs/workbench/services/textfile/common/encoding'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index e5fe74b3a85..63c98d57c14 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -14,9 +14,10 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { asCSSUrl } from 'vs/base/browser/dom'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -const PERSISTED_FILE_ICON_THEME_STORAGE_KEY = 'iconThemeData'; - export class FileIconThemeData implements IWorkbenchFileIconTheme { + + static readonly STORAGE_KEY = 'iconThemeData'; + id: string; label: string; settingsId: string | null; @@ -108,7 +109,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { static fromStorageData(storageService: IStorageService): FileIconThemeData | undefined { - const input = storageService.get(PERSISTED_FILE_ICON_THEME_STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(FileIconThemeData.STORAGE_KEY, StorageScope.GLOBAL); if (!input) { return undefined; } @@ -129,7 +130,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { (theme as any)[key] = data[key]; break; case 'location': - theme.location = URI.revive(data.location); + // ignore, no longer restore break; case 'extensionData': theme.extensionData = ExtensionData.fromJSONObject(data.extensionData); @@ -148,7 +149,6 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { label: this.label, description: this.description, settingsId: this.settingsId, - location: this.location?.toJSON(), styleSheetContent: this.styleSheetContent, hasFileIcons: this.hasFileIcons, hasFolderIcons: this.hasFolderIcons, @@ -156,7 +156,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { extensionData: ExtensionData.toJSONObject(this.extensionData), watch: this.watch }); - storageService.store(PERSISTED_FILE_ICON_THEME_STORAGE_KEY, data, StorageScope.GLOBAL); + storageService.store(FileIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 617107fac7e..d97484ff14e 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -20,11 +20,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -const PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY = 'productIconThemeData'; - export const DEFAULT_PRODUCT_ICON_THEME_ID = ''; // TODO export class ProductIconThemeData implements IWorkbenchProductIconTheme { + + static readonly STORAGE_KEY = 'productIconThemeData'; + id: string; label: string; settingsId: string; @@ -104,7 +105,7 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { } static fromStorageData(storageService: IStorageService): ProductIconThemeData | undefined { - const input = storageService.get(PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(ProductIconThemeData.STORAGE_KEY, StorageScope.GLOBAL); if (!input) { return undefined; } @@ -122,7 +123,7 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { (theme as any)[key] = data[key]; break; case 'location': - theme.location = URI.revive(data.location); + // ignore, no longer restore break; case 'extensionData': theme.extensionData = ExtensionData.fromJSONObject(data.extensionData); @@ -141,12 +142,11 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { label: this.label, description: this.description, settingsId: this.settingsId, - location: this.location?.toJSON(), styleSheetContent: this.styleSheetContent, watch: this.watch, extensionData: ExtensionData.toJSONObject(this.extensionData), }); - storageService.store(PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY, data, StorageScope.GLOBAL); + storageService.store(ProductIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index eb96d29d5cd..32459f57524 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -18,7 +18,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { FileIconThemeData } from 'vs/workbench/services/themes/browser/fileIconThemeData'; -import { removeClasses, addClasses, createStyleSheet } from 'vs/base/browser/dom'; +import { createStyleSheet } from 'vs/base/browser/dom'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; @@ -38,6 +38,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService'; import { CodiconStyles } from 'vs/base/browser/ui/codicons/codiconStyles'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; // implementation @@ -108,7 +109,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService, @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { + // roam persisted color theme colors. Don't enable for icons as they contain references to fonts and images. + storageKeysSyncRegistryService.registerStorageKey({ key: ColorThemeData.STORAGE_KEY, version: 1 }); + this.container = layoutService.container; this.settings = new ThemeConfiguration(configurationService); @@ -456,11 +461,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.updateDynamicCSSRules(newTheme); if (this.currentColorTheme.id) { - removeClasses(this.container, this.currentColorTheme.id); + this.container.classList.remove(...this.currentColorTheme.classNames); } else { - removeClasses(this.container, VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME); + this.container.classList.remove(VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME); } - addClasses(this.container, newTheme.id); + this.container.classList.add(...newTheme.classNames); this.currentColorTheme.clearCaches(); this.currentColorTheme = newTheme; @@ -575,9 +580,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { _applyRules(iconThemeData.styleSheetContent!, fileIconThemeRulesClassName); if (iconThemeData.id) { - addClasses(this.container, fileIconsEnabledClass); + this.container.classList.add(fileIconsEnabledClass); } else { - removeClasses(this.container, fileIconsEnabledClass); + this.container.classList.remove(fileIconsEnabledClass); } this.fileIconThemeWatcher.update(iconThemeData); diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 6b9fa724b74..ec49eb71145 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -10,8 +10,6 @@ import { ExtensionData, ITokenColorCustomizations, ITextMateThemingRule, IWorkbe import { convertSettings } from 'vs/workbench/services/themes/common/themeCompatibility'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; -import * as objects from 'vs/base/common/objects'; -import * as arrays from 'vs/base/common/arrays'; import * as resources from 'vs/base/common/resources'; import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ITokenStyle, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; @@ -47,16 +45,16 @@ export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDe export type TextMateThemingRuleDefinitions = { [P in keyof TokenStyleData]?: ITextMateThemingRule | undefined; } & { scope?: ProbeScope; }; -const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; - export class ColorThemeData implements IWorkbenchColorTheme { + static readonly STORAGE_KEY = 'colorThemeData'; + id: string; label: string; settingsId: string; description?: string; isLoaded: boolean; - location?: URI; + location?: URI; // only set for extension from the registry, not for themes restored from the storage watch?: boolean; extensionData?: ExtensionData; @@ -517,27 +515,23 @@ export class ColorThemeData implements IWorkbenchColorTheme { id: this.id, label: this.label, settingsId: this.settingsId, - selector: this.id.split(' ').join('.'), // to not break old clients - themeTokenColors: this.themeTokenColors, + themeTokenColors: this.themeTokenColors.map(tc => ({ settings: tc.settings, scope: tc.scope })), // don't pesist names semanticTokenRules: this.semanticTokenRules.map(SemanticTokenRule.toJSONObject), extensionData: ExtensionData.toJSONObject(this.extensionData), - location: this.location?.toJSON(), themeSemanticHighlighting: this.themeSemanticHighlighting, colorMap: colorMapData, watch: this.watch }); - storageService.store(PERSISTED_THEME_STORAGE_KEY, value, StorageScope.GLOBAL); - } - hasEqualData(other: ColorThemeData) { - return objects.equals(this.colorMap, other.colorMap) - && objects.equals(this.themeTokenColors, other.themeTokenColors) - && arrays.equals(this.semanticTokenRules, other.semanticTokenRules, SemanticTokenRule.equals) - && this.themeSemanticHighlighting === other.themeSemanticHighlighting; + storageService.store(ColorThemeData.STORAGE_KEY, value, StorageScope.GLOBAL); } get baseTheme(): string { - return this.id.split(' ')[0]; + return this.classNames[0]; + } + + get classNames(): string[] { + return this.id.split(' '); } get type(): ColorScheme { @@ -576,7 +570,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } static fromStorageData(storageService: IStorageService): ColorThemeData | undefined { - const input = storageService.get(PERSISTED_THEME_STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(ColorThemeData.STORAGE_KEY, StorageScope.GLOBAL); if (!input) { return undefined; } @@ -607,7 +601,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } break; case 'location': - theme.location = URI.revive(data.location); + // ignore, no longer restore break; case 'extensionData': theme.extensionData = ExtensionData.fromJSONObject(data.extensionData); diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 62c3b7abc60..c7a52fbeb08 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -39,8 +39,8 @@ const colorThemeSettingSchema: IConfigurationPropertySchema = { errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = { - type: 'string', - markdownDescription: nls.localize('preferredDarkColorTheme', 'Specifies the preferred color theme for dark OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME), + type: 'string', // + markdownDescription: nls.localize({ key: 'preferredDarkColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme for dark OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME), default: DEFAULT_THEME_DARK_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, @@ -48,7 +48,7 @@ const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = { }; const preferredLightThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', - markdownDescription: nls.localize('preferredLightColorTheme', 'Specifies the preferred color theme for light OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME), + markdownDescription: nls.localize({ key: 'preferredLightColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme for light OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME), default: DEFAULT_THEME_LIGHT_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, @@ -56,7 +56,7 @@ const preferredLightThemeSettingSchema: IConfigurationPropertySchema = { }; const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', - markdownDescription: nls.localize('preferredHCColorTheme', 'Specifies the preferred color theme used in high contrast mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC), + markdownDescription: nls.localize({ key: 'preferredHCColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme used in high contrast mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC), default: DEFAULT_THEME_HC_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index 20e496a4fe3..b3aa420553d 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -113,7 +113,7 @@ export interface ThemeChangeEvent { export interface IThemeData { id: string; settingsId: string | null; - extensionData?: ExtensionData; + location?: URI; } export class ThemeRegistry { @@ -152,10 +152,9 @@ export class ThemeRegistry { extensionId: ext.description.identifier.value, extensionPublisher: ext.description.publisher, extensionName: ext.description.name, - extensionIsBuiltin: ext.description.isBuiltin, - extensionLocation: ext.description.extensionLocation + extensionIsBuiltin: ext.description.isBuiltin }; - this.onThemes(extensionData, ext.value, ext.collector); + this.onThemes(extensionData, ext.description.extensionLocation, ext.value, ext.collector); } for (const theme of this.extensionThemes) { if (!previousIds[theme.id]) { @@ -169,7 +168,7 @@ export class ThemeRegistry { }); } - private onThemes(extensionData: ExtensionData, themes: IThemeExtensionPoint[], collector: ExtensionMessageCollector): void { + private onThemes(extensionData: ExtensionData, extensionLocation: URI, themes: IThemeExtensionPoint[], collector: ExtensionMessageCollector): void { if (!Array.isArray(themes)) { collector.error(nls.localize( 'reqarray', @@ -198,9 +197,9 @@ export class ThemeRegistry { return; } - const themeLocation = resources.joinPath(extensionData.extensionLocation, theme.path); - if (!resources.isEqualOrParent(themeLocation, extensionData.extensionLocation)) { - collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", this.themesExtPoint.name, themeLocation.path, extensionData.extensionLocation.path)); + const themeLocation = resources.joinPath(extensionLocation, theme.path); + if (!resources.isEqualOrParent(themeLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", this.themesExtPoint.name, themeLocation.path, extensionLocation.path)); } let themeData = this.create(theme, themeLocation, extensionData); @@ -245,7 +244,7 @@ export class ThemeRegistry { public findThemeByExtensionLocation(extLocation: URI | undefined): Promise { if (extLocation) { return this.getThemes().then(allThemes => { - return allThemes.filter(t => t.extensionData && resources.isEqual(t.extensionData.extensionLocation, extLocation)); + return allThemes.filter(t => t.location && resources.isEqualOrParent(t.location, extLocation)); }); } return Promise.resolve([]); diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 9186681075f..53e7674b1ae 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -8,7 +8,6 @@ import { Event } from 'vs/base/common/event'; import { Color } from 'vs/base/common/color'; import { IColorTheme, IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { URI } from 'vs/base/common/uri'; import { isBoolean, isString } from 'vs/base/common/types'; export const IWorkbenchThemeService = createDecorator('themeService'); @@ -135,16 +134,15 @@ export interface ExtensionData { extensionPublisher: string; extensionName: string; extensionIsBuiltin: boolean; - extensionLocation: URI; } export namespace ExtensionData { export function toJSONObject(d: ExtensionData | undefined): any { - return d && { _extensionId: d.extensionId, _extensionIsBuiltin: d.extensionIsBuiltin, _extensionLocation: d.extensionLocation.toJSON(), _extensionName: d.extensionName, _extensionPublisher: d.extensionPublisher }; + return d && { _extensionId: d.extensionId, _extensionIsBuiltin: d.extensionIsBuiltin, _extensionName: d.extensionName, _extensionPublisher: d.extensionPublisher }; } export function fromJSONObject(o: any): ExtensionData | undefined { - if (o && isString(o._extensionId) && isBoolean(o._extensionIsBuiltin) && isString(o._extensionLocation) && isString(o._extensionName) && isString(o._extensionPublisher)) { - return { extensionId: o._extensionId, extensionIsBuiltin: o._extensionIsBuiltin, extensionLocation: URI.revive(o._extensionLocation), extensionName: o._extensionName, extensionPublisher: o._extensionPublisher }; + if (o && isString(o._extensionId) && isBoolean(o._extensionIsBuiltin) && isString(o._extensionName) && isString(o._extensionPublisher)) { + return { extensionId: o._extensionId, extensionIsBuiltin: o._extensionIsBuiltin, extensionName: o._extensionName, extensionPublisher: o._extensionPublisher }; } return undefined; } diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index 65126d9699e..cab6c54f0b9 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -8,7 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUpdateService } from 'vs/platform/update/common/update'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/services/timer/electron-sandbox/timerService.ts b/src/vs/workbench/services/timer/electron-sandbox/timerService.ts index fd16b7da21c..1192fb67378 100644 --- a/src/vs/workbench/services/timer/electron-sandbox/timerService.ts +++ b/src/vs/workbench/services/timer/electron-sandbox/timerService.ts @@ -8,7 +8,7 @@ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/enviro import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUpdateService } from 'vs/platform/update/common/update'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index f15d240c489..0f876c6daeb 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -21,7 +21,7 @@ import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authe import { getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; export const IUserDataInitializationService = createDecorator('IUserDataInitializationService'); diff --git a/src/vs/workbench/services/userData/common/fileUserDataProvider.ts b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts index 91158913e25..0baaa4afde3 100644 --- a/src/vs/workbench/services/userData/common/fileUserDataProvider.ts +++ b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts @@ -29,6 +29,11 @@ export class FileUserDataProvider extends Disposable implements private extUri: ExtUri; constructor( + /* + Original userdata and backup home locations. Used to + - listen to changes and trigger change events + - Compute UserData URIs from original URIs and vice-versa + */ private readonly fileSystemUserDataHome: URI, private readonly fileSystemBackupsHome: URI | undefined, private readonly fileSystemProvider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, diff --git a/src/vs/workbench/services/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/browser/userDataAutoSyncEnablementService.ts similarity index 79% rename from src/vs/workbench/services/userDataSync/browser/userDataAutoSyncService.ts rename to src/vs/workbench/services/userDataSync/browser/userDataAutoSyncEnablementService.ts index 50f385e3269..d0e5a506853 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataAutoSyncEnablementService.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -export class WebUserDataAutoSyncService extends UserDataAutoSyncService implements IUserDataAutoSyncService { +export class WebUserDataAutoSyncEnablementService extends UserDataAutoSyncEnablementService { private get workbenchEnvironmentService(): IWorkbenchEnvironmentService { return this.environmentService; } private enabled: boolean | undefined = undefined; @@ -22,7 +21,7 @@ export class WebUserDataAutoSyncService extends UserDataAutoSyncService implemen return this.enabled; } - protected setEnablement(enabled: boolean) { + setEnablement(enabled: boolean) { if (this.enabled !== enabled) { this.enabled = enabled; if (this.workbenchEnvironmentService.options?.settingsSyncOptions) { diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 720d17e3b0a..f6d93e4f9eb 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, IUserDataSyncStoreManagementService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, IUserDataSyncStoreManagementService, SyncStatus, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync'; @@ -29,7 +29,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -93,6 +93,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IStorageService private readonly storageService: IStorageService, + @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @@ -118,8 +119,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat if (this.userDataSyncStoreManagementService.userDataSyncStore) { this.syncStatusContext.set(this.userDataSyncService.status); this._register(userDataSyncService.onDidChangeStatus(status => this.syncStatusContext.set(status))); - this.syncEnablementContext.set(userDataAutoSyncService.isEnabled()); - this._register(userDataAutoSyncService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled))); + this.syncEnablementContext.set(userDataAutoSyncEnablementService.isEnabled()); + this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled))); this.waitAndInitialize(); } @@ -247,7 +248,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat if (!this.authenticationProviders.length) { throw new Error(localize('no authentication providers', "Settings sync cannot be turned on because there are no authentication providers available.")); } - if (this.userDataAutoSyncService.isEnabled()) { + if (this.userDataAutoSyncEnablementService.isEnabled()) { return; } if (this.userDataSyncService.status !== SyncStatus.Idle) { @@ -552,7 +553,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private async switch(sessionId: string, accountName: string, accountId: string): Promise { const currentAccount = this.current; - if (this.userDataAutoSyncService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) { + if (this.userDataAutoSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) { // accounts are switched while sync is enabled. } this.currentSessionId = sessionId; @@ -565,7 +566,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.currentSessionId = undefined; await this.update(); - if (this.userDataAutoSyncService.isEnabled()) { + if (this.userDataAutoSyncEnablementService.isEnabled()) { this.notificationService.notify({ severity: Severity.Error, message: localize('successive auth failures', "Settings sync is suspended because of successive authorization failures. Please sign in again to continue synchronizing"), diff --git a/src/vs/workbench/services/userDataSync/electron-sandbox/storageKeysSyncRegistryService.ts b/src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts similarity index 85% rename from src/vs/workbench/services/userDataSync/electron-sandbox/storageKeysSyncRegistryService.ts rename to src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts index 12287d95d05..578252aa973 100644 --- a/src/vs/workbench/services/userDataSync/electron-sandbox/storageKeysSyncRegistryService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService.ts @@ -5,13 +5,13 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; class StorageKeysSyncRegistryService extends StorageKeysSyncRegistryChannelClient implements IStorageKeysSyncRegistryService { constructor( - @IMainProcessService mainProcessService: IMainProcessService + @ISharedProcessService mainProcessService: ISharedProcessService ) { super(mainProcessService.getChannel('storageKeysSyncRegistryService')); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts index d48dbbdab49..7e381a0446b 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -3,16 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataAutoSyncService, UserDataSyncError, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { +class UserDataAutoSyncService implements IUserDataAutoSyncService { declare readonly _serviceBrand: undefined; @@ -20,12 +17,8 @@ class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implemen get onError(): Event { return Event.map(this.channel.listen('onError'), e => UserDataSyncError.toUserDataSyncError(e)); } constructor( - @IStorageService storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService, - @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); } diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index 6a4cf6d99e5..9f6279a0d89 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -18,7 +18,7 @@ import { basename } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; 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 a06dce40d02..2059826812e 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -11,7 +11,7 @@ import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestSer 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'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorModel } from 'vs/platform/editor/common/editor'; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 3527de197e4..b59d073c566 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -21,7 +21,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceEditorInput, IEditorModel, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { FileOperationEvent, IFileService, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -460,7 +460,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { toggleZenMode(): void { } isEditorLayoutCentered(): boolean { return false; } centerEditorLayout(_active: boolean): void { } - resizePart(_part: Parts, _sizeChange: number): void { } + resizePart(_part: Parts, _sizeChangeWidth: number, _sizeChangeHeight: number): void { } registerPart(part: Part): void { } isWindowMaximized() { return false; } updateWindowMaximizedState(maximized: boolean): void { } @@ -932,6 +932,10 @@ export class TestLifecycleService implements ILifecycleService { } fireWillShutdown(event: BeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); } + + shutdown(): void { + this.fireShutdown(); + } } export class TestTextResourceConfigurationService implements ITextResourceConfigurationService { @@ -1042,6 +1046,7 @@ export class TestHostService implements IHostService { async restart(): Promise { } async reload(): Promise { } + async close(): Promise { } async focus(options?: { force: boolean }): Promise { } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index ed43a453f08..950219dd483 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -11,7 +11,7 @@ import { NativeTextFileService, } from 'vs/workbench/services/textfile/electron- import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { FileOperationError, IFileService } from 'vs/platform/files/common/files'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 95a13d61a16..09ad97cd729 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -117,8 +117,10 @@ import { OpenerService } from 'vs/editor/browser/services/openerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; +import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; registerSingleton(IUserDataSyncResourceEnablementService, UserDataSyncResourceEnablementService); +registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService); registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService); registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); registerSingleton(IContextViewService, ContextViewService, true); @@ -295,4 +297,7 @@ import 'vs/workbench/contrib/welcome/common/viewsWelcome.contribution'; // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; +// Workspaces +import 'vs/workbench/contrib/workspaces/browser/workspaces.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 5f08dc5d96d..cd911e3e399 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -75,6 +75,7 @@ import 'vs/workbench/services/extensionManagement/electron-browser/extensionTips import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import 'vs/workbench/services/telemetry/electron-browser/telemetryService'; import 'vs/workbench/services/backup/node/backupFileService'; +import 'vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService'; diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index aa3f59f402a..cb86008e491 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -16,13 +16,17 @@ import 'vs/workbench/workbench.common.main'; //#endregion +import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; + +registerSingleton(IUserDataAutoSyncEnablementService, UserDataAutoSyncEnablementService); + //#region --- workbench services import 'vs/workbench/services/dialogs/electron-sandbox/fileDialogService'; import 'vs/workbench/services/workspaces/electron-sandbox/workspacesService'; import 'vs/workbench/services/textMate/electron-sandbox/textMateService'; -import 'vs/workbench/services/userDataSync/electron-sandbox/storageKeysSyncRegistryService'; import 'vs/workbench/services/menubar/electron-sandbox/menubarService'; import 'vs/workbench/services/dialogs/electron-sandbox/dialogService'; import 'vs/workbench/services/issue/electron-sandbox/issueService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index b21da0e7956..68906ff6fa7 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -13,7 +13,7 @@ import { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlServi import { LogLevel } from 'vs/platform/log/common/log'; import { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/browser/updateService'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IProductConfiguration } from 'vs/platform/product/common/productService'; @@ -277,6 +277,11 @@ interface IWorkbenchConstructionOptions { */ readonly webviewEndpoint?: string; + /** + * An URL pointing to the web worker extension host