diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b32dd7f64e2..cd632e134ef 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,14 +15,12 @@ "resmon.show.cpufreq": false }, - // noVNC, VNC ports, debug + // noVNC, VNC, debug ports "forwardPorts": [6080, 5901, 9222], "extensions": [ "dbaeumer.vscode-eslint", - "EditorConfig.EditorConfig", - "mutantdino.resourcemonitor", - "GitHub.vscode-pull-request-github" + "mutantdino.resourcemonitor" ], // Optionally loads a cached yarn install for the repo 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/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/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 270beb898a0..031a491648a 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -157,7 +157,7 @@ steps: inputs: testResultsFiles: '*-results.xml' searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e 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 9f616275b11..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" @@ -41,9 +55,9 @@ integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= "@types/node@^12.11.7": - version "12.12.67" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" - integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== + version "12.12.68" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.68.tgz#dd5acf4a52a458ff1d9ef4fd66406fba0afbbb33" + integrity sha512-3RW2s24ewB7F9dAHvgb9FRvNHn6nO9IK6Eaknbz7HTOe2a5GVne5XbUh5+YA+kcCn67glyHhClUUdFP73LWrgQ== ajv@^6.12.3: version "6.12.6" @@ -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.4" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.4.tgz#d088bd4b2d52917d69e43df7a1989ceb06958eb2" - integrity sha512-g5yf6RnhGsCymg2YOK2HoK5hyBphB6b5bCEqF/3YwLTMO+9epZnSa6qSzAzz3U68y7IOlmW7ssFP5kOjybcA9g== + 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 f2fa3baf958..0e08b6a84dc 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -623,6 +623,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" @@ -1637,6 +1641,7 @@ "null" ], "default": null, + "scope": "machine", "description": "%config.defaultCloneDirectory%" }, "git.enableSmartCommit": { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index fb66583c3d4..99068f285cc 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2498,7 +2498,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 48e5c457c22..d3c574c74b1 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1233,7 +1233,7 @@ export class Repository { } if (paths && paths.length) { - for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { await this.run([...args, '--', ...chunk]); } } else { @@ -1455,7 +1455,7 @@ export class Repository { const args = ['clean', '-f', '-q']; for (const paths of groups) { - for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { promises.push(limiter.queue(() => this.run([...args, '--', ...chunk]))); } } @@ -1495,7 +1495,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { await this.run([...args, '--', ...chunk]); } } else { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 5250ab4920a..80d185a8360 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'; @@ -642,6 +642,7 @@ export class Repository implements Disposable { } this._rebaseCommit = rebaseCommit; + commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit); } get rebaseCommit(): Commit | undefined { @@ -1786,6 +1787,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/markdown-language-features/src/features/foldingProvider.ts b/extensions/markdown-language-features/src/features/foldingProvider.ts index 590a7a1b246..553c1a3257b 100644 --- a/extensions/markdown-language-features/src/features/foldingProvider.ts +++ b/extensions/markdown-language-features/src/features/foldingProvider.ts @@ -11,12 +11,6 @@ import { flatten } from '../util/arrays'; const rangeLimit = 5000; -const isStartRegion = (t: string) => /^\s*/.test(t); -const isEndRegion = (t: string) => /^\s*/.test(t); - -const isRegionMarker = (token: Token) => - token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content)); - export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider { constructor( @@ -69,24 +63,6 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi } private async getBlockFoldingRanges(document: vscode.TextDocument): Promise { - - const isFoldableToken = (token: Token): boolean => { - switch (token.type) { - case 'fence': - case 'list_item_open': - return token.map[1] > token.map[0]; - - case 'html_block': - if (isRegionMarker(token)) { - return false; - } - return token.map[1] > token.map[0] + 1; - - default: - return false; - } - }; - const tokens = await this.engine.parse(document); const multiLineListItems = tokens.filter(isFoldableToken); return multiLineListItems.map(listItem => { @@ -95,7 +71,36 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) { end = end - 1; } - return new vscode.FoldingRange(start, end, listItem.type === 'html_block' && listItem.content.startsWith('/.test(t); +const isEndRegion = (t: string) => /^\s*/.test(t); + +const isRegionMarker = (token: Token) => + token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content)); + +const isFoldableToken = (token: Token): boolean => { + switch (token.type) { + case 'fence': + case 'list_item_open': + return token.map[1] > token.map[0]; + + case 'html_block': + if (isRegionMarker(token)) { + return false; + } + return token.map[1] > token.map[0] + 1; + + default: + return false; + } +}; diff --git a/extensions/markdown-language-features/src/tableOfContentsProvider.ts b/extensions/markdown-language-features/src/tableOfContentsProvider.ts index 6a3a5b62867..3e1de6f6779 100644 --- a/extensions/markdown-language-features/src/tableOfContentsProvider.ts +++ b/extensions/markdown-language-features/src/tableOfContentsProvider.ts @@ -57,19 +57,19 @@ export class TableOfContentsProvider { const toc: TocEntry[] = []; const tokens = await this.engine.parse(document); - const slugCount = new Map(); + const existingSlugEntries = new Map(); for (const heading of tokens.filter(token => token.type === 'heading_open')) { const lineNumber = heading.map[0]; const line = document.lineAt(lineNumber); let slug = githubSlugifier.fromHeading(line.text); - if (slugCount.has(slug.value)) { - const count = slugCount.get(slug.value)!; - slugCount.set(slug.value, count + 1); - slug = githubSlugifier.fromHeading(slug.value + '-' + (count + 1)); + const existingSlugEntry = existingSlugEntries.get(slug.value); + if (existingSlugEntry) { + ++existingSlugEntry.count; + slug = githubSlugifier.fromHeading(slug.value + '-' + existingSlugEntry.count); } else { - slugCount.set(slug.value, 0); + existingSlugEntries.set(slug.value, { count: 0 }); } toc.push({ @@ -91,7 +91,7 @@ export class TableOfContentsProvider { break; } } - const endLine = end !== undefined ? end : document.lineCount - 1; + const endLine = end ?? document.lineCount - 1; return { ...entry, location: new vscode.Location(document.uri, diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index d0872e17e1f..a30d58be4a5 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -359,7 +359,7 @@ export class AzureActiveDirectoryService { case 'online.dev.core.vsengsaas.visualstudio.com': return 'vsodev,'; default: - return ''; + return `${callbackUri.scheme},`; } } 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/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..3b4264f29ae 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -851,6 +851,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/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 5d9e60b7ec5..282a55d8e88 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.51.0", - "distro": "cb22bfd6ccdabc4a7765c63d247de8580923bd4a", + "distro": "5a2a07b357e780fb7130743e24cf814126485ef9", "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", 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/win32/bin/code.sh b/resources/win32/bin/code.sh index 39b904ecd3f..23fbbc9bf20 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -43,7 +43,7 @@ if [ $IN_WSL = true ]; then # use the Remote WSL extension if installed WSL_EXT_ID="ms-vscode-remote.remote-wsl" - ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID >/tmp/remote-wsl-loc.txt 2>/dev/null + ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID >/tmp/remote-wsl-loc.txt 2>/dev/null { 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 393f311c2d9..c1e9d19a013 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -35,33 +35,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 +56,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 { @@ -1018,8 +998,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); @@ -1403,3 +1390,48 @@ 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; + } +} 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/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index cb03f5f2df7..be71339754f 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -949,16 +949,18 @@ export class MenuBar extends Disposable { customMenu.buttonElement.classList.add('open'); + const buttonBoundingRect = customMenu.buttonElement.getBoundingClientRect(); + if (this.options.compactMode === Direction.Right) { - menuHolder.style.top = `0px`; - menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left + this.container.clientWidth}px`; + menuHolder.style.top = `${buttonBoundingRect.top}px`; + menuHolder.style.left = `${buttonBoundingRect.left + this.container.clientWidth}px`; } else if (this.options.compactMode === Direction.Left) { - menuHolder.style.top = `0px`; + menuHolder.style.top = `${buttonBoundingRect.top}px`; menuHolder.style.right = `${this.container.clientWidth}px`; menuHolder.style.left = 'auto'; } else { menuHolder.style.top = `${this.container.clientHeight}px`; - menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`; + menuHolder.style.left = `${buttonBoundingRect.left}px`; } customMenu.buttonElement.appendChild(menuHolder); 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/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/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 45c871d9aa9..f6389d25d26 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -48,7 +48,7 @@ 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'; @@ -70,6 +70,7 @@ import { IExtensionRecommendationNotificationService } from 'vs/platform/extensi 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'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -212,6 +213,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat 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/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index dcbb98b473f..aaf9feaf3ed 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -129,8 +129,8 @@ export abstract class Command { command: { id: this.id, title: item.title, - icon: item.icon - // precondition: this.precondition + icon: item.icon, + precondition: this.precondition }, when: item.when, order: item.order diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index faad9ab6da5..34f041ea626 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1293,6 +1293,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/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 09e4fa0e159..b65d6029cfd 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: undefined, + precondition: 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: undefined, + precondition: 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/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/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/monaco.d.ts b/src/vs/monaco.d.ts index 1d51626cd4e..12faeaf905d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6149,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/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/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/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/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..bc8a0cbbca5 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,7 @@ 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'; export class UserDataSyncClient extends Disposable { @@ -115,6 +116,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/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index 77b451c9005..a019d267e69 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -7,7 +7,7 @@ import { protocol, session } from 'electron'; import { Readable } from 'stream'; import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; +import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -130,13 +130,12 @@ export class WebviewProtocolProvider extends Disposable { const uri = URI.parse(request.url); const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); if (typeof entry === 'string') { - let url: string; - if (uri.path.startsWith('/electron-browser')) { - url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`); - } else { - url = require.toUrl(`vs/workbench/contrib/webview/browser/pre/${entry}`); - } - return callback(decodeURIComponent(url.replace(`${Schemas.file}://`, ''))); + const relativeResourcePath = uri.path.startsWith('/electron-browser') + ? `vs/workbench/contrib/webview/electron-browser/pre/${entry}` + : `vs/workbench/contrib/webview/browser/pre/${entry}`; + + const url = FileAccess.asFileUri(relativeResourcePath, require); + return callback(decodeURIComponent(url.fsPath)); } } catch { // noop diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 1168fb0a790..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); } /** @@ -5659,7 +5670,7 @@ declare module 'vscode' { * Get the absolute path of a resource contained in the extension. * * *Note* that an absolute uri can be constructed via [`Uri.joinPath`](#Uri.joinPath) and - * [`extensionUri`](#ExtensionContent.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` + * [`extensionUri`](#ExtensionContext.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. @@ -5688,7 +5699,7 @@ declare module 'vscode' { * Use [`workspaceState`](#ExtensionContext.workspaceState) or * [`globalState`](#ExtensionContext.globalState) to store key value data. * - * @deprecated Use [storagePath](#ExtensionContent.storageUri) instead. + * @deprecated Use [storageUri](#ExtensionContext.storageUri) instead. */ readonly storagePath: string | undefined; @@ -5711,7 +5722,7 @@ declare module 'vscode' { * * Use [`globalState`](#ExtensionContext.globalState) to store key value data. * - * @deprecated Use [globalStoragePath](#ExtensionContent.globalStorageUri) instead. + * @deprecated Use [globalStorageUri](#ExtensionContext.globalStorageUri) instead. */ readonly globalStoragePath: string; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5a3de044162..3b0258815ff 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -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/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 26d98c78511..410b6b061dc 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -567,7 +567,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } } - public async backup(): Promise { + public async backup(token: CancellationToken): Promise { const editors = this._getEditors(); if (!editors.length) { throw new Error('No editors found for resource, cannot back up'); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 635730ba75b..9cbb21d567f 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -571,13 +571,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/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index c5c03a86786..17b4458a107 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))); 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 d43016bcd07..e369b4f088d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -5,8 +5,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +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; @@ -43,12 +52,18 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc webviewView.show(preserveFocus); } - public $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void { + public $registerWebviewViewProvider( + extensionData: extHostProtocol.WebviewExtensionDescription, + viewType: string, + options?: { retainContextWhenHidden?: boolean } + ): void { if (this._webviewViewProviders.has(viewType)) { throw new Error(`View provider for ${viewType} already registered`); } - this._webviewViewService.register(viewType, { + const extension = reviveWebviewExtension(extensionData); + + const registration = this._webviewViewService.register(viewType, { resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { const handle = webviewView.webview.id; @@ -64,6 +79,8 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc } } + webviewView.webview.extension = extension; + if (options) { webviewView.webview.options = options; } @@ -85,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 b7ad89e366b..dd64b66a1ac 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -398,7 +398,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $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; @@ -644,7 +645,7 @@ export interface MainThreadCustomEditorsShape extends IDisposable { } export interface MainThreadWebviewViewsShape extends IDisposable { - $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void; + $registerWebviewViewProvider(extension: WebviewExtensionDescription, viewType: string, options?: { retainContextWhenHidden?: boolean }): void; $unregisterWebviewViewProvider(viewType: string): void; $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ef49bb42954..8e65422a946 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1810,10 +1810,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/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index d722f2b428e..97d30a82f61 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'; @@ -245,8 +245,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..ab6d9a7f6de 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: string } = { iconPath: 'back.svg' }; private constructor() { } } diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index a9f3db59641..205ad24c09e 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostWebview, ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; import * as extHostTypes from './extHostTypes'; @@ -146,7 +146,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS } this._viewProviders.set(viewType, { provider, extension }); - this._proxy.$registerWebviewViewProvider(viewType, webviewOptions); + this._proxy.$registerWebviewViewProvider(toExtensionData(extension), viewType, webviewOptions); return new extHostTypes.Disposable(() => { this._viewProviders.delete(viewType); diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 97c3bdb4838..9eb0d3ea714 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -284,8 +284,9 @@ registerAction2(ToggleScreencastModeAction); registerAction2(LogStorageAction); registerAction2(LogWorkingCopiesAction); +// --- Configuration -// Screencast Mode +// Screen Cast Mode const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ id: 'screencastMode', 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 a53bd1e9a88..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'; @@ -298,6 +298,8 @@ class GoHomeContributor implements IWorkbenchContribution { } } +// --- Actions Registration + const actionsRegistry = Registry.as(Extensions.WorkbenchActions); actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateUpAction, undefined), 'View: Navigate to the View Above', CATEGORIES.View.value); 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 a91096687ae..e5fdcdbca0a 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -32,6 +32,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ResourceMap } from 'vs/base/common/map'; import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -333,6 +335,24 @@ export class NewWindowAction extends Action { } } +class BlurAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.blur', + title: nls.localize('blur', "Remove keyboard focus from focused element") + }); + } + + run(): void { + const el = document.activeElement; + + if (isHTMLElement(el)) { + el.blur(); + } + } +} + const registry = Registry.as(Extensions.WorkbenchActions); // --- Actions Registration @@ -348,6 +368,8 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(ReloadWindowAction), registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAboutDialogAction), `Help: About`, CATEGORIES.Help.value); +registerAction2(BlurAction); + // --- Commands/Keybindings Registration const recentFilesPickerContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(inRecentFilesPickerContextKey)); @@ -372,26 +394,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R } }); -class BlurAction extends Action2 { - - constructor() { - super({ - id: 'workbench.action.blur', - title: nls.localize('blur', "Remove keyboard focus from focused element") - }); - } - - run(): void { - const el = document.activeElement; - - if (isHTMLElement(el)) { - el.blur(); - } - } -} - -registerAction2(BlurAction); - KeybindingsRegistry.registerKeybindingRule({ id: ReloadWindowAction.ID, weight: KeybindingWeight.WorkbenchContrib + 50, @@ -399,8 +401,26 @@ KeybindingsRegistry.registerKeybindingRule({ primary: KeyMod.CtrlCmd | KeyCode.KEY_R }); +CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => { + const configurationService = accessor.get(IConfigurationService); + const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; + + return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER); +}); + // --- Menu Registration +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: 'z_ConfirmClose', + command: { + id: 'workbench.action.toggleConfirmBeforeClose', + title: nls.localize('miConfirmClose', "Confirm Before Close"), + toggled: ContextKeyExpr.notEquals('config.window.confirmBeforeClose', 'never') + }, + order: 1, + when: IsWebContext +}); + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '1_new', command: { 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/layout.ts b/src/vs/workbench/browser/layout.ts index 6618279096d..a3cd99f3e0e 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'; @@ -1397,9 +1397,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/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 5cac87be5a8..e6d4dd3491f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1361,7 +1361,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.handleDirtyClosing(editors); } - private async doHandleDirtyClosing(editor: EditorInput): Promise { + private async doHandleDirtyClosing(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise { if (!editor.isDirty() || editor.isSaving()) { return false; // editor must be dirty and not saving } @@ -1396,9 +1396,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Auto-save on focus change: assume to Save unless the editor is untitled // because bringing up a dialog would save in this case anyway. + // However, make sure to respect `skipAutoSave` option in case the automated + // save fails which would result in the editor never closing + // (see https://github.com/microsoft/vscode/issues/108752) let confirmation: ConfirmResult; let saveReason = SaveReason.EXPLICIT; - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled()) { + let autoSave = false; + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.isUntitled() && !options?.skipAutoSave) { + autoSave = true; confirmation = ConfirmResult.SAVE; saveReason = SaveReason.FOCUS_CHANGE; } @@ -1430,7 +1435,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Otherwise, handle accordingly switch (confirmation) { case ConfirmResult.SAVE: - await editor.save(this.id, { reason: saveReason }); + const result = await editor.save(this.id, { reason: saveReason }); + if (!result && autoSave) { + // Save failed and we need to signal this back to the user, so + // we handle the dirty editor again but this time ensuring to + // show the confirm dialog + // (see https://github.com/microsoft/vscode/issues/108752) + return this.doHandleDirtyClosing(editor, { skipAutoSave: true }); + } return editor.isDirty(); // veto if still dirty case ConfirmResult.DONT_SAVE: 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 0c108f0c9d6..ef15d2e2131 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; @@ -1631,7 +1632,10 @@ 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 + // - on Safari (https://github.com/microsoft/vscode/issues/108996) + if (theme.type !== 'hc' && !isSafari) { 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/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index fc00d291436..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'; 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/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/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 d946123195e..255f9cac587 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -8,6 +8,7 @@ import * as nls from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { isStandalone } from 'vs/base/browser/browser'; // Configuration (function registerConfiguration(): void { @@ -402,10 +403,16 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'scope': ConfigurationScope.APPLICATION, '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.confirmBeforeQuit': { - 'type': 'boolean', - 'default': isWeb, - 'description': nls.localize('confirmBeforeQuitWeb', "Controls whether to ask for confirmation before closing the browser tab or window."), + 'window.confirmBeforeClose': { + '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 743585520e0..e28ef9cd9a3 100644 --- a/src/vs/workbench/contrib/backup/browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/browser/backupTracker.ts @@ -5,22 +5,53 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +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/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; export class BrowserBackupTracker extends BackupTracker implements IWorkbenchContribution { + // Delay creation of backups when content changes to avoid too much + // load on the backup service when the user is typing into the editor + // Since we always schedule a backup, even when auto save is on (web + // only), we have different scheduling delays based on auto save. This + // helps to avoid a race between saving (after 1s per default) and making + // a backup of the working copy. + private static readonly BACKUP_SCHEDULE_DELAYS = { + [AutoSaveMode.OFF]: 1000, + [AutoSaveMode.ON_FOCUS_CHANGE]: 1000, + [AutoSaveMode.ON_WINDOW_CHANGE]: 1000, + [AutoSaveMode.AFTER_SHORT_DELAY]: 2000, // explicitly higher to prevent races + [AutoSaveMode.AFTER_LONG_DELAY]: 1000 + }; + constructor( @IBackupFileService backupFileService: IBackupFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @ILogService logService: ILogService ) { - super(backupFileService, filesConfigurationService, workingCopyService, logService, lifecycleService); + super(backupFileService, workingCopyService, logService, lifecycleService); + } + + protected shouldScheduleBackup(workingCopy: IWorkingCopy): boolean { + // Web: we always want to schedule a backup, even if auto save + // is enabled because in web there is no handler on shutdown + // to trigger saving so there is a higher chance of dataloss. + // See https://github.com/microsoft/vscode/issues/108789 + return true; + } + + protected getBackupScheduleDelay(workingCopy: IWorkingCopy): number { + let autoSaveMode = this.filesConfigurationService.getAutoSaveMode(); + if (workingCopy.capabilities & WorkingCopyCapabilities.Untitled) { + autoSaveMode = AutoSaveMode.OFF; // auto-save is never on for untitled working copies + } + + return BrowserBackupTracker.BACKUP_SCHEDULE_DELAYS[autoSaveMode]; } protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { @@ -41,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 a854320c20b..d8f35fc9dae 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -5,47 +5,29 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +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 { - // Disable backup for when a short auto-save delay is configured with - // the rationale that the auto save will trigger a save periodically - // anway and thus creating frequent backups is not useful - // - // This will only apply to working copies that are not untitled where - // auto save is actually saving. - private static DISABLE_BACKUP_AUTO_SAVE_THRESHOLD = 1500; - - // Delay creation of backups when content changes to avoid too much - // load on the backup service when the user is typing into the editor - protected static BACKUP_FROM_CONTENT_CHANGE_DELAY = 1000; - // A map from working copy to a version ID we compute on each content // change. This version ID allows to e.g. ask if a backup for a specific // content has been made before closing. private readonly mapWorkingCopyToContentVersion = new Map(); - private backupsDisabledForAutoSaveables = false; - // A map of scheduled pending backups for working copies private readonly pendingBackups = new Map(); constructor( protected readonly backupFileService: IBackupFileService, - protected readonly filesConfigurationService: IFilesConfigurationService, protected readonly workingCopyService: IWorkingCopyService, protected readonly logService: ILogService, protected readonly lifecycleService: ILifecycleService ) { super(); - // Figure out initial auto save config - this.onAutoSaveConfigurationChange(filesConfigurationService.getAutoSaveConfiguration()); - // Fill in initial dirty working copies this.workingCopyService.dirtyWorkingCopies.forEach(workingCopy => this.onDidRegister(workingCopy)); @@ -60,9 +42,6 @@ export abstract class BackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.onDidChangeDirty(workingCopy))); this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); - // Listen to auto save config changes - this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(c => this.onAutoSaveConfigurationChange(c))); - // Lifecycle (handled in subclasses) this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); } @@ -105,44 +84,71 @@ export abstract class BackupTracker extends Disposable { } } - private onAutoSaveConfigurationChange(configuration: IAutoSaveConfiguration): void { - this.backupsDisabledForAutoSaveables = typeof configuration.autoSaveDelay === 'number' && configuration.autoSaveDelay < BackupTracker.DISABLE_BACKUP_AUTO_SAVE_THRESHOLD; - } + /** + * Allows subclasses to conditionally opt-out of doing a backup, e.g. if + * auto save is enabled. + */ + protected abstract shouldScheduleBackup(workingCopy: IWorkingCopy): boolean; + + /** + * Allows subclasses to control the delay before performing a backup from + * working copy content changes. + */ + protected abstract getBackupScheduleDelay(workingCopy: IWorkingCopy): number; private scheduleBackup(workingCopy: IWorkingCopy): void { - if (this.backupsDisabledForAutoSaveables && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { - return; // skip if auto save is enabled with a short delay - } // Clear any running backup operation - dispose(this.pendingBackups.get(workingCopy)); - this.pendingBackups.delete(workingCopy); + this.cancelBackup(workingCopy); + + // subclass prevented backup for working copy + if (!this.shouldScheduleBackup(workingCopy)) { + return; + } this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString()); // Schedule new backup + const cts = new CancellationTokenSource(); const handle = setTimeout(async () => { - - // Clear disposable - this.pendingBackups.delete(workingCopy); + if (cts.token.isCancellationRequested) { + return; + } // Backup if dirty if (workingCopy.isDirty()) { - this.logService.trace(`[backup tracker] running backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] creating backup`, workingCopy.resource.toString()); try { - const backup = await workingCopy.backup(); - await this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta); + const backup = await workingCopy.backup(cts.token); + if (cts.token.isCancellationRequested) { + return; + } + + if (workingCopy.isDirty()) { + this.logService.trace(`[backup tracker] storing backup`, workingCopy.resource.toString()); + + await this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta, cts.token); + } } catch (error) { this.logService.error(error); } } - }, BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY); + + if (cts.token.isCancellationRequested) { + return; + } + + // Clear disposable + this.pendingBackups.delete(workingCopy); + + }, this.getBackupScheduleDelay(workingCopy)); // Keep in map for disposal as needed this.pendingBackups.set(workingCopy, toDisposable(() => { this.logService.trace(`[backup tracker] clearing pending backup`, workingCopy.resource.toString()); + cts.dispose(true); clearTimeout(handle); })); } @@ -155,12 +161,16 @@ export abstract class BackupTracker extends Disposable { this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString()); // Clear any running backup operation - dispose(this.pendingBackups.get(workingCopy)); - this.pendingBackups.delete(workingCopy); + this.cancelBackup(workingCopy); // Forward to backup file service this.backupFileService.discardBackup(workingCopy.resource); } + private cancelBackup(workingCopy: IWorkingCopy): void { + dispose(this.pendingBackups.get(workingCopy)); + this.pendingBackups.delete(workingCopy); + } + protected abstract onBeforeShutdown(reason: ShutdownReason): boolean | Promise; } 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 403322f11e2..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'; @@ -20,12 +20,25 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { + // Delay creation of backups when working copy changes to avoid too much + // load on the backup service when the user is typing into the editor + private static readonly BACKUP_SCHEDULE_DELAY = 1000; + + // Disable backup for when a short auto-save delay is configured with + // the rationale that the auto save will trigger a save periodically + // anway and thus creating frequent backups is not useful + // + // This will only apply to working copies that are not untitled where + // auto save is actually saving. + private static readonly DISABLE_BACKUP_AUTO_SAVE_THRESHOLD = 1500; + constructor( @IBackupFileService backupFileService: IBackupFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @@ -36,7 +49,24 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @IEditorService private readonly editorService: IEditorService, @IEnvironmentService private readonly environmentService: IEnvironmentService ) { - super(backupFileService, filesConfigurationService, workingCopyService, logService, lifecycleService); + super(backupFileService, workingCopyService, logService, lifecycleService); + } + + protected shouldScheduleBackup(workingCopy: IWorkingCopy): boolean { + if (workingCopy.capabilities & WorkingCopyCapabilities.Untitled) { + return true; // always backup untitled + } + + const autoSaveConfiguration = this.filesConfigurationService.getAutoSaveConfiguration(); + if (typeof autoSaveConfiguration.autoSaveDelay === 'number' && autoSaveConfiguration.autoSaveDelay < NativeBackupTracker.DISABLE_BACKUP_AUTO_SAVE_THRESHOLD) { + return false; // skip backup when auto save is already enabled with a low delay + } + + return true; + } + + protected getBackupScheduleDelay(): number { + return NativeBackupTracker.BACKUP_SCHEDULE_DELAY; } protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { @@ -166,7 +196,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // Backup does not exist else { - const backup = await workingCopy.backup(); + const backup = await workingCopy.backup(CancellationToken.None); await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta); backups.push(workingCopy); 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 5e77da98cba..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 @@ -28,10 +28,10 @@ import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/ele import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +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'; @@ -45,6 +45,9 @@ import { TestFilesConfigurationService } from 'vs/workbench/test/browser/workben import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -69,9 +72,10 @@ class TestBackupTracker extends NativeBackupTracker { @IEnvironmentService environmentService: IEnvironmentService ) { super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService); + } - // Reduce timeout for tests - BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY = 10; + protected getBackupScheduleDelay(): number { + return 10; // Reduce timeout for tests } } @@ -215,6 +219,55 @@ suite('BackupTracker', () => { tracker.dispose(); }); + test('Track backups (custom)', async function () { + const [accessor, part, tracker] = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + backupDelay = 0; + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + async backup(token: CancellationToken): Promise { + await timeout(this.backupDelay); + + return {}; + } + } + + const resource = toResource.call(this, '/path/custom.txt'); + const customWorkingCopy = new TestBackupWorkingCopy(resource); + + // Normal + customWorkingCopy.setDirty(true); + await accessor.backupFileService.joinBackupResource(); + assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + customWorkingCopy.setDirty(true); + await accessor.backupFileService.joinBackupResource(); + assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + await accessor.backupFileService.joinDiscardBackup(); + assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + + // Cancellation + customWorkingCopy.setDirty(true); + await timeout(0); + customWorkingCopy.setDirty(false); + await accessor.backupFileService.joinDiscardBackup(); + assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + + customWorkingCopy.dispose(); + part.dispose(); + tracker.dispose(); + }); + test('onWillShutdown - no veto if no dirty files', async function () { const [accessor, part, tracker] = await createTracker(); 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/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/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/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 272a59189ad..9766d9e88b9 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')]; } 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/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5a648e644f2..8c472ae7e4e 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'; 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/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index b6c1ee1f468..e0b6acb323a 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -55,6 +55,11 @@ padding-bottom: 0; } + +.monaco-workbench.safari .monaco-action-bar .start-debug-action-item .configuration .monaco-select-box { + margin-bottom: 0px; +} + .monaco-workbench .monaco-action-bar .start-debug-action-item .configuration.disabled .monaco-select-box { opacity: 0.7; font-style: italic; @@ -93,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; @@ -102,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/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/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/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 dcc07b01e51..401c1897061 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,7 +14,7 @@ import { IExtensionRecommendationsService } from 'vs/workbench/services/extensio import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, @@ -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'; @@ -57,6 +57,10 @@ import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; +import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ResourceContextKey } from 'vs/workbench/common/resources'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -74,6 +78,16 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] }); +// Explorer +MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { + group: '4_extensions', + command: { + id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, + title: localize('installVSIX', "Install VSIX"), + }, + when: ResourceContextKey.Extension.isEqualTo('.vsix') +}); + // Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -206,6 +220,45 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, + handler: async (accessor: ServicesAccessor, resources: URI[] | URI) => { + const extensionService = accessor.get(IExtensionService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const instantiationService = accessor.get(IInstantiationService); + const hostService = accessor.get(IHostService); + const notificationService = accessor.get(INotificationService); + + const viewletService = accessor.get(IViewletService); + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + + if (!viewlet) { + return; + } + + const extensions = Array.isArray(resources) ? resources : [resources]; + await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) + .then(async (extensions) => { + for (const extension of extensions) { + const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name); + const actions = requireReload ? [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] : []; + notificationService.prompt( + Severity.Info, + message, + actions, + { sticky: true } + ); + } + await instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run(true); + }); + } +}); + CommandsRegistry.registerCommand({ id: 'workbench.extensions.uninstallExtension', description: { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0f54df948fc..8ef92310fcb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose, Disposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +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 { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -3007,50 +3007,26 @@ export class InstallVSIXAction extends Action { constructor( id = InstallVSIXAction.ID, label = InstallVSIXAction.LABEL, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService private readonly notificationService: INotificationService, - @IHostService private readonly hostService: IHostService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IExtensionService private readonly extensionService: IExtensionService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @ICommandService private readonly commandService: ICommandService ) { super(id, label, 'extension-action install-vsix', true); } - async run(vsixPaths?: URI[]): Promise { - if (!vsixPaths) { - vsixPaths = await this.fileDialogService.showOpenDialog({ - title: localize('installFromVSIX', "Install from VSIX"), - filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], - canSelectFiles: true, - openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) - }); + async run(): Promise { + const vsixPaths = await this.fileDialogService.showOpenDialog({ + title: localize('installFromVSIX', "Install from VSIX"), + filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], + canSelectFiles: true, + openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) + }); - if (!vsixPaths) { - return; - } + if (!vsixPaths) { + return; } // Install extension(s), display notification(s), display @installed extensions - await Promise.all(vsixPaths.map(async (vsix) => await this.extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => this.hostService.reload() - }] : []; - this.notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - } - await this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run(true); - }); + await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 92f6e26bff7..f89a61d48b6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -17,7 +17,7 @@ import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from '../common/extensions'; import { ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, @@ -60,6 +60,7 @@ import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); @@ -364,6 +365,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IExtensionService extensionService: IExtensionService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IPreferencesService private readonly preferencesService: IPreferencesService, + @ICommandService private readonly commandService: ICommandService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); @@ -475,7 +477,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE try { // Attempt to install the extension(s) - await this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL).run(vsixPaths); + await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); } catch (err) { this.notificationService.error(err); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 83067be82bc..110c26969b5 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -145,3 +145,4 @@ export class ExtensionContainers extends Disposable { } export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension'; +export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; 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..aab73adb67d 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,7 +47,7 @@ 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'; 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/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/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/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 6d7d1cdf745..644cdf7437f 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -95,6 +95,8 @@ suite('EditorAutoSave', () => { }); test('editor auto saves on focus change if configured', async function () { + this.retries(3); // https://github.com/microsoft/vscode/issues/108727 + const [accessor, part, editorAutoSave] = await createEditorAutoSave({ autoSave: 'onFocusChange' }); const resource = toResource.call(this, '/path/index.txt'); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index db5448308c4..a36b0947db6 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'; 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/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/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/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 5311ecb8aac..fd79380d7c9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -11,7 +11,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService } from 'vs/platform/files/common/files'; @@ -66,7 +66,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM readonly onDidChangeDirty = that.onDidChangeDirty; readonly onDidChangeContent = that.onDidChangeContent; isDirty(): boolean { return that.isDirty(); } - backup(): Promise { return that.backup(); } + backup(token: CancellationToken): Promise { return that.backup(token); } save(): Promise { return that.save(); } revert(options?: IRevertOptions): Promise { return that.revert(options); } }; @@ -89,10 +89,13 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM } } - async backup(): Promise> { + async backup(token: CancellationToken): Promise> { if (this._notebook.supportBackup) { - const tokenSource = new CancellationTokenSource(); + const tokenSource = new CancellationTokenSource(token); const backupId = await this._notebookService.backup(this.viewType, this.resource, tokenSource.token); + if (token.isCancellationRequested) { + return {}; + } const stats = await this._resolveStats(this.resource); return { 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 71baa088c19..7e86eb96f38 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -208,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 3ca781e9eee..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); @@ -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/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..5ef37918355 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,20 @@ 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; + } + return new URL(address).host; + } + set description(description: string | undefined) { this._description = description; } @@ -422,7 +425,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; } 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/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/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a8fa472a3bf..b5ddfadde33 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1307,14 +1307,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 +1433,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() }))); 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/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 056378f5b6b..63bf2771a46 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -138,7 +138,7 @@ export class PatternInputWidget extends Widget implements IThemable { private render(options: IOptions): void { this.domNode = document.createElement('div'); this.domNode.style.width = this.width + 'px'; - dom.addClass(this.domNode, 'monaco-findInput'); + this.domNode.classList.add('monaco-findInput'); this.inputBox = new ContextScopedHistoryInputBox(this.domNode, this.contextViewProvider, { placeholder: this.placeholder || '', 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/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index ba7bd8e4b98..c1a29a74874 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -225,7 +225,7 @@ export class MatchRenderer extends Disposable implements ITreeRenderer('search').showLineNumbers; const lineNumberStr = showLineNumbers ? `:${match.range().startLineNumber}` : ''; - DOM.toggleClass(templateData.lineNumber, 'show', (numLines > 0) || showLineNumbers); + templateData.lineNumber.classList.toggle('show', (numLines > 0) || showLineNumbers); templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers)); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index e123c8bb3af..414f7501adc 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -988,7 +988,7 @@ export class SearchView extends ViewPane { } const actionsPosition = this.searchConfig.actionsPosition; - dom.toggleClass(this.getContainer(), SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right'); + this.getContainer().classList.toggle(SearchView.ACTIONS_RIGHT_CLASS_NAME, actionsPosition === 'right'); this.searchWidget.setWidth(this.size.width - 28 /* container margin */); @@ -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/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index d149e29f822..c566e5d387f 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -293,7 +293,7 @@ export class SearchWidget extends Widget { }; this.toggleReplaceButton = this._register(new Button(parent, opts)); this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false'); - dom.addClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames); + this.toggleReplaceButton.element.classList.add(...searchHideReplaceIcon.classNamesArray); this.toggleReplaceButton.icon = 'toggle-replace-button'; // TODO@joh need to dispose this listener eventually this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton()); @@ -356,7 +356,7 @@ export class SearchWidget extends Widget { if (options.showContextToggle) { this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); - dom.addClass(this.contextLinesInput.element, 'context-lines-input'); + this.contextLinesInput.element.classList.add('context-lines-input'); this.contextLinesInput.value = '' + (this.configurationService.getValue('search').searchEditor.defaultNumberOfContextLines ?? 1); this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged())); this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); @@ -365,7 +365,7 @@ export class SearchWidget extends Widget { } private onContextLinesChanged() { - dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked); + this.domNode.classList.toggle('show-context', this.showContextCheckbox.checked); this._onDidToggleContext.fire(); if (this.contextLinesInput.value.includes('-')) { @@ -383,7 +383,7 @@ export class SearchWidget extends Widget { this.showContextCheckbox.checked = true; this.contextLinesInput.value = '' + lines; } - dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked); + this.domNode.classList.toggle('show-context', this.showContextCheckbox.checked); } private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void { @@ -429,13 +429,13 @@ export class SearchWidget extends Widget { } private onToggleReplaceButton(): void { - dom.toggleClass(this.replaceContainer, 'disabled'); + this.replaceContainer.classList.toggle('disabled'); if (this.isReplaceShown()) { - dom.removeClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames); - dom.addClasses(this.toggleReplaceButton.element, searchShowReplaceIcon.classNames); + this.toggleReplaceButton.element.classList.remove(...searchHideReplaceIcon.classNamesArray); + this.toggleReplaceButton.element.classList.add(...searchShowReplaceIcon.classNamesArray); } else { - dom.removeClasses(this.toggleReplaceButton.element, searchShowReplaceIcon.classNames); - dom.addClasses(this.toggleReplaceButton.element, searchHideReplaceIcon.classNames); + this.toggleReplaceButton.element.classList.remove(...searchShowReplaceIcon.classNamesArray); + this.toggleReplaceButton.element.classList.add(...searchHideReplaceIcon.classNamesArray); } this.toggleReplaceButton.element.setAttribute('aria-expanded', this.isReplaceShown() ? 'true' : 'false'); this.updateReplaceActiveState(); 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 fd9de5391f2..a75dbd61ed5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -28,16 +28,17 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { ITextFileSaveOptions, ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +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, }; @@ -113,7 +114,7 @@ export class SearchEditorInput extends EditorInput { readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } - backup(): Promise { return input.backup(); } + backup(token: CancellationToken): Promise { return input.backup(token); } save(options?: ISaveOptions): Promise { return input.save(0, options).then(editor => !!editor); } revert(options?: IRevertOptions): Promise { return input.revert(0, options); } }; @@ -265,7 +266,7 @@ export class SearchEditorInput extends EditorInput { return false; } - private async backup(): Promise { + private async backup(token: CancellationToken): Promise { const content = stringToSnapshot((await this.model).getValue()); return { content }; } 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/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/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/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..7011a47f060 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'; 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/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..68aa3c4b42a 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,45 @@ 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._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 +157,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..9a46e058c68 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -7,14 +7,14 @@ 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 { ILifecycleService } from 'vs/workbench/services/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 { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider, IRemoteTerminalService } 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'; @@ -85,8 +85,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 +108,9 @@ 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 ) { this._activeTabIndex = 0; this._isShuttingDown = false; @@ -221,7 +223,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 +341,29 @@ 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(); + 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 +635,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 +645,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..7ee9b719d9c 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -116,6 +116,21 @@ export interface ISendCommandResultToTerminalProcessArguments { payload: any; } +export interface IOrphanQuestionReplyArgs { + id: number; +} + +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 +141,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 +159,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 +191,7 @@ export class RemoteTerminalChannelClient { ) { } - private _readSingleTemrinalConfiguration(key: string): ISingleTerminalConfiguration { + private _readSingleTerminalConfiguration(key: string): ISingleTerminalConfiguration { const result = this._configurationService.inspect(key); return { userValue: result.userValue, @@ -178,18 +203,18 @@ export class RemoteTerminalChannelClient { public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, 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, @@ -306,4 +331,15 @@ 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 { + return this._channel.call('$listTerminals'); + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 1af8a10e218..83f5b682d17 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 @@ -266,6 +280,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 +310,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 +326,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 +440,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/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..2df022d0558 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -896,7 +896,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer extends Disposable { public readonly id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, - public readonly extension: WebviewExtensionDescription | undefined, + public extension: WebviewExtensionDescription | undefined, private readonly webviewThemeDataProvider: WebviewThemeDataProvider, @INotificationService notificationService: INotificationService, @ILogService private readonly _logService: ILogService, @@ -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/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index f5a6f27629e..3168612411f 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -30,6 +30,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private _initialScrollProgress: number = 0; private _state: string | undefined = undefined; + private _extension: WebviewExtensionDescription | undefined; private _contentOptions: WebviewContentOptions; private _options: WebviewOptions; @@ -42,13 +43,14 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv public readonly id: string, initialOptions: WebviewOptions, initialContentOptions: WebviewContentOptions, - public readonly extension: WebviewExtensionDescription | undefined, + extension: WebviewExtensionDescription | undefined, @ILayoutService private readonly _layoutService: ILayoutService, @IWebviewService private readonly _webviewService: IWebviewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); + this._extension = extension; this._options = initialOptions; this._contentOptions = initialContentOptions; @@ -175,6 +177,12 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv this.withWebview(webview => webview.state = value); } + public get extension(): WebviewExtensionDescription | undefined { return this._extension; } + public set extension(value: WebviewExtensionDescription | undefined) { + this._extension = value; + this.withWebview(webview => webview.extension = value); + } + public get options(): WebviewOptions { return this._options; } public set options(value: WebviewOptions) { this._options = { customClasses: this._options.customClasses, ...value }; } 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 f154416d17d..41e915c4ecf 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -6,7 +6,7 @@ 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'; @@ -35,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; @@ -72,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(); } @@ -84,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 { @@ -103,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); } }); }); @@ -116,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(); @@ -127,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, new Dimension(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); } } @@ -152,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}`); @@ -179,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/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..efa338d250b 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'; 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 59f88d7cebf..0392acfd564 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -457,7 +457,7 @@ export class AuthenticationService extends Disposable implements IAuthentication const didTimeout: Promise = new Promise((_, reject) => { setTimeout(() => { reject(); - }, 2000); + }, 5000); }); return Promise.race([didRegister, didTimeout]); diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 3637e0d4467..dfd54a9ee75 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -6,6 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const IBackupFileService = createDecorator('backupFileService'); @@ -59,8 +60,9 @@ export interface IBackupFileService { * @param versionId The optionsl version id of the resource to backup. * @param meta The optional meta data of the resource to backup. This information * can be restored later when loading the backup again. + * @param token The optional cancellation token if the operation can be cancelled. */ - backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise; + backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise; /** * Discards the backup associated with a resource if it exists. diff --git a/src/vs/workbench/services/backup/common/backupFileService.ts b/src/vs/workbench/services/backup/common/backupFileService.ts index 175d5e9ecca..5049d8f6dc3 100644 --- a/src/vs/workbench/services/backup/common/backupFileService.ts +++ b/src/vs/workbench/services/backup/common/backupFileService.ts @@ -21,6 +21,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { TextSnapshotReadable, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IBackupFilesModel { resolve(backupRoot: URI): Promise; @@ -157,8 +158,8 @@ export class BackupFileService implements IBackupFileService { return this.impl.hasBackupSync(resource, versionId); } - backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { - return this.impl.backup(resource, content, versionId, meta); + backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { + return this.impl.backup(resource, content, versionId, meta, token); } discardBackup(resource: URI): Promise { @@ -232,8 +233,11 @@ class BackupFileServiceImpl extends Disposable implements IBackupFileService { return this.model.has(backupResource, versionId); } - async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { const model = await this.ready; + if (token?.isCancellationRequested) { + return; + } const backupResource = this.toBackupResource(resource); if (model.has(backupResource, versionId, meta)) { @@ -241,6 +245,10 @@ class BackupFileServiceImpl extends Disposable implements IBackupFileService { } return this.ioOperationQueues.queueFor(backupResource).queue(async () => { + if (token?.isCancellationRequested) { + return; + } + let preamble: string | undefined = undefined; // With Metadata: URI + META-START + Meta + END @@ -419,7 +427,7 @@ export class InMemoryBackupFileService implements IBackupFileService { return this.backups.has(backupResource.toString()); } - async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { const backupResource = this.toBackupResource(resource); this.backups.set(backupResource.toString(), content || stringToSnapshot('')); } diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 80d03fe3ae6..0e00c5a4982 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -28,6 +28,7 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file import { VSBuffer } from 'vs/base/common/buffer'; import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { TestProductService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice'); const backupHome = path.join(userdataDir, 'Backups'); @@ -80,8 +81,8 @@ export class NodeTestBackupFileService extends BackupFileService { return new Promise(resolve => this.backupResourceJoiners.push(resolve)); } - async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: any): Promise { - await super.backup(resource, content, versionId, meta); + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: any, token?: CancellationToken): Promise { + await super.backup(resource, content, versionId, meta, token); while (this.backupResourceJoiners.length) { this.backupResourceJoiners.pop()!(); @@ -262,6 +263,16 @@ suite('BackupFileService', () => { model.dispose(); }); + + test('cancellation', async () => { + const cts = new CancellationTokenSource(); + const promise = service.backup(fooFile, undefined, undefined, undefined, cts.token); + cts.cancel(); + await promise; + + assert.equal(fs.existsSync(fooBackupPath), false); + assert.ok(!service.hasBackupSync(fooFile)); + }); }); suite('discardBackup', () => { 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/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/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..026e286977d 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'; @@ -121,6 +121,9 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost `; const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; iframe.setAttribute('src', iframeContent); + const timeout = setTimeout(() => { + this._onDidExit.fire([ExtensionHostExitCode.StartTimeout10s, 'The Web Worker Extension Host did not start in 10s']); + }, 10000); const barrier = new Barrier(); let port!: MessagePort; @@ -139,16 +142,19 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost err.name = name; err.stack = stack; onUnexpectedError(err); - this._onDidExit.fire([18, err.message]); + clearTimeout(timeout); + this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, err.message]); return; } const { data } = event.data; if (barrier.isOpen() || !(data instanceof MessagePort)) { console.warn('UNEXPECTED message', event); - this._onDidExit.fire([81, 'UNEXPECTED message']); + clearTimeout(timeout); + this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, 'UNEXPECTED message']); return; } port = data; + clearTimeout(timeout); barrier.open(); })); @@ -193,7 +199,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 +223,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/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 7a6406a499a..f9fbf7e59be 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -141,7 +141,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx let toAdd: IExtension[] = []; let toRemove: string[] = []; for (const extension of extensions) { - if (this._extensionEnablementService.isEnabled(extension)) { + if (this._safeInvokeIsEnabled(extension)) { // an extension has been enabled toAdd.push(extension); } else { @@ -154,7 +154,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._register(this._extensionManagementService.onDidInstallExtension((event) => { if (event.local) { - if (this._extensionEnablementService.isEnabled(event.local)) { + if (this._safeInvokeIsEnabled(event.local)) { // an extension has been installed this._handleDeltaExtensions(new DeltaExtensionsQueueItem([event.local], [])); } @@ -618,7 +618,15 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return false; } - return this._extensionEnablementService.isEnabled(toExtension(extension)); + return this._safeInvokeIsEnabled(toExtension(extension)); + } + + protected _safeInvokeIsEnabled(extension: IExtension): boolean { + try { + return this._extensionEnablementService.isEnabled(extension); + } catch (err) { + return false; + } } protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { 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/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; } +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 certain DOM events to detect keybinding usage + this._register(addDisposableListener(this.layoutService.container, EventType.KEY_DOWN, e => this.updateShutdownReasonFromEvent(e))); + this._register(addDisposableListener(this.layoutService.container, EventType.KEY_UP, () => this.updateShutdownReasonFromEvent(undefined))); + this._register(addDisposableListener(this.layoutService.container, EventType.MOUSE_DOWN, () => this.updateShutdownReasonFromEvent(undefined))); + this._register(addDisposableListener(this.layoutService.container, EventType.MOUSE_UP, () => this.updateShutdownReasonFromEvent(undefined))); + this._register(this.onDidChangeFocus(() => this.updateShutdownReasonFromEvent(undefined))); } 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.confirmBeforeQuit')) { - console.warn('Unload prevented: window.confirmBeforeQuit=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: KeyboardEvent | undefined): void { + if (this.shutdownReason === HostShutdownReason.Api) { + return; // do not overwrite any explicitly set shutdown reason + } + + this.shutdownReason = e ? HostShutdownReason.Keyboard : 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,7 +405,10 @@ export class BrowserHostService extends Disposable implements IHostService { } async reload(): Promise { - this.signalExpectedShutdown = true; + + // We know that `window.location.reload` will trigger a shutdown + // so we update `shutdownReason` to reflect that + this.shutdownReason = HostShutdownReason.Api; window.location.reload(); } 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/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/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 0f1d5bb3263..ffcc91f57fa 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -22,7 +22,7 @@ import { basename } from 'vs/base/common/path'; import { IWorkingCopyService, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { UTF8 } from 'vs/workbench/services/textfile/common/encoding'; interface IBackupMetaData { @@ -187,7 +187,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Backup - async backup(): Promise { + async backup(token: CancellationToken): Promise { // Fill in metadata if we are resolved let meta: IBackupMetaData | undefined = undefined; 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/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 36b7fd663c3..cde9e21cf7e 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'; @@ -152,12 +152,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const fileIconData = FileIconThemeData.fromStorageData(this.storageService); if (fileIconData) { - this.applyAndSetFileIconTheme(fileIconData); + this.applyAndSetFileIconTheme(fileIconData, true); } const productIconData = ProductIconThemeData.fromStorageData(this.storageService); if (productIconData) { - this.applyAndSetProductIconTheme(productIconData); + this.applyAndSetProductIconTheme(productIconData, true); } this.initialize().then(undefined, errors.onUnexpectedError).then(_ => { @@ -456,11 +456,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; @@ -569,15 +569,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return false; } - private applyAndSetFileIconTheme(iconThemeData: FileIconThemeData): void { + private applyAndSetFileIconTheme(iconThemeData: FileIconThemeData, silent = false): void { this.currentFileIconTheme = iconThemeData; _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); @@ -585,8 +585,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (iconThemeData.id) { this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'fileIcon'); } - this.onFileIconThemeChange.fire(this.currentFileIconTheme); + if (!silent) { + this.onFileIconThemeChange.fire(this.currentFileIconTheme); + } } public getProductIconThemes(): Promise { @@ -639,7 +641,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return false; } - private applyAndSetProductIconTheme(iconThemeData: ProductIconThemeData): void { + private applyAndSetProductIconTheme(iconThemeData: ProductIconThemeData, silent = false): void { this.currentProductIconTheme = iconThemeData; @@ -650,8 +652,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (iconThemeData.id) { this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'productIcon'); } - this.onProductIconThemeChange.fire(this.currentProductIconTheme); - + if (!silent) { + this.onProductIconThemeChange.fire(this.currentProductIconTheme); + } } } diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 6b9fa724b74..73ede2fd1f3 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'; @@ -56,7 +54,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { 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,11 +515,9 @@ 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, semanticTokenRules: this.semanticTokenRules.map(SemanticTokenRule.toJSONObject), extensionData: ExtensionData.toJSONObject(this.extensionData), - location: this.location?.toJSON(), themeSemanticHighlighting: this.themeSemanticHighlighting, colorMap: colorMapData, watch: this.watch @@ -529,15 +525,12 @@ export class ColorThemeData implements IWorkbenchColorTheme { 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; + get baseTheme(): string { + return this.classNames[0]; } - get baseTheme(): string { - return this.id.split(' ')[0]; + get classNames(): string[] { + return this.id.split(' '); } get type(): ColorScheme { @@ -607,7 +600,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/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/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index c06d942139a..6d27bf234b3 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -21,6 +21,7 @@ import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { @@ -265,7 +266,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.dispose(); } - async backup(): Promise { + async backup(token: CancellationToken): Promise { return { content: withNullAsUndefined(this.createSnapshot()) }; } 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-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/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index e5f96509de8..a390c9be0dd 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -11,6 +11,7 @@ import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from import { ResourceMap } from 'vs/base/common/map'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { ITextSnapshot } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const enum WorkingCopyCapabilities { @@ -98,8 +99,10 @@ export interface IWorkingCopy { * * Providers of working copies should use `IBackupFileService.resolve(workingCopy.resource)` * to retrieve the backup metadata associated when loading the working copy. + * + * @param token support for cancellation */ - backup(): Promise; + backup(token: CancellationToken): Promise; /** * Asks the working copy to save. If the working copy was dirty, it is diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index a9178e8c36b..a1c06d18642 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -11,7 +11,7 @@ import { toResource } from 'vs/base/test/common/utils'; import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { FileOperation } from 'vs/platform/files/common/files'; -import { TestWorkingCopy } from 'vs/workbench/services/workingCopy/test/common/workingCopyService.test'; +import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { VSBuffer } from 'vs/base/common/buffer'; suite('WorkingCopyFileService', () => { diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 97840f97145..df5f7b2e048 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,74 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; -import { basename } from 'vs/base/common/resources'; - -export class TestWorkingCopy extends Disposable implements IWorkingCopy { - - private readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent = this._onDidChangeContent.event; - - private readonly _onDispose = this._register(new Emitter()); - readonly onDispose = this._onDispose.event; - - readonly capabilities = WorkingCopyCapabilities.None; - - readonly name = basename(this.resource); - - private dirty = false; - - constructor(public readonly resource: URI, isDirty = false) { - super(); - - this.dirty = isDirty; - } - - setDirty(dirty: boolean): void { - if (this.dirty !== dirty) { - this.dirty = dirty; - this._onDidChangeDirty.fire(); - } - } - - setContent(content: string): void { - this._onDidChangeContent.fire(); - } - - isDirty(): boolean { - return this.dirty; - } - - async save(options?: ISaveOptions): Promise { - return true; - } - - async revert(options?: IRevertOptions): Promise { - this.setDirty(false); - } - - async backup(): Promise { - return {}; - } - - dispose(): void { - this._onDispose.fire(); - - super.dispose(); - } -} +import { TestWorkingCopy, TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices'; suite('WorkingCopyService', () => { - test('registry - basics', () => { const service = new TestWorkingCopyService(); 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..34e84902831 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 { diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 866a0fda122..b0bddb28d99 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -14,12 +14,14 @@ import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderW import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { InMemoryStorageService, IWillSaveStateEvent } from 'vs/platform/storage/common/storage'; -import { WorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { @@ -125,6 +127,63 @@ export class TestStorageService extends InMemoryStorageService { export class TestWorkingCopyService extends WorkingCopyService { } +export class TestWorkingCopy extends Disposable implements IWorkingCopy { + + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; + + readonly capabilities = WorkingCopyCapabilities.None; + + readonly name = resources.basename(this.resource); + + private dirty = false; + + constructor(public readonly resource: URI, isDirty = false) { + super(); + + this.dirty = isDirty; + } + + setDirty(dirty: boolean): void { + if (this.dirty !== dirty) { + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + } + + setContent(content: string): void { + this._onDidChangeContent.fire(); + } + + isDirty(): boolean { + return this.dirty; + } + + async save(options?: ISaveOptions): Promise { + return true; + } + + async revert(options?: IRevertOptions): Promise { + this.setDirty(false); + } + + async backup(token: CancellationToken): Promise { + return {}; + } + + dispose(): void { + this._onDispose.fire(); + + super.dispose(); + } +} + export class TestWorkingCopyFileService implements IWorkingCopyFileService { declare readonly _serviceBrand: undefined; 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..fa0ea82dcf2 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -295,4 +295,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.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index aa3f59f402a..3dfbe3d9b27 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -16,6 +16,11 @@ 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 diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index b21da0e7956..16accfae70b 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'; @@ -444,7 +444,8 @@ interface IWorkbenchConstructionOptions { interface IWorkbench { commands: { executeCommand(command: string, ...args: any[]): Promise; - } + }, + shutdown: () => void; } /** @@ -456,7 +457,7 @@ interface IWorkbench { let created = false; let workbenchPromiseResolve: Function; const workbenchPromise = new Promise(resolve => workbenchPromiseResolve = resolve); -async function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { +function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): IDisposable { // Mark start of workbench mark('didLoadWorkbenchMain'); @@ -470,10 +471,6 @@ async function create(domElement: HTMLElement, options: IWorkbenchConstructionOp created = true; } - // Startup workbench and resolve waiters - const workbench = await main(domElement, options); - workbenchPromiseResolve(workbench); - // Register commands if any if (Array.isArray(options.commands)) { for (const command of options.commands) { @@ -484,6 +481,21 @@ async function create(domElement: HTMLElement, options: IWorkbenchConstructionOp }); } } + + // Startup workbench and resolve waiters + let instantiatedWorkbench: IWorkbench | undefined = undefined; + main(domElement, options).then(workbench => { + instantiatedWorkbench = workbench; + workbenchPromiseResolve(workbench); + }); + + return toDisposable(() => { + if (instantiatedWorkbench) { + instantiatedWorkbench.shutdown(); + } else { + workbenchPromise.then(instantiatedWorkbench => instantiatedWorkbench.shutdown()); + } + }); } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index f738609dfde..22d0ae14060 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -65,14 +65,15 @@ import { ITunnelService, TunnelService } from 'vs/platform/remote/common/tunnel' import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { StorageKeysSyncRegistryService, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { WebUserDataAutoSyncService } from 'vs/workbench/services/userDataSync/browser/userDataAutoSyncService'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { WebUserDataAutoSyncEnablementService } from 'vs/workbench/services/userDataSync/browser/userDataAutoSyncEnablementService'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; @@ -90,8 +91,9 @@ registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService); -registerSingleton(IUserDataSyncService, UserDataSyncService, true); -registerSingleton(IUserDataAutoSyncService, WebUserDataAutoSyncService, true); +registerSingleton(IUserDataSyncService, UserDataSyncService); +registerSingleton(IUserDataAutoSyncEnablementService, WebUserDataAutoSyncEnablementService); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); diff --git a/yarn.lock b/yarn.lock index b59bc29d438..6bbc9a10963 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9296,10 +9296,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -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== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"