diff options
author | Sandeep Somavarapu <sasomava@microsoft.com> | 2022-09-13 21:35:14 +0300 |
---|---|---|
committer | Sandeep Somavarapu <sasomava@microsoft.com> | 2022-09-13 21:35:14 +0300 |
commit | bb54b84573a141744e3ab51db06f59b4757fc137 (patch) | |
tree | 3fe9212324c746d3d9f2bec19d50784eb1bf9032 | |
parent | d0d5cbd82f4944741a40ff2784dc86f296825a23 (diff) | |
parent | 28e52a46fe8df0c924c881e438e124c05f171b9c (diff) |
Merge branch 'main' into sandy081/profilesStorageServicesandy081/profilesStorageService
169 files changed, 2076 insertions, 1716 deletions
diff --git a/.github/commands.json b/.github/commands.json index c429bff29f2..5ade58ab38a 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -86,21 +86,6 @@ }, { "type": "label", - "name": "notebook", - "regex": "notebook.*", - "assign": [ - "rebornix" - ] - }, - { - "type": "label", - "name": "notebook-triage", - "regex": "notebook.*", - "action": "updateLabels", - "addLabel": "notebook-triage" - }, - { - "type": "label", "name": "L10N", "assign": [ "csigs", diff --git a/extensions/css-language-features/server/src/utils/documentContext.ts b/extensions/css-language-features/server/src/utils/documentContext.ts index 3defe4a445d..c9f46fb7578 100644 --- a/extensions/css-language-features/server/src/utils/documentContext.ts +++ b/extensions/css-language-features/server/src/utils/documentContext.ts @@ -30,8 +30,9 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp return folderUri + ref.substring(1); } } - base = base.substring(0, base.lastIndexOf('/') + 1); - return Utils.resolvePath(URI.parse(base), ref).toString(true); + const baseUri = URI.parse(base); + const baseUriDir = baseUri.path.endsWith('/') ? baseUri : Utils.dirname(baseUri); + return Utils.resolvePath(baseUriDir, ref).toString(true); }, }; } diff --git a/extensions/html-language-features/server/src/node/nodeFs.ts b/extensions/html-language-features/server/src/node/nodeFs.ts index 9bab4cf2913..edc9be776a6 100644 --- a/extensions/html-language-features/server/src/node/nodeFs.ts +++ b/extensions/html-language-features/server/src/node/nodeFs.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileSystemProvider, getScheme } from '../requests'; +import { FileSystemProvider } from '../requests'; import { URI as Uri } from 'vscode-uri'; import * as fs from 'fs'; @@ -11,7 +11,7 @@ import { FileType } from 'vscode-css-languageservice'; export function getNodeFileFS(): FileSystemProvider { function ensureFileUri(location: string) { - if (getScheme(location) !== 'file') { + if (!location.startsWith('file:')) { throw new Error('fileSystemProvider can only handle file URLs'); } } diff --git a/extensions/html-language-features/server/src/requests.ts b/extensions/html-language-features/server/src/requests.ts index 3899cf9eff5..725f6f3b135 100644 --- a/extensions/html-language-features/server/src/requests.ts +++ b/extensions/html-language-features/server/src/requests.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vscode-uri'; import { RequestType, Connection } from 'vscode-languageserver'; import { RuntimeEnvironment } from './htmlServer'; @@ -77,80 +76,3 @@ export function getFileSystemProvider(handledSchemas: string[], connection: Conn } }; } - -export function getScheme(uri: string) { - return uri.substr(0, uri.indexOf(':')); -} - -export function dirname(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; -} - -export function basename(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return uri.substr(lastIndexOfSlash + 1); -} - - -const Slash = '/'.charCodeAt(0); -const Dot = '.'.charCodeAt(0); - -export function extname(uri: string) { - for (let i = uri.length - 1; i >= 0; i--) { - const ch = uri.charCodeAt(i); - if (ch === Dot) { - if (i > 0 && uri.charCodeAt(i - 1) !== Slash) { - return uri.substr(i); - } else { - break; - } - } else if (ch === Slash) { - break; - } - } - return ''; -} - -export function isAbsolutePath(path: string) { - return path.charCodeAt(0) === Slash; -} - -export function resolvePath(uriString: string, path: string): string { - if (isAbsolutePath(path)) { - const uri = URI.parse(uriString); - const parts = path.split('/'); - return uri.with({ path: normalizePath(parts) }).toString(); - } - return joinPath(uriString, path); -} - -export function normalizePath(parts: string[]): string { - const newParts: string[] = []; - for (const part of parts) { - if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { - // ignore - } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { - newParts.pop(); - } else { - newParts.push(part); - } - } - if (parts.length > 1 && parts[parts.length - 1].length === 0) { - newParts.push(''); - } - let res = newParts.join('/'); - if (parts[0].length === 0) { - res = '/' + res; - } - return res; -} - -export function joinPath(uriString: string, ...paths: string[]): string { - const uri = URI.parse(uriString); - const parts = uri.path.split('/'); - for (const path of paths) { - parts.push(...path.split('/')); - } - return uri.with({ path: normalizePath(parts) }).toString(); -} diff --git a/extensions/html-language-features/server/src/utils/documentContext.ts b/extensions/html-language-features/server/src/utils/documentContext.ts index 88e3f032885..9cf8ce9ea76 100644 --- a/extensions/html-language-features/server/src/utils/documentContext.ts +++ b/extensions/html-language-features/server/src/utils/documentContext.ts @@ -6,7 +6,7 @@ import { DocumentContext } from 'vscode-css-languageservice'; import { endsWith, startsWith } from '../utils/strings'; import { WorkspaceFolder } from 'vscode-languageserver'; -import { resolvePath } from '../requests'; +import { URI, Utils } from 'vscode-uri'; export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { function getRootFolder(): string | undefined { @@ -34,8 +34,9 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp return folderUri + ref.substr(1); } } - base = base.substr(0, base.lastIndexOf('/') + 1); - return resolvePath(base, ref); + const baseUri = URI.parse(base); + const baseUriDir = baseUri.path.endsWith('/') ? baseUri : Utils.dirname(baseUri); + return Utils.resolvePath(baseUriDir, ref).toString(true); }, }; } diff --git a/extensions/ipynb/src/serializers.ts b/extensions/ipynb/src/serializers.ts index 455fb0d2745..21e97824f18 100644 --- a/extensions/ipynb/src/serializers.ts +++ b/extensions/ipynb/src/serializers.ts @@ -7,6 +7,7 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode'; import { CellMetadata, CellOutputMetadata } from './common'; import { textMimeTypes } from './deserializers'; +import { compressOutputItemStreams } from './streamCompressor'; const textDecoder = new TextDecoder(); @@ -270,21 +271,17 @@ type JupyterOutput = function convertStreamOutput(output: NotebookCellOutput): JupyterOutput { const outputs: string[] = []; - output.items - .filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout) - .map((opit) => textDecoder.decode(opit.data)) - .forEach(value => { - // Ensure each line is a seprate entry in an array (ending with \n). - const lines = value.split('\n'); - // If the last item in `outputs` is not empty and the first item in `lines` is not empty, then concate them. - // As they are part of the same line. - if (outputs.length && lines.length && lines[0].length > 0) { - outputs[outputs.length - 1] = `${outputs[outputs.length - 1]}${lines.shift()!}`; - } - for (const line of lines) { - outputs.push(line); - } - }); + const compressedStream = output.items.length ? new TextDecoder().decode(compressOutputItemStreams(output.items[0].mime, output.items)) : ''; + // Ensure each line is a separate entry in an array (ending with \n). + const lines = compressedStream.split('\n'); + // If the last item in `outputs` is not empty and the first item in `lines` is not empty, then concate them. + // As they are part of the same line. + if (outputs.length && lines.length && lines[0].length > 0) { + outputs[outputs.length - 1] = `${outputs[outputs.length - 1]}${lines.shift()!}`; + } + for (const line of lines) { + outputs.push(line); + } for (let index = 0; index < (outputs.length - 1); index++) { outputs[index] = `${outputs[index]}\n`; diff --git a/extensions/ipynb/src/streamCompressor.ts b/extensions/ipynb/src/streamCompressor.ts new file mode 100644 index 00000000000..cea3184e16c --- /dev/null +++ b/extensions/ipynb/src/streamCompressor.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { NotebookCellOutputItem } from 'vscode'; + + +/** + * Given a stream of individual stdout outputs, this function will return the compressed lines, escaping some of the common terminal escape codes. + * E.g. some terminal escape codes would result in the previous line getting cleared, such if we had 3 lines and + * last line contained such a code, then the result string would be just the first two lines. + */ +export function compressOutputItemStreams(mimeType: string, outputs: NotebookCellOutputItem[]) { + // return outputs.find(op => op.mime === mimeType)!.data.buffer; + + const buffers: Uint8Array[] = []; + let startAppending = false; + // Pick the first set of outputs with the same mime type. + for (const output of outputs) { + if (output.mime === mimeType) { + if ((buffers.length === 0 || startAppending)) { + buffers.push(output.data); + startAppending = true; + } + } else if (startAppending) { + startAppending = false; + } + } + compressStreamBuffer(buffers); + const totalBytes = buffers.reduce((p, c) => p + c.byteLength, 0); + const combinedBuffer = new Uint8Array(totalBytes); + let offset = 0; + for (const buffer of buffers) { + combinedBuffer.set(buffer, offset); + offset = offset + buffer.byteLength; + } + return combinedBuffer; +} +const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`; +const MOVE_CURSOR_1_LINE_COMMAND_BYTES = MOVE_CURSOR_1_LINE_COMMAND.split('').map(c => c.charCodeAt(0)); +const LINE_FEED = 10; +function compressStreamBuffer(streams: Uint8Array[]) { + streams.forEach((stream, index) => { + if (index === 0 || stream.length < MOVE_CURSOR_1_LINE_COMMAND.length) { + return; + } + + const previousStream = streams[index - 1]; + + // Remove the previous line if required. + const command = stream.subarray(0, MOVE_CURSOR_1_LINE_COMMAND.length); + if (command[0] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[0] && command[1] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[1] && command[2] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[2]) { + const lastIndexOfLineFeed = previousStream.lastIndexOf(LINE_FEED); + if (lastIndexOfLineFeed === -1) { + return; + } + streams[index - 1] = previousStream.subarray(0, lastIndexOfLineFeed); + streams[index] = stream.subarray(MOVE_CURSOR_1_LINE_COMMAND.length); + } + }); + return streams; +} diff --git a/extensions/r/language-configuration.json b/extensions/r/language-configuration.json index dd691e2a6d4..3a2e2f34f5f 100644 --- a/extensions/r/language-configuration.json +++ b/extensions/r/language-configuration.json @@ -11,13 +11,16 @@ ["{", "}"], ["[", "]"], ["(", ")"], + ["`", "`"], { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] } + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "%", "close": "%", "notIn": ["string", "comment"] } ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], + ["`", "`"], ["\"", "\""], ["'", "'"] ] diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index d5d1723533c..144c6654609 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -135,7 +135,7 @@ const Extract_Interface = Object.freeze<CodeActionKind>({ }); const Move_NewFile = Object.freeze<CodeActionKind>({ - kind: vscode.CodeActionKind.Refactor.append('move').append('newFile'), + kind: vscode.CodeActionKind.RefactorMove.append('newFile'), matches: refactor => refactor.name.startsWith('Move to a new file') }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index 7d4d4f9e7ed..e447db1326c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -115,18 +115,6 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { }; return dto; }, - saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { - return { - id: '1', - delete: () => { } - }; - } }; (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Notebook API tests', function () { @@ -244,7 +232,8 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { await closeAllEditors(); }); - test('#115855 onDidSaveNotebookDocument', async function () { + // TODO: Skipped due to notebook content provider removal + test.skip('#115855 onDidSaveNotebookDocument', async function () { const resource = await createRandomNotebookFile(); const notebook = await vscode.workspace.openNotebookDocument(resource); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index ab11a4ee493..e89bf64a26a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -26,15 +26,6 @@ suite('Notebook Document', function () { [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, uri.toString(), 'javascript')], ); } - async saveNotebook(_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) { - // - } - async saveNotebookAs(_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) { - // - } - async backupNotebook(_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) { - return { id: '', delete() { } }; - } }; const disposables: vscode.Disposable[] = []; @@ -329,40 +320,6 @@ suite('Notebook Document', function () { assert.ok(document.metadata.custom.extraNotebookMetadata, `Test metadata not found`); }); - test('document save API', async function () { - const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const notebook = await vscode.workspace.openNotebookDocument(uri); - - assert.strictEqual(notebook.uri.toString(), uri.toString()); - assert.strictEqual(notebook.isDirty, false); - assert.strictEqual(notebook.isUntitled, false); - assert.strictEqual(notebook.cellCount, 1); - assert.strictEqual(notebook.notebookType, 'notebook.nbdtest'); - - const edit = new vscode.WorkspaceEdit(); - edit.set(notebook.uri, [vscode.NotebookEdit.replaceCells(new vscode.NotebookRange(0, 0), [{ - kind: vscode.NotebookCellKind.Markup, - languageId: 'markdown', - metadata: undefined, - outputs: [], - value: 'new_markdown' - }, { - kind: vscode.NotebookCellKind.Code, - languageId: 'fooLang', - metadata: undefined, - outputs: [], - value: 'new_code' - }])]); - - const success = await vscode.workspace.applyEdit(edit); - assert.strictEqual(success, true); - assert.strictEqual(notebook.isDirty, true); - - await notebook.save(); - assert.strictEqual(notebook.isDirty, false); - }); - - test('setTextDocumentLanguage for notebook cells', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); @@ -395,21 +352,6 @@ suite('Notebook Document', function () { assert.strictEqual(cellDoc.languageId, 'css'); }); - test('dirty state - complex', async function () { - const resource = await utils.createRandomFile(undefined, undefined, '.nbdtest'); - const document = await vscode.workspace.openNotebookDocument(resource); - assert.strictEqual(document.isDirty, false); - - const edit = new vscode.WorkspaceEdit(); - edit.set(document.uri, [vscode.NotebookEdit.replaceCells(new vscode.NotebookRange(0, document.cellCount), [])]); - assert.ok(await vscode.workspace.applyEdit(edit)); - - assert.strictEqual(document.isDirty, true); - - await document.save(); - assert.strictEqual(document.isDirty, false); - }); - test('dirty state - serializer', async function () { const resource = await utils.createRandomFile(undefined, undefined, '.nbdserializer'); const document = await vscode.workspace.openNotebookDocument(resource); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 0c4cff5cf4b..fa92d91e8a2 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -124,18 +124,6 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { ] }; return dto; - }, - saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { - return { - id: '1', - delete: () => { } - }; } }; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts index aa22d484574..403b81454e9 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts @@ -20,6 +20,7 @@ suite('vscode API - types', () => { assert.ok(vscode.CodeActionKind.Refactor instanceof vscode.CodeActionKind); assert.ok(vscode.CodeActionKind.RefactorExtract instanceof vscode.CodeActionKind); assert.ok(vscode.CodeActionKind.RefactorInline instanceof vscode.CodeActionKind); + assert.ok(vscode.CodeActionKind.RefactorMove instanceof vscode.CodeActionKind); assert.ok(vscode.CodeActionKind.RefactorRewrite instanceof vscode.CodeActionKind); assert.ok(vscode.CodeActionKind.Source instanceof vscode.CodeActionKind); assert.ok(vscode.CodeActionKind.SourceOrganizeImports instanceof vscode.CodeActionKind); diff --git a/extensions/vscode-notebook-tests/src/extension.ts b/extensions/vscode-notebook-tests/src/extension.ts index 7160f07c1a1..4cd81a74756 100644 --- a/extensions/vscode-notebook-tests/src/extension.ts +++ b/extensions/vscode-notebook-tests/src/extension.ts @@ -43,18 +43,6 @@ export function activate(context: vscode.ExtensionContext): any { }; return dto; - }, - saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { - return { - id: '1', - delete: () => { } - }; } })); diff --git a/package.json b/package.json index ea705527d8c..7458844c9bf 100644 --- a/package.json +++ b/package.json @@ -86,13 +86,13 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "5.0.0-beta.54", - "xterm-addon-canvas": "0.2.0-beta.23", - "xterm-addon-search": "0.10.0-beta.6", - "xterm-addon-serialize": "0.8.0-beta.6", + "xterm": "5.0.0-beta.60", + "xterm-addon-canvas": "0.2.0-beta.26", + "xterm-addon-search": "0.10.0-beta.7", + "xterm-addon-serialize": "0.8.0-beta.7", "xterm-addon-unicode11": "0.4.0-beta.5", - "xterm-addon-webgl": "0.13.0-beta.49", - "xterm-headless": "5.0.0-beta.5", + "xterm-addon-webgl": "0.13.0-beta.55", + "xterm-headless": "4.20.0-beta.74", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -230,7 +230,7 @@ "@vscode/windows-registry": "1.0.6", "windows-foreground-love": "0.4.0", "windows-mutex": "0.4.1", - "windows-process-tree": "0.3.3" + "windows-process-tree": "0.3.4" }, "resolutions": { "elliptic": "^6.5.3", diff --git a/remote/package.json b/remote/package.json index d34e30223c7..5f6e845b83a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,18 +24,18 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "5.0.0-beta.54", - "xterm-addon-canvas": "0.2.0-beta.23", - "xterm-addon-search": "0.10.0-beta.6", - "xterm-addon-serialize": "0.8.0-beta.6", + "xterm": "5.0.0-beta.60", + "xterm-addon-canvas": "0.2.0-beta.26", + "xterm-addon-search": "0.10.0-beta.7", + "xterm-addon-serialize": "0.8.0-beta.7", "xterm-addon-unicode11": "0.4.0-beta.5", - "xterm-addon-webgl": "0.13.0-beta.49", - "xterm-headless": "5.0.0-beta.5", + "xterm-addon-webgl": "0.13.0-beta.55", + "xterm-headless": "4.20.0-beta.74", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "optionalDependencies": { "@vscode/windows-registry": "1.0.6", - "windows-process-tree": "0.3.3" + "windows-process-tree": "0.3.4" } } diff --git a/remote/web/package.json b/remote/web/package.json index 428d0f52a05..c147bc2d260 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,10 +11,10 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "5.0.0-beta.54", - "xterm-addon-canvas": "0.2.0-beta.23", - "xterm-addon-search": "0.10.0-beta.6", + "xterm": "5.0.0-beta.60", + "xterm-addon-canvas": "0.2.0-beta.26", + "xterm-addon-search": "0.10.0-beta.7", "xterm-addon-unicode11": "0.4.0-beta.5", - "xterm-addon-webgl": "0.13.0-beta.49" + "xterm-addon-webgl": "0.13.0-beta.55" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index b117c32ad58..6ca0789c3ad 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -68,27 +68,27 @@ vscode-textmate@7.0.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== -xterm-addon-canvas@0.2.0-beta.23: - version "0.2.0-beta.23" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.23.tgz#f5ee0db3b029ea705ef3c1228825c28ec6368b48" - integrity sha512-414qLxMlOzC3LyAt1qHmvrcW2VIPAsFQkXTGcSzX42XCOTF4lA9Jf8ePVNgokQAyvlGK3j3K0y0d7lTTR5I/Zw== +xterm-addon-canvas@0.2.0-beta.26: + version "0.2.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.26.tgz#db6d134177bac58d24e02d11c123f0cefb0e95b9" + integrity sha512-OZctolm/iUjSG11iYERJSu9ax2GBXe96ASYcHfJAeq19IMHadQvD3AWaJl25/MMChmvJ0qT1Q/+6p0ElgfV77Q== -xterm-addon-search@0.10.0-beta.6: - version "0.10.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.6.tgz#a475d793a13b378f56b439b8c7eeeff2095831ae" - integrity sha512-fDS0dbM/ZuVBfieWyXJgFvQwNk95rpVbaBRcVpUM9sM/R5+ePQr+uhcaicfuWAku7urP7P/QNnkeAkeQjf8E6w== +xterm-addon-search@0.10.0-beta.7: + version "0.10.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.7.tgz#77812514c4aa84668d9e247a9172618ccd2517d7" + integrity sha512-58dFGbLQc3C0Iww/Jq65HcXC9/RL+57duY5+rijts6KBZqAlGQCN3f2ORFKRvJEQDTgxOcnK9o9welyKK+PQ3Q== xterm-addon-unicode11@0.4.0-beta.5: version "0.4.0-beta.5" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.5.tgz#3900e66f10d2e506133b61d7421aab6878d32665" integrity sha512-+g+fuxAd/tkCEJ/jhdnebXKtdPrhsu4VKWNnB/3qM35GbuGQOasmYFYnKL+HYZMpbQ6YqeZcXTVg/wnCTttz0g== -xterm-addon-webgl@0.13.0-beta.49: - version "0.13.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.49.tgz#0cbffccccec06f5638ddc793ae7a0ff8ce0a891b" - integrity sha512-c1/8hLrw3PuPAnyPVLNg8i2FDkyu5SkU654DPEEgKgHHeAh3sfil28LleBpPhpP24531i7XNt1LLHCGMJ+gkFw== +xterm-addon-webgl@0.13.0-beta.55: + version "0.13.0-beta.55" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.55.tgz#d116fbb8d2e2bbfa562876f90d1aeedf48cc7eb8" + integrity sha512-i595z+lcbJaxLM7WTk845440lfyc3RERn/yWqTql+gnoA1YoP3gAnl/qdluFrKndM8sQGWmCsz9qACANXRjLbA== -xterm@5.0.0-beta.54: - version "5.0.0-beta.54" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.54.tgz#2c353221f289af22327aae6318bc6422c636fd41" - integrity sha512-wRzs1NbVCkZUzAqvglQcDVreT7RLLFkpdBi0oOLbZXgTaYr/Be93aCuuEjOVp7lnV0hi1gEP5K9Ugn621QffNw== +xterm@5.0.0-beta.60: + version "5.0.0-beta.60" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.60.tgz#1d16d6828f125c18c6d6e7db8769d1d848707a35" + integrity sha512-wkMXXfmwF9jIBtjSoEy7nyh54lDJz4wE0CuYzyBP/cjbTnjAkheeZcY9cJBlDRtP4NoZ7EhsA9GyXNeIrviiJg== diff --git a/remote/yarn.lock b/remote/yarn.lock index 0ac2187f123..12af52878b9 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -776,10 +776,10 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -windows-process-tree@0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.3.tgz#7c178815f02bf4cfbcac1f93b2f3a3cc10bc9245" - integrity sha512-rkiAMP0AS27xikFyn7i4gPbOK16UdjY8X/C6eo37CnfNLqTvK2eEaT+Dh0e5xnvmlsi0lEKd60O+4ajzfDkq7A== +windows-process-tree@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.4.tgz#6bc4b8010129c30ff95bcd333b9f94744dd3c4fb" + integrity sha512-rtSX73i9OnkDxSdBP9c1YBunEwheZdO/hjRwNk9uSoWqO92x0zDRGfIIK0MtUn8gZZD+2kPEVpj5MmfNl7JpJA== dependencies: nan "^2.13.2" @@ -788,40 +788,40 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-canvas@0.2.0-beta.23: - version "0.2.0-beta.23" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.23.tgz#f5ee0db3b029ea705ef3c1228825c28ec6368b48" - integrity sha512-414qLxMlOzC3LyAt1qHmvrcW2VIPAsFQkXTGcSzX42XCOTF4lA9Jf8ePVNgokQAyvlGK3j3K0y0d7lTTR5I/Zw== +xterm-addon-canvas@0.2.0-beta.26: + version "0.2.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.26.tgz#db6d134177bac58d24e02d11c123f0cefb0e95b9" + integrity sha512-OZctolm/iUjSG11iYERJSu9ax2GBXe96ASYcHfJAeq19IMHadQvD3AWaJl25/MMChmvJ0qT1Q/+6p0ElgfV77Q== -xterm-addon-search@0.10.0-beta.6: - version "0.10.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.6.tgz#a475d793a13b378f56b439b8c7eeeff2095831ae" - integrity sha512-fDS0dbM/ZuVBfieWyXJgFvQwNk95rpVbaBRcVpUM9sM/R5+ePQr+uhcaicfuWAku7urP7P/QNnkeAkeQjf8E6w== +xterm-addon-search@0.10.0-beta.7: + version "0.10.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.7.tgz#77812514c4aa84668d9e247a9172618ccd2517d7" + integrity sha512-58dFGbLQc3C0Iww/Jq65HcXC9/RL+57duY5+rijts6KBZqAlGQCN3f2ORFKRvJEQDTgxOcnK9o9welyKK+PQ3Q== -xterm-addon-serialize@0.8.0-beta.6: - version "0.8.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.6.tgz#fe21a74a0ca3ecdf12843136115f074a431b3876" - integrity sha512-hb3TRqvg36MW5H4ZnYjw4EHb55iZ4rOOuH+Hx4ZTBDI1pszPtryFqXbS93NBLKgsOqDovIDsH8fWvNfhPdGmsQ== +xterm-addon-serialize@0.8.0-beta.7: + version "0.8.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.7.tgz#73a71834a687c825ff3d3c824229fbd856d1570f" + integrity sha512-cghmB/2DYwX4HvjGMWmbxYO3NrvgfYWrQt0QGb0oToZh1gOgoEkUxZVZiOl5WlqFYpI+jHXXX48XgfFONZ1rMA== xterm-addon-unicode11@0.4.0-beta.5: version "0.4.0-beta.5" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.5.tgz#3900e66f10d2e506133b61d7421aab6878d32665" integrity sha512-+g+fuxAd/tkCEJ/jhdnebXKtdPrhsu4VKWNnB/3qM35GbuGQOasmYFYnKL+HYZMpbQ6YqeZcXTVg/wnCTttz0g== -xterm-addon-webgl@0.13.0-beta.49: - version "0.13.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.49.tgz#0cbffccccec06f5638ddc793ae7a0ff8ce0a891b" - integrity sha512-c1/8hLrw3PuPAnyPVLNg8i2FDkyu5SkU654DPEEgKgHHeAh3sfil28LleBpPhpP24531i7XNt1LLHCGMJ+gkFw== +xterm-addon-webgl@0.13.0-beta.55: + version "0.13.0-beta.55" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.55.tgz#d116fbb8d2e2bbfa562876f90d1aeedf48cc7eb8" + integrity sha512-i595z+lcbJaxLM7WTk845440lfyc3RERn/yWqTql+gnoA1YoP3gAnl/qdluFrKndM8sQGWmCsz9qACANXRjLbA== -xterm-headless@5.0.0-beta.5: - version "5.0.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.0.0-beta.5.tgz#e29b6c5081f31f887122b7263ba996b0c46b3c22" - integrity sha512-CMQ1+prBNF92oBMeZzc2rfTcmOaCGfwwSaoPYNTjyziZT6mZsEg7amajYkb0YAnqJ29MFm4kPGZbU78/dX4k2A== +xterm-headless@4.20.0-beta.74: + version "4.20.0-beta.74" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.74.tgz#1eade8cdfbf4389cadf0ae8b8b2cb536862323d2" + integrity sha512-WwHcSrnHGbqcRKJTDJgEJT4y4X5KPJxcMbi5RGj/T1FoXg/uYU23DO1RtvJV8ZnRKLbcY/Ru0wWf7ZGDrEk1DA== -xterm@5.0.0-beta.54: - version "5.0.0-beta.54" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.54.tgz#2c353221f289af22327aae6318bc6422c636fd41" - integrity sha512-wRzs1NbVCkZUzAqvglQcDVreT7RLLFkpdBi0oOLbZXgTaYr/Be93aCuuEjOVp7lnV0hi1gEP5K9Ugn621QffNw== +xterm@5.0.0-beta.60: + version "5.0.0-beta.60" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.60.tgz#1d16d6828f125c18c6d6e7db8769d1d848707a35" + integrity sha512-wkMXXfmwF9jIBtjSoEy7nyh54lDJz4wE0CuYzyBP/cjbTnjAkheeZcY9cJBlDRtP4NoZ7EhsA9GyXNeIrviiJg== yallist@^4.0.0: version "4.0.0" diff --git a/src/main.js b/src/main.js index 73fe1be2ade..884120e01b0 100644 --- a/src/main.js +++ b/src/main.js @@ -56,8 +56,7 @@ perf.mark('code/willStartCrashReporter'); // * --disable-crash-reporter command line parameter is not set // // Disable crash reporting in all other cases. -if (args['crash-reporter-directory'] || - (argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter'])) { +if (args['crash-reporter-directory'] || (argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter'])) { configureCrashReporter(); } perf.mark('code/didStartCrashReporter'); diff --git a/src/vs/base/browser/ui/iconLabel/iconLabels.ts b/src/vs/base/browser/ui/iconLabel/iconLabels.ts index 4fe1a5c79e8..5ecaddf78f1 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabels.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabels.ts @@ -14,7 +14,9 @@ export function renderLabelWithIcons(text: string): Array<HTMLSpanElement | stri let textStart = 0, textStop = 0; while ((match = labelWithIconsRegex.exec(text)) !== null) { textStop = match.index || 0; - elements.push(text.substring(textStart, textStop)); + if (textStart < textStop) { + elements.push(text.substring(textStart, textStop)); + } textStart = (match.index || 0) + match[0].length; const [, escaped, codicon] = match; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 054cf4de2d4..e8d7a30dfc4 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -493,32 +493,36 @@ export interface EmitterOptions { } -class EventProfiling { +export class EventProfiling { + + static readonly all = new Set<EventProfiling>(); private static _idPool = 0; - private _name: string; + readonly name: string; + public listenerCount: number = 0; + public invocationCount = 0; + public elapsedOverall = 0; + public durations: number[] = []; + private _stopWatch?: StopWatch; - private _listenerCount: number = 0; - private _invocationCount = 0; - private _elapsedOverall = 0; constructor(name: string) { - this._name = `${name}_${EventProfiling._idPool++}`; + this.name = `${name}_${EventProfiling._idPool++}`; + EventProfiling.all.add(this); } start(listenerCount: number): void { this._stopWatch = new StopWatch(true); - this._listenerCount = listenerCount; + this.listenerCount = listenerCount; } stop(): void { if (this._stopWatch) { const elapsed = this._stopWatch.elapsed(); - this._elapsedOverall += elapsed; - this._invocationCount += 1; - - console.info(`did FIRE ${this._name}: elapsed_ms: ${elapsed.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`); + this.durations.push(elapsed); + this.elapsedOverall += elapsed; + this.invocationCount += 1; this._stopWatch = undefined; } } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 5fd52a6fa76..506ee713281 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -15,6 +15,7 @@ let _isWeb = false; let _isElectron = false; let _isIOS = false; let _isCI = false; +let _isMobile = false; let _locale: string | undefined = undefined; let _language: string = LANGUAGE_DEFAULT; let _translationsConfigFile: string | undefined = undefined; @@ -79,6 +80,7 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isMacintosh = _userAgent.indexOf('Macintosh') >= 0; _isIOS = (_userAgent.indexOf('Macintosh') >= 0 || _userAgent.indexOf('iPad') >= 0 || _userAgent.indexOf('iPhone') >= 0) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0; _isLinux = _userAgent.indexOf('Linux') >= 0; + _isMobile = _userAgent?.indexOf('Mobi') >= 0; _isWeb = true; const configuredLocale = nls.getConfiguredDefaultLocale( @@ -157,6 +159,7 @@ export const isElectron = _isElectron; export const isWeb = _isWeb; export const isWebWorker = (_isWeb && typeof globals.importScripts === 'function'); export const isIOS = _isIOS; +export const isMobile = _isMobile; /** * Whether we run inside a CI environment, such as * GH actions or Azure Pipelines. diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index 9a554301a27..006cd42d707 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -10,12 +10,11 @@ 'use strict'; /** - * @param {NodeRequire} nodeRequire * @param {typeof import('path')} path * @param {typeof import('fs')} fs * @param {typeof import('../common/performance')} perf */ - function factory(nodeRequire, path, fs, perf) { + function factory(path, fs, perf) { /** * @param {string} file @@ -248,12 +247,12 @@ if (typeof define === 'function') { // amd - define(['require', 'path', 'fs', 'vs/base/common/performance'], function (require, /** @type {typeof import('path')} */ path, /** @type {typeof import('fs')} */ fs, /** @type {typeof import('../common/performance')} */ perf) { return factory(require.__$__nodeRequire, path, fs, perf); }); + define(['path', 'fs', 'vs/base/common/performance'], function (/** @type {typeof import('path')} */ path, /** @type {typeof import('fs')} */ fs, /** @type {typeof import('../common/performance')} */ perf) { return factory(path, fs, perf); }); } else if (typeof module === 'object' && typeof module.exports === 'object') { const path = require('path'); const fs = require('fs'); const perf = require('../common/performance'); - module.exports = factory(require, path, fs, perf); + module.exports = factory(path, fs, perf); } else { throw new Error('Unknown context'); } diff --git a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts index 227751e86c3..386580ae5a6 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { TestServiceClient } from './testService'; @@ -15,7 +16,9 @@ function createClient(): Client { }); } -suite('IPC, Child Process', () => { +suite('IPC, Child Process', function () { + this.timeout(10000); + test('createChannel', () => { const client = createClient(); const channel = client.getChannel('test'); @@ -45,10 +48,12 @@ suite('IPC, Child Process', () => { }); }); - const request = service.marco(); - const result = Promise.all([request, event]); + return timeout(100).then(() => { + const request = service.marco(); + const result = Promise.all([request, event]); - return result.finally(() => client.dispose()); + return result.finally(() => client.dispose()); + }); }); test('event dispose', () => { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 68053657b4c..2e57bc75e9f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -5,7 +5,6 @@ import { app, BrowserWindow, dialog, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; -import { statSync } from 'fs'; import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -109,9 +108,8 @@ import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } fro import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { UserDataTransientProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler'; -import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; -import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc'; +import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; /** * The main VS Code application. There will only ever be one instance, @@ -328,12 +326,12 @@ export class CodeApplication extends Disposable { }); // macOS dock activate - app.on('activate', (event, hasVisibleWindows) => { + app.on('activate', async (event, hasVisibleWindows) => { this.logService.trace('app#activate'); // Mac only event: open new window when we get activated if (!hasVisibleWindows) { - this.windowsMainService?.openEmptyWindow({ context: OpenContext.DOCK }); + await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DOCK }); } }); @@ -365,7 +363,7 @@ export class CodeApplication extends Disposable { event.preventDefault(); // Keep in array because more might come! - macOpenFileURIs.push(this.getWindowOpenableFromPathSync(path)); + macOpenFileURIs.push(hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) }); // Clear previous handler if any if (runningTimeout !== undefined) { @@ -374,8 +372,8 @@ export class CodeApplication extends Disposable { } // Handle paths delayed in case more are coming! - runningTimeout = setTimeout(() => { - this.windowsMainService?.open({ + runningTimeout = setTimeout(async () => { + await this.windowsMainService?.open({ context: OpenContext.DOCK /* can also be opening from finder while app is running */, cli: this.environmentMainService.args, urisToOpen: macOpenFileURIs, @@ -388,8 +386,8 @@ export class CodeApplication extends Disposable { }, 100); }); - app.on('new-window-for-tab', () => { - this.windowsMainService?.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button + app.on('new-window-for-tab', async () => { + await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button }); //#region Bootstrap IPC Handlers @@ -539,15 +537,11 @@ export class CodeApplication extends Disposable { // Setup Handlers this.setUpHandlers(appInstantiationService); - // Ensure profile exists when passed in from CLI - const profilePromise = this.userDataProfilesMainService.checkAndCreateProfileFromCli(this.environmentMainService.args); - const profile = profilePromise ? await profilePromise : undefined; - // Init Channels appInstantiationService.invokeFunction(accessor => this.initChannels(accessor, mainProcessElectronServer, sharedProcessClient)); // Open Windows - appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, profile, mainProcessElectronServer)); + await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, mainProcessElectronServer)); // Post Open Windows Tasks appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor, sharedProcess)); @@ -633,7 +627,8 @@ export class CodeApplication extends Disposable { services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv], false)); // Dialogs - services.set(IDialogMainService, new SyncDescriptor(DialogMainService, undefined, true)); + const dialogMainService = new DialogMainService(this.logService); + services.set(IDialogMainService, dialogMainService); // Launch services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService, undefined, false /* proxied to other processes */)); @@ -660,10 +655,6 @@ export class CodeApplication extends Disposable { // Webview Manager services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); - // Workspaces - services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService, undefined, false /* proxied to other processes */)); - services.set(IWorkspacesManagementMainService, new SyncDescriptor(WorkspacesManagementMainService, undefined, true)); - services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService, undefined, false)); // Menubar services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService)); @@ -691,6 +682,12 @@ export class CodeApplication extends Disposable { const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService, this.stateMainService); services.set(IBackupMainService, backupMainService); + // Workspaces + const workspacesManagementMainService = new WorkspacesManagementMainService(this.environmentMainService, this.logService, this.userDataProfilesMainService, backupMainService, dialogMainService, this.productService); + services.set(IWorkspacesManagementMainService, workspacesManagementMainService); + services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService, undefined, false /* proxied to other processes */)); + services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService, undefined, false)); + // URL handling services.set(IURLService, new SyncDescriptor(NativeURLService, undefined, false /* proxied to other processes */)); @@ -713,7 +710,10 @@ export class CodeApplication extends Disposable { services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); // Init services that require it - await backupMainService.initialize(); + await Promises.settled([ + backupMainService.initialize(), + workspacesManagementMainService.initialize() + ]); return this.mainInstantiationService.createChild(services); } @@ -834,7 +834,7 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel); } - private openFirstWindow(accessor: ServicesAccessor, profile: IUserDataProfile | undefined, mainProcessElectronServer: ElectronIPCServer): ICodeWindow[] { + private async openFirstWindow(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer): Promise<ICodeWindow[]> { const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); const urlService = accessor.get(IURLService); const nativeHostMainService = accessor.get(INativeHostMainService); @@ -922,7 +922,7 @@ export class CodeApplication extends Disposable { const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); logService.trace('app#handleURL: windowOpenableFromProtocolLink = ', windowOpenableFromProtocolLink); if (windowOpenableFromProtocolLink) { - const [window] = windowsMainService.open({ + const [window] = await windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], @@ -937,7 +937,7 @@ export class CodeApplication extends Disposable { } if (shouldOpenInNewWindow) { - const [window] = windowsMainService.open({ + const [window] = await windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, forceNewWindow: true, @@ -994,6 +994,9 @@ export class CodeApplication extends Disposable { }); } + // Ensure profile exists when passed in from CLI + const profile = await this.userDataProfilesMainService.checkAndCreateProfileFromCli(this.environmentMainService.args); + // Start without file/folder arguments if (!hasCliArgs && !hasFolderURIs && !hasFileURIs) { @@ -1017,7 +1020,7 @@ export class CodeApplication extends Disposable { return windowsMainService.open({ context: OpenContext.DOCK, cli: args, - urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)), + urisToOpen: macOpenFiles.map(path => (hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) })), noRecentEntry, waitMarkerFileURI, initialStartup: true, @@ -1109,23 +1112,6 @@ export class CodeApplication extends Disposable { return undefined; } - private getWindowOpenableFromPathSync(path: string): IWindowOpenable { - try { - const fileStat = statSync(path); - if (fileStat.isDirectory()) { - return { folderUri: URI.file(path) }; - } - - if (hasWorkspaceFileExtension(path)) { - return { workspaceUri: URI.file(path) }; - } - } catch (error) { - // ignore errors - } - - return { fileUri: URI.file(path) }; - } - private afterWindowOpen(accessor: ServicesAccessor, sharedProcess: SharedProcess): void { const telemetryService = accessor.get(ITelemetryService); const updateService = accessor.get(IUpdateService); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index d2e59149d7d..e358efbb545 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -34,7 +34,7 @@ import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsServ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { addArg, parseMainProcessArgv } from 'vs/platform/environment/node/argvHelper'; -import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import { createWaitMarkerFileSync } from 'vs/platform/environment/node/wait'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -469,8 +469,9 @@ class CodeMain { // // Note: we are not doing this if the wait marker has been already // added as argument. This can happen if Code was started from CLI. + if (args.wait && !args.waitMarkerFilePath) { - const waitMarkerFilePath = createWaitMarkerFile(args.verbose); + const waitMarkerFilePath = createWaitMarkerFileSync(args.verbose); if (waitMarkerFilePath) { addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath); args.waitMarkerFilePath = waitMarkerFilePath; diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 190d64038e9..daae8a10f16 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -19,7 +19,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { addArg, parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from 'vs/platform/environment/node/stdin'; -import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import { createWaitMarkerFileSync } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { randomPath } from 'vs/base/common/extpath'; @@ -220,7 +220,7 @@ export async function main(argv: string[]): Promise<any> { // is closed and then exit the waiting process. let waitMarkerFilePath: string | undefined; if (args.wait) { - waitMarkerFilePath = createWaitMarkerFile(verbose); + waitMarkerFilePath = createWaitMarkerFileSync(verbose); if (waitMarkerFilePath) { addArg(argv, '--waitMarkerFilePath', waitMarkerFilePath); } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 8ce775febd3..33623ac6b3c 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -195,6 +195,9 @@ export class TextAreaHandler extends ViewPart { }, getValueInRange: (range: Range, eol: EndOfLinePreference): string => { return this._context.viewModel.getValueInRange(range, eol); + }, + getValueLengthInRange: (range: Range): number => { + return this._context.viewModel.model.getValueLengthInRange(range); } }; @@ -242,6 +245,15 @@ export class TextAreaHandler extends ViewPart { return new TextAreaState(textBefore, textBefore.length, textBefore.length, position, position); } } + // on macOS, write current selection into textarea will allow system text services pick selected text, + // but we still want to limit the amount of text given Chromium handles very poorly text even of a few + // thousand chars + // (https://github.com/microsoft/vscode/issues/27799) + const LIMIT_CHARS = 500; + if (platform.isMacintosh && !selection.isEmpty() && simpleModel.getValueLengthInRange(selection) < LIMIT_CHARS) { + const text = simpleModel.getValueInRange(selection, EndOfLinePreference.TextDefined); + return new TextAreaState(text, 0, text.length, selection.getStartPosition(), selection.getEndPosition()); + } // on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work // if the textarea has no content selected. So if there is an editor selection, ensure something diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index 97b381b66c4..0953b7a0594 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -23,6 +23,7 @@ export interface ISimpleModel { getLineCount(): number; getLineMaxColumn(lineNumber: number): number; getValueInRange(range: Range, eol: EndOfLinePreference): string; + getValueLengthInRange(range: Range): number; } export interface ITypeData { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index bf2cc58e639..5cefe83dfc8 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -849,9 +849,11 @@ export interface ITextModel { /** * Set the current language mode associated with the model. + * @param languageId The new language. + * @param source The source of the call that set the language. * @internal */ - setMode(languageId: string): void; + setMode(languageId: string, source?: string): void; /** * Returns the real (inner-most) language mode at a given position. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f099642e294..102dc04c973 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1907,8 +1907,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this.tokenization.getLanguageId(); } - public setMode(languageId: string): void { - this.tokenization.setLanguageId(languageId); + public setMode(languageId: string, source?: string): void { + this.tokenization.setLanguageId(languageId, source); } public getLanguageIdAtPosition(lineNumber: number, column: number): string { diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index cc2cf81f02b..378dcb56589 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -485,7 +485,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); } - public setLanguageId(languageId: string): void { + public setLanguageId(languageId: string, source: string = 'api'): void { if (this._languageId === languageId) { // There's nothing to do return; @@ -493,7 +493,8 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz const e: IModelLanguageChangedEvent = { oldLanguage: this._languageId, - newLanguage: languageId + newLanguage: languageId, + source }; this._languageId = languageId; diff --git a/src/vs/editor/common/services/model.ts b/src/vs/editor/common/services/model.ts index 037ac0ca270..88cc4606c7e 100644 --- a/src/vs/editor/common/services/model.ts +++ b/src/vs/editor/common/services/model.ts @@ -22,7 +22,7 @@ export interface IModelService { updateModel(model: ITextModel, value: string | ITextBufferFactory): void; - setMode(model: ITextModel, languageSelection: ILanguageSelection): void; + setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void; destroyModel(resource: URI): void; diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index aba06a6a51f..c03821b7b0c 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -91,11 +91,11 @@ class ModelData implements IDisposable { this._disposeLanguageSelection(); } - public setLanguage(languageSelection: ILanguageSelection): void { + public setLanguage(languageSelection: ILanguageSelection, source?: string): void { this._disposeLanguageSelection(); this._languageSelection = languageSelection; - this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId)); - this.model.setMode(languageSelection.languageId); + this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId, source)); + this.model.setMode(languageSelection.languageId, source); } } @@ -516,7 +516,7 @@ export class ModelService extends Disposable implements IModelService { return modelData.model; } - public setMode(model: ITextModel, languageSelection: ILanguageSelection): void { + public setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void { if (!languageSelection) { return; } @@ -524,7 +524,7 @@ export class ModelService extends Disposable implements IModelService { if (!modelData) { return; } - modelData.setLanguage(languageSelection); + modelData.setLanguage(languageSelection, source); } public destroyModel(resource: URI): void { diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index aefa7dca03b..a2f38b12900 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -19,6 +19,11 @@ export interface IModelLanguageChangedEvent { * New language */ readonly newLanguage: string; + + /** + * Source of the call that caused the event. + */ + readonly source: string; } /** diff --git a/src/vs/editor/common/tokenizationTextModelPart.ts b/src/vs/editor/common/tokenizationTextModelPart.ts index 6c8e7c322ed..bade56184c8 100644 --- a/src/vs/editor/common/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/tokenizationTextModelPart.ts @@ -95,7 +95,7 @@ export interface ITokenizationTextModelPart { getLanguageId(): string; getLanguageIdAtPosition(lineNumber: number, column: number): string; - setLanguageId(languageId: string): void; + setLanguageId(languageId: string, source?: string): void; readonly backgroundTokenizationState: BackgroundTokenizationState; readonly onBackgroundTokenizationStateChanged: Event<void>; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts b/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts new file mode 100644 index 00000000000..b65d75bc5b2 --- /dev/null +++ b/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { Lazy } from 'vs/base/common/lazy'; +import { CodeAction } from 'vs/editor/common/languages'; +import { codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +export interface ResolveCodeActionKeybinding { + readonly kind: CodeActionKind; + readonly preferred: boolean; + readonly resolvedKeybinding: ResolvedKeybinding; +} + +export class CodeActionKeybindingResolver { + private static readonly codeActionCommands: readonly string[] = [ + refactorCommandId, + codeActionCommandId, + sourceActionCommandId, + organizeImportsCommandId, + fixAllCommandId + ]; + + constructor( + private readonly keybindingService: IKeybindingService + ) { } + + public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined { + // Lazy since we may not actually ever read the value + const allCodeActionBindings = new Lazy<readonly ResolveCodeActionKeybinding[]>(() => this.keybindingService.getKeybindings() + .filter(item => CodeActionKeybindingResolver.codeActionCommands.indexOf(item.command!) >= 0) + .filter(item => item.resolvedKeybinding) + .map((item): ResolveCodeActionKeybinding => { + // Special case these commands since they come built-in with VS Code and don't use 'commandArgs' + let commandArgs = item.commandArgs; + if (item.command === organizeImportsCommandId) { + commandArgs = { kind: CodeActionKind.SourceOrganizeImports.value }; + } else if (item.command === fixAllCommandId) { + commandArgs = { kind: CodeActionKind.SourceFixAll.value }; + } + + return { + resolvedKeybinding: item.resolvedKeybinding!, + ...CodeActionCommandArgs.fromUser(commandArgs, { + kind: CodeActionKind.None, + apply: CodeActionAutoApply.Never + }) + }; + })); + + return (action) => { + if (action.kind) { + const binding = this.bestKeybindingForCodeAction(action, allCodeActionBindings.getValue()); + return binding?.resolvedKeybinding; + } + return undefined; + }; + } + + private bestKeybindingForCodeAction( + action: CodeAction, + candidates: readonly ResolveCodeActionKeybinding[] + ): ResolveCodeActionKeybinding | undefined { + if (!action.kind) { + return undefined; + } + const kind = new CodeActionKind(action.kind); + + return candidates + .filter(candidate => candidate.kind.contains(kind)) + .filter(candidate => { + if (candidate.preferred) { + // If the candidate keybinding only applies to preferred actions, the this action must also be preferred + return action.isPreferred; + } + return true; + }) + .reduceRight((currentBest, candidate) => { + if (!currentBest) { + return candidate; + } + // Select the more specific binding + return currentBest.kind.contains(candidate.kind) ? candidate : currentBest; + }, undefined as ResolveCodeActionKeybinding | undefined); + } +} diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index c01c0f51c43..f51c051b10b 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts @@ -12,17 +12,15 @@ import { IListEvent, IListMouseEvent, IListRenderer } from 'vs/base/browser/ui/l import { List } from 'vs/base/browser/ui/list/listWidget'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; import 'vs/css!./media/action'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { CodeAction, Command } from 'vs/editor/common/languages'; +import { Command } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; +import { CodeActionItem, CodeActionSet } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -31,6 +29,7 @@ import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/cont import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver'; export const Context = { Visible: new RawContextKey<boolean>('codeActionMenuVisible', false, localize('codeActionMenuVisible', "Whether the code action list widget is visible")) @@ -43,16 +42,6 @@ interface CodeActionWidgetDelegate { onSelectCodeAction: (action: CodeActionItem, trigger: CodeActionTrigger) => Promise<any>; } -interface ResolveCodeActionKeybinding { - readonly kind: CodeActionKind; - readonly preferred: boolean; - readonly resolvedKeybinding: ResolvedKeybinding; -} - -function stripNewlines(str: string): string { - return str.replace(/\r\n|\r|\n/g, ' '); -} - export interface CodeActionShowOptions { readonly includeDisabledActions: boolean; readonly fromLightbulb?: boolean; @@ -66,13 +55,12 @@ enum CodeActionListItemKind { interface CodeActionListItemCodeAction { readonly kind: CodeActionListItemKind.CodeAction; readonly action: CodeActionItem; - readonly index: number; + readonly group: CodeActionGroup; } interface CodeActionListItemHeader { readonly kind: CodeActionListItemKind.Header; - readonly headerTitle: string; - readonly index: number; + readonly group: CodeActionGroup; } type ICodeActionMenuItem = CodeActionListItemCodeAction | CodeActionListItemHeader; @@ -84,6 +72,28 @@ interface ICodeActionMenuTemplateData { readonly keybinding: KeybindingLabel; } +function stripNewlines(str: string): string { + return str.replace(/\r\n|\r|\n/g, ' '); +} + +interface CodeActionGroup { + readonly kind: CodeActionKind; + readonly title: string; + readonly icon: Codicon; + readonly iconColor?: string; +} + +const uncategorizedCodeActionGroup = Object.freeze<CodeActionGroup>({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...'), icon: Codicon.lightBulb, iconColor: 'var(--vscode-editorLightBulb-foreground)' }); + +const codeActionGroups = Object.freeze<CodeActionGroup[]>([ + { kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix...'), icon: Codicon.lightBulb, }, + { kind: CodeActionKind.Extract, title: localize('codeAction.widget.id.extract', 'Extract...'), icon: Codicon.wrench, }, + { kind: CodeActionKind.Convert, title: localize('codeAction.widget.id.convert', 'Convert...'), icon: Codicon.zap, iconColor: 'var(--vscode-editorLightBulbAutoFix-foreground)' }, + { kind: CodeActionKind.SurroundWith, title: localize('codeAction.widget.id.surround', 'Surround With...'), icon: Codicon.symbolArray, }, + { kind: CodeActionKind.Source, title: localize('codeAction.widget.id.source', 'Source Action...'), icon: Codicon.lightBulb, iconColor: 'var(--vscode-editorLightBulb-foreground)' }, + uncategorizedCodeActionGroup, +]); + class CodeActionItemRenderer implements IListRenderer<CodeActionListItemCodeAction, ICodeActionMenuTemplateData> { constructor( private readonly keybindingResolver: CodeActionKeybindingResolver, @@ -109,22 +119,8 @@ class CodeActionItemRenderer implements IListRenderer<CodeActionListItemCodeActi } renderElement(element: CodeActionListItemCodeAction, _index: number, data: ICodeActionMenuTemplateData): void { - // Icons and Label modification based on group - const kind = element.action.action.kind ? new CodeActionKind(element.action.action.kind) : CodeActionKind.None; - if (CodeActionKind.SurroundWith.contains(kind)) { - data.icon.className = Codicon.symbolArray.classNames; - } else if (CodeActionKind.Extract.contains(kind)) { - data.icon.className = Codicon.wrench.classNames; - } else if (CodeActionKind.Convert.contains(kind)) { - data.icon.className = Codicon.zap.classNames; - data.icon.style.color = `var(--vscode-editorLightBulbAutoFix-foreground)`; - } else if (CodeActionKind.QuickFix.contains(kind)) { - data.icon.className = Codicon.lightBulb.classNames; - data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`; - } else { - data.icon.className = Codicon.lightBulb.classNames; - data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`; - } + data.icon.className = element.group.icon.classNames; + data.icon.style.color = element.group.iconColor ?? ''; data.text.textContent = stripNewlines(element.action.action.title); @@ -136,21 +132,11 @@ class CodeActionItemRenderer implements IListRenderer<CodeActionListItemCodeActi dom.show(data.keybinding.element); } - // Check if action has disabled reason if (element.action.action.disabled) { data.container.title = element.action.action.disabled; - } else { - const updateLabel = () => { - data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", this.keybindingService.lookupKeybinding(acceptSelectedCodeActionCommand)?.getLabel(), this.keybindingService.lookupKeybinding(previewSelectedCodeActionCommand)?.getLabel()); - }; - updateLabel(); - } - - if (element.action.action.disabled) { data.container.classList.add('option-disabled'); - data.container.style.backgroundColor = 'transparent !important'; - data.icon.style.opacity = '0.4'; } else { + data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", this.keybindingService.lookupKeybinding(acceptSelectedCodeActionCommand)?.getLabel(), this.keybindingService.lookupKeybinding(previewSelectedCodeActionCommand)?.getLabel()); data.container.classList.remove('option-disabled'); } } @@ -179,7 +165,7 @@ class HeaderRenderer implements IListRenderer<CodeActionListItemHeader, HeaderTe } renderElement(element: CodeActionListItemHeader, _index: number, templateData: HeaderTemplateData): void { - templateData.text.textContent = element.headerTitle; + templateData.text.textContent = element.group.title; } disposeTemplate(_templateData: HeaderTemplateData): void { @@ -197,9 +183,6 @@ class CodeActionList extends Disposable { private readonly list: List<ICodeActionMenuItem>; private readonly allMenuItems: ICodeActionMenuItem[]; - private readonly viewItems: readonly CodeActionListItemCodeAction[]; - private focusedEnabledItem?: number; - private currSelectedItem?: number; constructor( codeActions: readonly CodeActionItem[], @@ -220,7 +203,6 @@ class CodeActionList extends Disposable { new HeaderRenderer(), ], { keyboardSupport: false, - mouseSupport: false, accessibilityProvider: { getAriaLabel: element => { if (element.kind === CodeActionListItemKind.CodeAction) { @@ -244,14 +226,9 @@ class CodeActionList extends Disposable { this._register(this.list.onDidChangeSelection(e => this.onListSelection(e))); this.allMenuItems = this.toMenuItems(codeActions, showHeaders); - this.viewItems = this.allMenuItems.filter(item => item.kind === CodeActionListItemKind.CodeAction && !item.action.action.disabled) as CodeActionListItemCodeAction[]; this.list.splice(0, this.list.length, this.allMenuItems); - if (this.viewItems.length >= 1) { - this.focusedEnabledItem = 0; - this.currSelectedItem = this.viewItems[0].index; - this.list.setFocus([this.currSelectedItem]); - } + this.focusNext(); } public layout(minWidth: number): number { @@ -284,126 +261,75 @@ class CodeActionList extends Disposable { } public focusPrevious() { - if (typeof this.focusedEnabledItem === 'undefined') { - this.focusedEnabledItem = this.viewItems[0].index; - } else if (this.viewItems.length < 1) { - return; - } - - const startIndex = this.focusedEnabledItem; - let item: ICodeActionMenuItem; - - do { - this.focusedEnabledItem = this.focusedEnabledItem - 1; - if (this.focusedEnabledItem < 0) { - this.focusedEnabledItem = this.viewItems.length - 1; - } - item = this.viewItems[this.focusedEnabledItem]; - this.list.setFocus([item.index]); - this.currSelectedItem = item.index; - } while (this.focusedEnabledItem !== startIndex && item.action.action.disabled); + this.list.focusPrevious(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled); } public focusNext() { - if (typeof this.focusedEnabledItem === 'undefined') { - this.focusedEnabledItem = this.viewItems.length - 1; - } else if (this.viewItems.length < 1) { + this.list.focusNext(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled); + } + + public acceptSelected() { + const focused = this.list.getFocus(); + if (focused.length === 0) { return; } - const startIndex = this.focusedEnabledItem; - let item: ICodeActionMenuItem; + const focusIndex = focused[0]; + const element = this.list.element(focusIndex); + if (element.kind !== CodeActionListItemKind.CodeAction || element.action.action.disabled) { + return; + } - do { - this.focusedEnabledItem = (this.focusedEnabledItem + 1) % this.viewItems.length; - item = this.viewItems[this.focusedEnabledItem]; - this.list.setFocus([item.index]); - this.currSelectedItem = item.index; - } while (this.focusedEnabledItem !== startIndex && item.action.action.disabled); + this.list.setSelection([focusIndex]); } - public onEnterSet() { - if (typeof this.currSelectedItem === 'number') { - this.list.setSelection([this.currSelectedItem]); + private onListSelection(e: IListEvent<ICodeActionMenuItem>): void { + if (!e.elements.length) { + return; } - } - private onListSelection(e: IListEvent<ICodeActionMenuItem>): void { - for (const element of e.elements) { - if (element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled) { - this.onDidSelect(element.action); - } + const element = e.elements[0]; + if (element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled) { + this.onDidSelect(element.action); + } else { + this.list.setSelection([]); } } private onListHover(e: IListMouseEvent<ICodeActionMenuItem>): void { - if (!e.element) { - this.currSelectedItem = undefined; - this.list.setFocus([]); - } else { - if (e.element.kind === CodeActionListItemKind.CodeAction && !e.element.action.action.disabled) { - this.list.setFocus([e.element.index]); - this.focusedEnabledItem = this.viewItems.indexOf(e.element); - this.currSelectedItem = e.element.index; - } else { - this.currSelectedItem = undefined; - this.list.setFocus([e.element.index]); - } - } + this.list.setFocus(typeof e.index === 'number' ? [e.index] : []); } private onListClick(e: IListMouseEvent<ICodeActionMenuItem>): void { if (e.element && e.element.kind === CodeActionListItemKind.CodeAction && e.element.action.action.disabled) { - this.currSelectedItem = undefined; this.list.setFocus([]); } } private toMenuItems(inputCodeActions: readonly CodeActionItem[], showHeaders: boolean): ICodeActionMenuItem[] { if (!showHeaders) { - return inputCodeActions.map((action, index): ICodeActionMenuItem => ({ kind: CodeActionListItemKind.CodeAction, action, index })); + return inputCodeActions.map((action): ICodeActionMenuItem => ({ kind: CodeActionListItemKind.CodeAction, action, group: uncategorizedCodeActionGroup })); } - // Groups code actions by their kind - const quickfixGroup: CodeActionItem[] = []; - const extractGroup: CodeActionItem[] = []; - const convertGroup: CodeActionItem[] = []; - const surroundGroup: CodeActionItem[] = []; - const sourceGroup: CodeActionItem[] = []; - const otherGroup: CodeActionItem[] = []; + // Group code actions + const menuEntries = codeActionGroups.map(group => ({ group, actions: [] as CodeActionItem[] })); for (const action of inputCodeActions) { const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None; - if (CodeActionKind.SurroundWith.contains(kind)) { - surroundGroup.push(action); - } else if (CodeActionKind.QuickFix.contains(kind)) { - quickfixGroup.push(action); - } else if (CodeActionKind.Extract.contains(kind)) { - extractGroup.push(action); - } else if (CodeActionKind.Convert.contains(kind)) { - convertGroup.push(action); - } else if (CodeActionKind.Source.contains(kind)) { - sourceGroup.push(action); - } else { - otherGroup.push(action); + for (const menuEntry of menuEntries) { + if (menuEntry.group.kind.contains(kind)) { + menuEntry.actions.push(action); + break; + } } } - const menuEntries: ReadonlyArray<{ title: string; actions: CodeActionItem[] }> = [ - { title: localize('codeAction.widget.id.quickfix', 'Quick Fix...'), actions: quickfixGroup }, - { title: localize('codeAction.widget.id.extract', 'Extract...'), actions: extractGroup }, - { title: localize('codeAction.widget.id.convert', 'Convert...'), actions: convertGroup }, - { title: localize('codeAction.widget.id.surround', 'Surround With...'), actions: surroundGroup }, - { title: localize('codeAction.widget.id.source', 'Source Action...'), actions: sourceGroup }, - { title: localize('codeAction.widget.id.more', 'More Actions...'), actions: otherGroup }, - ]; - const allMenuItems: ICodeActionMenuItem[] = []; for (const menuEntry of menuEntries) { if (menuEntry.actions.length) { - allMenuItems.push({ kind: CodeActionListItemKind.Header, headerTitle: menuEntry.title, index: allMenuItems.length }); + allMenuItems.push({ kind: CodeActionListItemKind.Header, group: menuEntry.group }); for (const action of menuEntry.actions) { - allMenuItems.push({ kind: CodeActionListItemKind.CodeAction, action, index: allMenuItems.length }); + allMenuItems.push({ kind: CodeActionListItemKind.CodeAction, action, group: menuEntry.group }); } } } @@ -431,7 +357,8 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { readonly anchor: IAnchor; readonly codeActions: CodeActionSet; }; - private _ctxMenuWidgetVisible: IContextKey<boolean>; + + private readonly _ctxMenuWidgetVisible: IContextKey<boolean>; constructor( private readonly _editor: ICodeEditor, @@ -487,11 +414,11 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { this.codeActionList.value?.focusNext(); } - public onEnterSet() { - this.codeActionList.value?.onEnterSet(); + public acceptSelected() { + this.codeActionList.value?.acceptSelected(); } - public hideCodeActionWidget() { + public hide() { this._ctxMenuWidgetVisible.reset(); this.codeActionList.clear(); this._contextViewService.hideContextView(); @@ -513,7 +440,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { showingCodeActions, this.shouldShowHeaders(), action => { - this.hideCodeActionWidget(); + this.hide(); this._delegate.onSelectCodeAction(action, trigger); }, this._keybindingService); @@ -563,12 +490,10 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { const width = this.codeActionList.value.layout(actionBarWidth); widget.style.width = `${width}px`; - renderDisposables.add(this._editor.onDidLayoutChange(() => this.hideCodeActionWidget())); + renderDisposables.add(this._editor.onDidLayoutChange(() => this.hide())); const focusTracker = renderDisposables.add(dom.trackFocus(element)); - renderDisposables.add(focusTracker.onDidBlur(() => { - this.hideCodeActionWidget(); - })); + renderDisposables.add(focusTracker.onDidBlur(() => this.hide())); this._ctxMenuWidgetVisible.set(true); @@ -581,7 +506,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { private toggleShowDisabled(newShowDisabled: boolean): void { const previouslyShowingActions = this.currentShowingContext; - this.hideCodeActionWidget(); + this.hide(); showDisabled = newShowDisabled; @@ -676,77 +601,3 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { })); } } - -export class CodeActionKeybindingResolver { - private static readonly codeActionCommands: readonly string[] = [ - refactorCommandId, - codeActionCommandId, - sourceActionCommandId, - organizeImportsCommandId, - fixAllCommandId - ]; - - constructor( - private readonly keybindingService: IKeybindingService, - ) { } - - public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined { - // Lazy since we may not actually ever read the value - const allCodeActionBindings = new Lazy<readonly ResolveCodeActionKeybinding[]>(() => - this.keybindingService.getKeybindings() - .filter(item => CodeActionKeybindingResolver.codeActionCommands.indexOf(item.command!) >= 0) - .filter(item => item.resolvedKeybinding) - .map((item): ResolveCodeActionKeybinding => { - // Special case these commands since they come built-in with VS Code and don't use 'commandArgs' - let commandArgs = item.commandArgs; - if (item.command === organizeImportsCommandId) { - commandArgs = { kind: CodeActionKind.SourceOrganizeImports.value }; - } else if (item.command === fixAllCommandId) { - commandArgs = { kind: CodeActionKind.SourceFixAll.value }; - } - - return { - resolvedKeybinding: item.resolvedKeybinding!, - ...CodeActionCommandArgs.fromUser(commandArgs, { - kind: CodeActionKind.None, - apply: CodeActionAutoApply.Never - }) - }; - })); - - return (action) => { - if (action.kind) { - const binding = this.bestKeybindingForCodeAction(action, allCodeActionBindings.getValue()); - return binding?.resolvedKeybinding; - } - return undefined; - }; - } - - private bestKeybindingForCodeAction( - action: CodeAction, - candidates: readonly ResolveCodeActionKeybinding[], - ): ResolveCodeActionKeybinding | undefined { - if (!action.kind) { - return undefined; - } - const kind = new CodeActionKind(action.kind); - - return candidates - .filter(candidate => candidate.kind.contains(kind)) - .filter(candidate => { - if (candidate.preferred) { - // If the candidate keybinding only applies to preferred actions, the this action must also be preferred - return action.isPreferred; - } - return true; - }) - .reduceRight((currentBest, candidate) => { - if (!currentBest) { - return candidate; - } - // Select the more specific binding - return currentBest.kind.contains(candidate.kind) ? candidate : currentBest; - }, undefined as ResolveCodeActionKeybinding | undefined); - } -} diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts index 5d692619870..d5af03fc557 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts @@ -66,11 +66,11 @@ export class CodeActionUi extends Disposable { } public hideCodeActionWidget() { - this._codeActionWidget.rawValue?.hideCodeActionWidget(); + this._codeActionWidget.rawValue?.hide(); } public onEnter() { - this._codeActionWidget.rawValue?.onEnterSet(); + this._codeActionWidget.rawValue?.acceptSelected(); } public onPreviewEnter() { diff --git a/src/vs/editor/contrib/codeAction/browser/media/action.css b/src/vs/editor/contrib/codeAction/browser/media/action.css index a2ed5cc2470..2561c7f4f04 100644 --- a/src/vs/editor/contrib/codeAction/browser/media/action.css +++ b/src/vs/editor/contrib/codeAction/browser/media/action.css @@ -3,21 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.codeActionWidget .monaco-list:not(.element-focused):focus:before { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 5; /* make sure we are on top of the tree items */ - content: ""; - pointer-events: none; /* enable click through */ - outline: 0 solid !important; /* we still need to handle the empty tree or no focus item case */ - outline-width: 0 !important; - outline-style: none; - outline-offset: 0; -} - .codeActionWidget { font-size: 13px; border-radius: 0; @@ -26,11 +11,11 @@ z-index: 40; display: block; width: 100%; - border: 1px solid var(--vscode-menu-separatorBackground) !important; + border: 1px solid var(--vscode-editorWidget-border) !important; border-color: none; - background-color: var(--vscode-menu-background); - color: var(--vscode-menu-foreground); - box-shadow: rgb(0,0,0, 16%) 0 2px 8px; + background-color: var(--vscode-editorWidget-background); + color: var(--vscode--editorWidget-foreground); + box-shadow: var(--vscode-widget-shadow) 0 2px 8px; } .codeActionWidget .monaco-list { @@ -57,26 +42,15 @@ width: 100%; } -.codeActionWidget .monaco-list .monaco-list-row.code-action:hover:not(.option-disabled), -.codeActionWidget .monaco-list .monaco-list-row.code-action.focused:not(.option-disabled) { - background-color: var(--vscode-list-hoverBackground) !important; - color: var(--vscode-list-activeSelectionForeground) !important; -} - -.codeActionWidget .monaco-list .monaco-list-row.code-action:hover:not(.option-disabled) { - background-color: var(--vscode-list-hoverBackground) !important; - outline: 1px dashed var(--vscode-menu-selectionBorder, transparent); - outline-offset: -1px; -} - .codeActionWidget .monaco-list .monaco-list-row.code-action.focused:not(.option-disabled) { - background-color: var(--vscode-menu-selectionBackground) !important; + background-color: var(--vscode-quickInputList-focusBackground); + color: var(--vscode-quickInputList-focusForeground); outline: 1px solid var(--vscode-menu-selectionBorder, transparent); outline-offset: -1px; } .codeActionWidget .monaco-list-row.group-header { - color: var(--vscode-textLink-activeForeground); + color: var(--vscode-pickerGroup-foreground); font-weight: bold; } @@ -92,7 +66,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - background-color: var(--vscode-menu-background) !important; + background-color: transparent !important; outline: 0 solid !important; } @@ -106,6 +80,10 @@ color: var(--vscode-disabledForeground); } +.codeActionWidget .monaco-list-row.code-action.option-disabled .codicon { + opacity: 0.4; +} + .codeActionWidget .monaco-list-row.code-action:not(.option-disabled) .codicon { color: inherit; } diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts index 27138dce76f..f25921dc01f 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { ChordKeybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; import { organizeImportsCommandId, refactorCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKeybindingResolver } from 'vs/editor/contrib/codeAction/browser/codeActionMenu'; +import { CodeActionKeybindingResolver } from 'vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; suite('CodeActionKeybindingResolver', () => { const refactorKeybinding = createCodeActionKeybinding( diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index 87d5a93b21e..cc7d0b8c67a 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -19,7 +19,7 @@ import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeE import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions'; import * as corePosition from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { IEditorAction, ScrollType } from 'vs/editor/common/editorCommon'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import { isLocationLink, Location, LocationLink } from 'vs/editor/common/languages'; @@ -56,9 +56,6 @@ export interface SymbolNavigationActionConfig { muteMessage: boolean; } - - - export class SymbolNavigationAnchor { static is(thing: any): thing is SymbolNavigationAnchor { @@ -79,7 +76,7 @@ export class SymbolNavigationAnchor { export abstract class SymbolNavigationAction extends EditorAction2 { - private static _allSymbolNavigationCommands = new Set<string>(); + private static _allSymbolNavigationCommands = new Map<string, SymbolNavigationAction>(); private static _activeAlternativeCommands = new Set<string>(); readonly configuration: SymbolNavigationActionConfig; @@ -101,7 +98,7 @@ export abstract class SymbolNavigationAction extends EditorAction2 { constructor(configuration: SymbolNavigationActionConfig, opts: IAction2Options) { super(SymbolNavigationAction.aaa(opts)); this.configuration = configuration; - SymbolNavigationAction._allSymbolNavigationCommands.add(opts.id); + SymbolNavigationAction._allSymbolNavigationCommands.set(opts.id, this); } override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg?: SymbolNavigationAnchor | unknown, range?: Range): Promise<void> { @@ -113,6 +110,7 @@ export abstract class SymbolNavigationAction extends EditorAction2 { const progressService = accessor.get(IEditorProgressService); const symbolNavService = accessor.get(ISymbolNavigationService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const instaService = accessor.get(IInstantiationService); const model = editor.getModel(); const position = editor.getPosition(); @@ -128,11 +126,11 @@ export abstract class SymbolNavigationAction extends EditorAction2 { alert(references.ariaMessage); - let altAction: IEditorAction | null | undefined; + let altAction: SymbolNavigationAction | null | undefined; if (references.referenceAt(model.uri, position)) { const altActionId = this._getAlternativeCommand(editor); if (!SymbolNavigationAction._activeAlternativeCommands.has(altActionId) && SymbolNavigationAction._allSymbolNavigationCommands.has(altActionId)) { - altAction = editor.getAction(altActionId); + altAction = SymbolNavigationAction._allSymbolNavigationCommands.get(altActionId)!; } } @@ -147,9 +145,9 @@ export abstract class SymbolNavigationAction extends EditorAction2 { } else if (referenceCount === 1 && altAction) { // already at the only result, run alternative SymbolNavigationAction._activeAlternativeCommands.add(this.desc.id); - altAction.run().finally(() => { + instaService.invokeFunction((accessor) => altAction!.runEditorCommand(accessor, editor, arg, range).finally(() => { SymbolNavigationAction._activeAlternativeCommands.delete(this.desc.id); - }); + })); } else { // normal results handling diff --git a/src/vs/editor/test/browser/controller/imeTester.ts b/src/vs/editor/test/browser/controller/imeTester.ts index 11ce1dc78c9..e0e94e35664 100644 --- a/src/vs/editor/test/browser/controller/imeTester.ts +++ b/src/vs/editor/test/browser/controller/imeTester.ts @@ -34,6 +34,10 @@ class SingleLineTestModel implements ISimpleModel { return this._line.substring(range.startColumn - 1, range.endColumn - 1); } + getValueLengthInRange(range: Range): number { + return this.getValueInRange(range, EndOfLinePreference.TextDefined).length; + } + getModelLineContent(lineNumber: number): string { return this._line; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2259a4f522d..8736a56f0b9 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2634,6 +2634,10 @@ declare namespace monaco.editor { * New language */ readonly newLanguage: string; + /** + * Source of the call that caused the event. + */ + readonly source: string; } /** diff --git a/src/vs/platform/backup/electron-main/backup.ts b/src/vs/platform/backup/electron-main/backup.ts index a729ef372a4..81ab8756fee 100644 --- a/src/vs/platform/backup/electron-main/backup.ts +++ b/src/vs/platform/backup/electron-main/backup.ts @@ -17,7 +17,8 @@ export interface IBackupMainService { getEmptyWindowBackups(): IEmptyWindowBackupInfo[]; - registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string; + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo): string; + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom: string): Promise<string>; registerFolderBackup(folderInfo: IFolderBackupInfo): string; registerEmptyWindowBackup(emptyWindowInfo: IEmptyWindowBackupInfo): string; diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 4b38dd8688a..ed16c44eee1 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { createHash } from 'crypto'; -import * as fs from 'fs'; import { isEqual } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; import { join } from 'vs/base/common/path'; @@ -134,7 +133,9 @@ export class BackupMainService implements IBackupMainService { return this.emptyWindows.slice(0); // return a copy } - registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string { + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo): string; + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom: string): Promise<string>; + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string | Promise<string> { if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) { this.workspaces.push(workspaceInfo); this.storeWorkspacesMetadata(); @@ -143,23 +144,23 @@ export class BackupMainService implements IBackupMainService { const backupPath = join(this.backupHome, workspaceInfo.workspace.id); if (migrateFrom) { - this.moveBackupFolderSync(backupPath, migrateFrom); + return this.moveBackupFolder(backupPath, migrateFrom).then(() => backupPath); } return backupPath; } - private moveBackupFolderSync(backupPath: string, moveFromPath: string): void { + private async moveBackupFolder(backupPath: string, moveFromPath: string): Promise<void> { // Target exists: make sure to convert existing backups to empty window backups - if (fs.existsSync(backupPath)) { - this.convertToEmptyWindowBackupSync(backupPath); + if (await Promises.exists(backupPath)) { + await this.convertToEmptyWindowBackup(backupPath); } // When we have data to migrate from, move it over to the target location - if (fs.existsSync(moveFromPath)) { + if (await Promises.exists(moveFromPath)) { try { - fs.renameSync(moveFromPath, backupPath); + await Promises.rename(moveFromPath, backupPath); } catch (error) { this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`); } @@ -324,22 +325,6 @@ export class BackupMainService implements IBackupMainService { return true; } - private convertToEmptyWindowBackupSync(backupPath: string): boolean { - const newEmptyWindowBackupInfo = this.prepareNewEmptyWindowBackup(); - - // Rename backupPath to new empty window backup path - const newEmptyWindowBackupPath = join(this.backupHome, newEmptyWindowBackupInfo.backupFolder); - try { - fs.renameSync(backupPath, newEmptyWindowBackupPath); - } catch (error) { - this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); - return false; - } - this.emptyWindows.push(newEmptyWindowBackupInfo); - - return true; - } - async getDirtyWorkspaces(): Promise<Array<IWorkspaceBackupInfo | IFolderBackupInfo>> { const dirtyWorkspaces: Array<IWorkspaceBackupInfo | IFolderBackupInfo> = []; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 6a239744151..1f2e1729c22 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -328,13 +328,13 @@ flakySuite('BackupMainService', () => { assert.strictEqual(service.getEmptyWindowBackups().length, 1); }); - test('service supports to migrate backup data from another location', () => { + test('service supports to migrate backup data from another location', async () => { const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); service.registerFolderBackup(toFolderBackupInfo(URI.file(backupPathToMigrate))); - const workspaceBackupPath = service.registerWorkspaceBackup(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate); + const workspaceBackupPath = await service.registerWorkspaceBackup(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate); assert.ok(fs.existsSync(workspaceBackupPath)); assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt'))); @@ -344,7 +344,7 @@ flakySuite('BackupMainService', () => { assert.strictEqual(0, emptyBackups.length); }); - test('service backup migration makes sure to preserve existing backups', () => { + test('service backup migration makes sure to preserve existing backups', async () => { const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); @@ -355,7 +355,7 @@ flakySuite('BackupMainService', () => { fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data'); service.registerFolderBackup(toFolderBackupInfo(URI.file(backupPathToPreserve))); - const workspaceBackupPath = service.registerWorkspaceBackup(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate); + const workspaceBackupPath = await service.registerWorkspaceBackup(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate); assert.ok(fs.existsSync(workspaceBackupPath)); assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt'))); diff --git a/src/vs/platform/contextkey/common/contextkeys.ts b/src/vs/platform/contextkey/common/contextkeys.ts index de968735c0a..296245b4608 100644 --- a/src/vs/platform/contextkey/common/contextkeys.ts +++ b/src/vs/platform/contextkey/common/contextkeys.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isIOS, isLinux, isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; +import { isIOS, isLinux, isMacintosh, isMobile, isWeb, isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -14,6 +14,7 @@ export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindow export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser")); export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform")); export const IsIOSContext = new RawContextKey<boolean>('isIOS', isIOS, localize('isIOS', "Whether the operating system is iOS")); +export const IsMobileContext = new RawContextKey<boolean>('isMobile', isMobile, localize('isMobile', "Whether the platform is a mobile web browser")); export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true); export const ProductQualityContext = new RawContextKey<string>('productQualityType', '', localize('productQualityType', "Quality type of VS Code")); diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts index c9615fe27d8..bc140432faf 100644 --- a/src/vs/platform/credentials/common/credentialsMainService.ts +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -8,6 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { isWindows } from 'vs/base/common/platform'; +import { retry } from 'vs/base/common/async'; interface ChunkedPassword { content: string; @@ -46,6 +47,7 @@ export abstract class BaseCredentialsMainService extends Disposable implements I //#endregion async getPassword(service: string, account: string): Promise<string | null> { + this.logService.trace('Getting password from keytar:', service, account); let keytar: KeytarModule; try { keytar = await this.withKeytar(); @@ -54,33 +56,48 @@ export abstract class BaseCredentialsMainService extends Disposable implements I return null; } - const password = await keytar.getPassword(service, account); - if (password) { - try { - let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); - if (!content || !hasNextChunk) { - return password; - } + const password = await retry(() => keytar.getPassword(service, account), 50, 3); + if (!password) { + this.logService.trace('Did not get a password from keytar for account:', account); + return password; + } - let index = 1; - while (hasNextChunk) { - const nextChunk = await keytar.getPassword(service, `${account}-${index}`); - const result: ChunkedPassword = JSON.parse(nextChunk!); - content += result.content; - hasNextChunk = result.hasNextChunk; - index++; - } + let content: string | undefined; + let hasNextChunk: boolean | undefined; + try { + const parsed: ChunkedPassword = JSON.parse(password); + content = parsed.content; + hasNextChunk = parsed.hasNextChunk; + } catch { + // Ignore this similar to how we ignore parse errors in the delete + // because on non-windows this will not be a JSON string. + } - return content; - } catch { - return password; - } + if (!content || !hasNextChunk) { + this.logService.trace('Got password from keytar for account:', account); + return password; } - return password; + try { + let index = 1; + while (hasNextChunk) { + const nextChunk = await retry(() => keytar.getPassword(service, `${account}-${index}`), 50, 3); + const result: ChunkedPassword = JSON.parse(nextChunk!); + content += result.content; + hasNextChunk = result.hasNextChunk; + index++; + } + + this.logService.trace(`Got ${index}-chunked password from keytar for account:`, account); + return content; + } catch (e) { + this.logService.error(e); + return password; + } } async setPassword(service: string, account: string, password: string): Promise<void> { + this.logService.trace('Setting password using keytar:', service, account); let keytar: KeytarModule; try { keytar = await this.withKeytar(); @@ -89,28 +106,6 @@ export abstract class BaseCredentialsMainService extends Disposable implements I throw e; } - const MAX_SET_ATTEMPTS = 3; - - // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. - const setPasswordWithRetry = async (service: string, account: string, password: string) => { - let attempts = 0; - let error: any; - while (attempts < MAX_SET_ATTEMPTS) { - try { - await keytar.setPassword(service, account, password); - return; - } catch (e) { - error = e; - this.logService.warn('Error attempting to set a password: ', e?.message ?? e); - attempts++; - await new Promise(resolve => setTimeout(resolve, 200)); - } - } - - // throw last error - throw error; - }; - if (isWindows && password.length > BaseCredentialsMainService.MAX_PASSWORD_LENGTH) { let index = 0; let chunk = 0; @@ -124,19 +119,21 @@ export abstract class BaseCredentialsMainService extends Disposable implements I content: passwordChunk, hasNextChunk: hasNextChunk }; - - await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); + await retry(() => keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)), 50, 3); chunk++; } + this.logService.trace(`Got${chunk ? ` ${chunk}-chunked` : ''} password from keytar for account:`, account); } else { - await setPasswordWithRetry(service, account, password); + await retry(() => keytar.setPassword(service, account, password), 50, 3); + this.logService.trace('Got password from keytar for account:', account); } this._onDidChangePassword.fire({ service, account }); } async deletePassword(service: string, account: string): Promise<boolean> { + this.logService.trace('Deleting password using keytar:', service, account); let keytar: KeytarModule; try { keytar = await this.withKeytar(); @@ -147,14 +144,30 @@ export abstract class BaseCredentialsMainService extends Disposable implements I const password = await keytar.getPassword(service, account); if (!password) { + this.logService.trace('Did not get a password to delete from keytar for account:', account); return false; } - const didDelete = await keytar.deletePassword(service, account); + + let content: string | undefined; + let hasNextChunk: boolean | undefined; try { - let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); - if (content && hasNextChunk) { + const possibleChunk = JSON.parse(password); + content = possibleChunk.content; + hasNextChunk = possibleChunk.hasNextChunk; + } catch { + // When the password is saved the entire JSON payload is encrypted then stored, thus the result from getPassword might not be valid JSON + // https://github.com/microsoft/vscode/blob/c22cb87311b5eb1a3bf5600d18733f7485355dc0/src/vs/workbench/api/browser/mainThreadSecretState.ts#L83 + // However in the chunked case we JSONify each chunk after encryption so for the chunked case we do expect valid JSON here + // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L128 + // Empty catch here just as in getPassword because we expect to handle both JSON cases and non JSON cases here it's not an error case to fail to parse + // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L76 + } + + let index = 0; + if (content && hasNextChunk) { + try { // need to delete additional chunks - let index = 1; + index++; while (hasNextChunk) { const accountWithIndex = `${account}-${index}`; const nextChunk = await keytar.getPassword(service, accountWithIndex); @@ -164,21 +177,20 @@ export abstract class BaseCredentialsMainService extends Disposable implements I hasNextChunk = result.hasNextChunk; index++; } + } catch (e) { + this.logService.error(e); } - } catch { - // When the password is saved the entire JSON payload is encrypted then stored, thus the result from getPassword might not be valid JSON - // https://github.com/microsoft/vscode/blob/c22cb87311b5eb1a3bf5600d18733f7485355dc0/src/vs/workbench/api/browser/mainThreadSecretState.ts#L83 - // However in the chunked case we JSONify each chunk after encryption so for the chunked case we do expect valid JSON here - // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L128 - // Empty catch here just as in getPassword because we expect to handle both JSON cases and non JSON cases here it's not an error case to fail to parse - // https://github.com/microsoft/vscode/blob/708cb0c507d656b760f9d08115b8ebaf8964fd73/src/vs/platform/credentials/common/credentialsMainService.ts#L76 } - if (didDelete) { + // Delete the first account to determine deletion success + if (await keytar.deletePassword(service, account)) { this._onDidChangePassword.fire({ service, account }); + this.logService.trace(`Deleted${index ? ` ${index}-chunked` : ''} password from keytar for account:`, account); + return true; } - return didDelete; + this.logService.trace(`Keytar failed to delete${index ? ` ${index}-chunked` : ''} password for account:`, account); + return false; } async findPassword(service: string): Promise<string | null> { @@ -190,7 +202,7 @@ export abstract class BaseCredentialsMainService extends Disposable implements I return null; } - return keytar.findPassword(service); + return await keytar.findPassword(service); } async findCredentials(service: string): Promise<Array<{ account: string; password: string }>> { @@ -202,7 +214,7 @@ export abstract class BaseCredentialsMainService extends Disposable implements I return []; } - return keytar.findCredentials(service); + return await keytar.findCredentials(service); } public clear(): Promise<void> { diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index 0b14a28f7fe..63adb055852 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -36,14 +36,10 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens return { success: false }; } - // Ensure profile exists when passed in from args - const profilePromise = this.userDataProfilesMainService.checkAndCreateProfileFromCli(pargs); - const profile = profilePromise ? await profilePromise : undefined; - - const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { + const [codeWindow] = await this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { context: OpenContext.API, cli: pargs, - profile + profile: await this.userDataProfilesMainService.checkAndCreateProfileFromCli(pargs) }); if (!debugRenderer) { diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts index 4d618da59dc..3d10b4b8a15 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts @@ -42,33 +42,33 @@ export class DiagnosticsMainService implements IDiagnosticsMainService { async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const diagnostics: Array<IDiagnosticInfo | IRemoteDiagnosticError | undefined> = await Promise.all(windows.map(window => { - return new Promise<IDiagnosticInfo | IRemoteDiagnosticError | undefined>((resolve) => { - const remoteAuthority = window.remoteAuthority; - if (remoteAuthority) { - const replyChannel = `vscode:getDiagnosticInfoResponse${window.id}`; - const args: IDiagnosticInfoOptions = { - includeProcesses: options.includeProcesses, - folders: options.includeWorkspaceMetadata ? this.getFolderURIs(window) : undefined - }; - - window.sendWhenReady('vscode:getDiagnosticInfo', CancellationToken.None, { replyChannel, args }); - - validatedIpcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => { - // No data is returned if getting the connection fails. - if (!data) { - resolve({ hostName: remoteAuthority, errorMessage: `Unable to resolve connection to '${remoteAuthority}'.` }); - } - - resolve(data); - }); - - setTimeout(() => { - resolve({ hostName: remoteAuthority, errorMessage: `Connection to '${remoteAuthority}' could not be established` }); - }, 5000); - } else { - resolve(undefined); - } + const diagnostics: Array<IDiagnosticInfo | IRemoteDiagnosticError | undefined> = await Promise.all(windows.map(async window => { + const remoteAuthority = window.remoteAuthority; + if (!remoteAuthority) { + return undefined; + } + + const replyChannel = `vscode:getDiagnosticInfoResponse${window.id}`; + const args: IDiagnosticInfoOptions = { + includeProcesses: options.includeProcesses, + folders: options.includeWorkspaceMetadata ? await this.getFolderURIs(window) : undefined + }; + + return new Promise<IDiagnosticInfo | IRemoteDiagnosticError>(resolve => { + window.sendWhenReady('vscode:getDiagnosticInfo', CancellationToken.None, { replyChannel, args }); + + validatedIpcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => { + // No data is returned if getting the connection fails. + if (!data) { + resolve({ hostName: remoteAuthority, errorMessage: `Unable to resolve connection to '${remoteAuthority}'.` }); + } + + resolve(data); + }); + + setTimeout(() => { + resolve({ hostName: remoteAuthority, errorMessage: `Connection to '${remoteAuthority}' could not be established` }); + }, 5000); }); })); @@ -82,7 +82,7 @@ export class DiagnosticsMainService implements IDiagnosticsMainService { for (const window of BrowserWindow.getAllWindows()) { const codeWindow = this.windowsMainService.getWindowById(window.id); if (codeWindow) { - windows.push(this.codeWindowToInfo(codeWindow)); + windows.push(await this.codeWindowToInfo(codeWindow)); } else { windows.push(this.browserWindowToInfo(window)); } @@ -97,8 +97,8 @@ export class DiagnosticsMainService implements IDiagnosticsMainService { }; } - private codeWindowToInfo(window: ICodeWindow): IWindowDiagnostics { - const folderURIs = this.getFolderURIs(window); + private async codeWindowToInfo(window: ICodeWindow): Promise<IWindowDiagnostics> { + const folderURIs = await this.getFolderURIs(window); const win = assertIsDefined(window.win); return this.browserWindowToInfo(win, folderURIs, window.remoteAuthority); @@ -113,14 +113,14 @@ export class DiagnosticsMainService implements IDiagnosticsMainService { }; } - private getFolderURIs(window: ICodeWindow): URI[] { + private async getFolderURIs(window: ICodeWindow): Promise<URI[]> { const folderURIs: URI[] = []; const workspace = window.openedWorkspace; if (isSingleFolderWorkspaceIdentifier(workspace)) { folderURIs.push(workspace.uri); } else if (isWorkspaceIdentifier(workspace)) { - const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces + const resolvedWorkspace = await this.workspacesManagementMainService.resolveLocalWorkspace(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces if (resolvedWorkspace) { const rootFolders = resolvedWorkspace.folders; rootFolders.forEach(root => { diff --git a/src/vs/platform/environment/node/wait.ts b/src/vs/platform/environment/node/wait.ts index 0be07ad2266..793d8e0467e 100644 --- a/src/vs/platform/environment/node/wait.ts +++ b/src/vs/platform/environment/node/wait.ts @@ -7,7 +7,7 @@ import { writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { randomPath } from 'vs/base/common/extpath'; -export function createWaitMarkerFile(verbose?: boolean): string | undefined { +export function createWaitMarkerFileSync(verbose?: boolean): string | undefined { const randomWaitMarkerPath = randomPath(tmpdir()); try { diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 1f6bba77e24..21908bbcff0 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -11,7 +11,9 @@ import { IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } fro import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; // TRACING -const _enableTracing = false; +const _enableAllTracing = false + // || "TRUE" // DO NOT CHECK IN! + ; class CyclicDependencyError extends Error { constructor(graph: Graph<any>) { @@ -24,24 +26,26 @@ export class InstantiationService implements IInstantiationService { declare readonly _serviceBrand: undefined; - private readonly _services: ServiceCollection; - private readonly _strict: boolean; - private readonly _parent?: InstantiationService; + readonly _globalGraph?: Graph<string>; + private _globalGraphImplicitDependency?: string; - constructor(services: ServiceCollection = new ServiceCollection(), strict: boolean = false, parent?: InstantiationService) { - this._services = services; - this._strict = strict; - this._parent = parent; + constructor( + private readonly _services: ServiceCollection = new ServiceCollection(), + private readonly _strict: boolean = false, + private readonly _parent?: InstantiationService, + private readonly _enableTracing: boolean = _enableAllTracing + ) { this._services.set(IInstantiationService, this); + this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined; } createChild(services: ServiceCollection): IInstantiationService { - return new InstantiationService(services, this._strict, this); + return new InstantiationService(services, this._strict, this, this._enableTracing); } invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { - const _trace = Trace.traceInvocation(fn); + const _trace = Trace.traceInvocation(this._enableTracing, fn); let _done = false; try { const accessor: ServicesAccessor = { @@ -69,10 +73,10 @@ export class InstantiationService implements IInstantiationService { let _trace: Trace; let result: any; if (ctorOrDescriptor instanceof SyncDescriptor) { - _trace = Trace.traceCreation(ctorOrDescriptor.ctor); + _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor); result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace); } else { - _trace = Trace.traceCreation(ctorOrDescriptor); + _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor); result = this._createInstance(ctorOrDescriptor, rest, _trace); } _trace.stop(); @@ -130,6 +134,9 @@ export class InstantiationService implements IInstantiationService { } protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T { + if (this._globalGraph && this._globalGraphImplicitDependency) { + this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id)); + } const thing = this._getServiceInstanceOrDescriptor(id); if (thing instanceof SyncDescriptor) { return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)); @@ -178,6 +185,9 @@ export class InstantiationService implements IInstantiationService { this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true); } + // take note of all service dependencies + this._globalGraph?.insertEdge(String(item.id), String(dependency.id)); + if (instanceOrDesc instanceof SyncDescriptor) { const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) }; graph.insertEdge(item, d); @@ -216,7 +226,7 @@ export class InstantiationService implements IInstantiationService { private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T { if (this._services.get(id) instanceof SyncDescriptor) { - return this._createServiceInstance(ctor, args, supportsDelayedInstantiation, _trace); + return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace); } else if (this._parent) { return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace); } else { @@ -224,16 +234,22 @@ export class InstantiationService implements IInstantiationService { } } - private _createServiceInstance<T>(ctor: any, args: any[] = [], _supportsDelayedInstantiation: boolean, _trace: Trace): T { - if (!_supportsDelayedInstantiation) { + private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T { + if (!supportsDelayedInstantiation) { // eager instantiation return this._createInstance(ctor, args, _trace); } else { + const child = new InstantiationService(undefined, this._strict, this, this._enableTracing); + child._globalGraphImplicitDependency = String(id); + // Return a proxy object that's backed by an idle value. That // strategy is to instantiate services in our idle time or when actually // needed but not when injected into a consumer - const idle = new IdleValue<any>(() => this._createInstance<T>(ctor, args, _trace)); + const idle = new IdleValue<any>(() => { + const result = child._createInstance<T>(ctor, args, _trace); + return result; + }); return <T>new Proxy(Object.create(null), { get(target: any, key: PropertyKey): any { if (key in target) { @@ -274,17 +290,19 @@ const enum TraceType { export class Trace { + static all = new Set<string>(); + private static readonly _None = new class extends Trace { constructor() { super(-1, null); } override stop() { } override branch() { return this; } }; - static traceInvocation(ctor: any): Trace { - return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || (ctor.toString() as string).substring(0, 42).replace(/\n/g, '')); + static traceInvocation(_enableTracing: boolean, ctor: any): Trace { + return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n')); } - static traceCreation(ctor: any): Trace { + static traceCreation(_enableTracing: boolean, ctor: any): Trace { return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name); } @@ -334,7 +352,7 @@ export class Trace { ]; if (dur > 2 || causedCreation) { - console.log(lines.join('\n')); + Trace.all.add(lines.join('\n')); } } } diff --git a/src/vs/platform/instantiation/test/common/instantiationService.test.ts b/src/vs/platform/instantiation/test/common/instantiationService.test.ts index 90bd049d0ca..a737b46533c 100644 --- a/src/vs/platform/instantiation/test/common/instantiationService.test.ts +++ b/src/vs/platform/instantiation/test/common/instantiationService.test.ts @@ -393,4 +393,70 @@ suite('Instantiation Service', () => { assert.ok(obj); }); + test('Sync/Async dependency loop', async function () { + + const A = createDecorator<A>('A'); + const B = createDecorator<B>('B'); + interface A { _serviceBrand: undefined; doIt(): void } + interface B { _serviceBrand: undefined; b(): boolean } + + class BConsumer { + constructor(@B readonly b: B) { + + } + doIt() { + return this.b.b(); + } + } + + class AService implements A { + _serviceBrand: undefined; + prop: BConsumer; + constructor(@IInstantiationService insta: IInstantiationService) { + this.prop = insta.createInstance(BConsumer); + } + doIt() { + return this.prop.doIt(); + } + } + + class BService implements B { + _serviceBrand: undefined; + constructor(@A a: A) { + assert.ok(a); + } + b() { return true; } + } + + // SYNC -> explodes AImpl -> [insta:BConsumer] -> BImpl -> AImpl + { + const insta1 = new InstantiationService(new ServiceCollection( + [A, new SyncDescriptor(AService)], + [B, new SyncDescriptor(BService)], + ), true, undefined, true); + + try { + insta1.invokeFunction(accessor => accessor.get(A)); + assert.ok(false); + + } catch (error) { + assert.ok(error instanceof Error); + assert.ok(error.message.includes('RECURSIVELY')); + } + } + + // ASYNC -> doesn't explode but cycle is tracked + { + const insta2 = new InstantiationService(new ServiceCollection( + [A, new SyncDescriptor(AService, undefined, true)], + [B, new SyncDescriptor(BService, undefined)], + ), true, undefined, true); + + const a = insta2.invokeFunction(accessor => accessor.get(A)); + a.doIt(); + + const cycle = insta2._globalGraph?.findCycleSlow(); + assert.strictEqual(cycle, 'A -> B -> A'); + } + }); }); diff --git a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts index 993fbfe9643..504f2600de8 100644 --- a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts +++ b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts @@ -28,7 +28,7 @@ export class TestInstantiationService extends InstantiationService { } public get<T>(service: ServiceIdentifier<T>): T { - return super._getOrCreateServiceInstance(service, Trace.traceCreation(TestInstantiationService)); + return super._getOrCreateServiceInstance(service, Trace.traceCreation(false, TestInstantiationService)); } public set<T>(service: ServiceIdentifier<T>, instance: T): T { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index f5730717f2c..54dc0b65e1b 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { app } from 'electron'; -import { coalesce } from 'vs/base/common/arrays'; +import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { whenDeleted } from 'vs/base/node/pfs'; @@ -71,8 +71,10 @@ export class LaunchMainService implements ILaunchMainService { // Create a window if there is none if (this.windowsMainService.getWindowCount() === 0) { - const window = this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP })[0]; - whenWindowReady = window.ready(); + const window = firstOrDefault(await this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP })); + if (window) { + whenWindowReady = window.ready(); + } } // Make sure a window is open, ready to receive the url event @@ -116,8 +118,7 @@ export class LaunchMainService implements ILaunchMainService { const remoteAuthority = args.remote || undefined; // Ensure profile exists when passed in from CLI - const profilePromise = this.userDataProfilesMainService.checkAndCreateProfileFromCli(args); - const profile = profilePromise ? await profilePromise : undefined; + const profile = await this.userDataProfilesMainService.checkAndCreateProfileFromCli(args); const baseConfig: IOpenConfiguration = { context, @@ -130,7 +131,7 @@ export class LaunchMainService implements ILaunchMainService { // Special case extension development if (!!args.extensionDevelopmentPath) { - this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig); + await this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig); } // Start without file/folder arguments @@ -165,7 +166,7 @@ export class LaunchMainService implements ILaunchMainService { // Open new Window if (openNewWindow) { - usedWindows = this.windowsMainService.open({ + usedWindows = await this.windowsMainService.open({ ...baseConfig, forceNewWindow: true, forceEmpty: true @@ -180,7 +181,7 @@ export class LaunchMainService implements ILaunchMainService { usedWindows = [lastActive]; } else { - usedWindows = this.windowsMainService.open({ + usedWindows = await this.windowsMainService.open({ ...baseConfig, forceEmpty: true }); @@ -190,7 +191,7 @@ export class LaunchMainService implements ILaunchMainService { // Start with file/folder arguments else { - usedWindows = this.windowsMainService.open({ + usedWindows = await this.windowsMainService.open({ ...baseConfig, forceNewWindow: args['new-window'], preferNewWindow: !args['reuse-window'] && !args.wait, diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 50b6febdf85..b61c05ea574 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -532,19 +532,19 @@ export class Menubar { return new MenuItem(this.likeAction(commandId, { label: item.label, - click: (menuItem, win, event) => { + click: async (menuItem, win, event) => { const openInNewWindow = this.isOptionClick(event); - const success = this.windowsMainService.open({ + const success = (await this.windowsMainService.open({ context: OpenContext.MENU, cli: this.environmentMainService.args, urisToOpen: [openable], forceNewWindow: openInNewWindow, gotoLineMode: false, remoteAuthority: item.remoteAuthority - }).length > 0; + })).length > 0; if (!success) { - this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]); + await this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]); } } }, false)); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 84d42eb4877..d5da363f058 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -146,7 +146,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: IOpenWindowOptions = Object.create(null)): Promise<void> { if (toOpen.length > 0) { - this.windowsMainService.open({ + await this.windowsMainService.open({ context: OpenContext.API, contextWindowId: windowId, urisToOpen: toOpen, @@ -166,7 +166,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } private async doOpenEmptyWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise<void> { - this.windowsMainService.openEmptyWindow({ + await this.windowsMainService.openEmptyWindow({ context: OpenContext.API, contextWindowId: windowId }, options); @@ -384,33 +384,33 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async pickFileFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> { const paths = await this.dialogMainService.pickFileFolder(options); if (paths) { - this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); + await this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); } } async pickFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> { const paths = await this.dialogMainService.pickFolder(options); if (paths) { - this.doOpenPicked(paths.map(path => ({ folderUri: URI.file(path) })), options, windowId); + await this.doOpenPicked(paths.map(path => ({ folderUri: URI.file(path) })), options, windowId); } } async pickFileAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> { const paths = await this.dialogMainService.pickFile(options); if (paths) { - this.doOpenPicked(paths.map(path => ({ fileUri: URI.file(path) })), options, windowId); + await this.doOpenPicked(paths.map(path => ({ fileUri: URI.file(path) })), options, windowId); } } async pickWorkspaceAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> { const paths = await this.dialogMainService.pickWorkspace(options); if (paths) { - this.doOpenPicked(paths.map(path => ({ workspaceUri: URI.file(path) })), options, windowId); + await this.doOpenPicked(paths.map(path => ({ workspaceUri: URI.file(path) })), options, windowId); } } - private doOpenPicked(openable: IWindowOpenable[], options: INativeOpenDialogOptions, windowId: number | undefined): void { - this.windowsMainService.open({ + private async doOpenPicked(openable: IWindowOpenable[], options: INativeOpenDialogOptions, windowId: number | undefined): Promise<void> { + await this.windowsMainService.open({ context: OpenContext.DIALOG, contextWindowId: windowId, cli: this.environmentMainService.args, @@ -623,7 +623,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region macOS Touchbar async newWindowTab(): Promise<void> { - this.windowsMainService.open({ + await this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 7e63a164e2c..f0808b12783 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.67.0-dev', + version: '1.72.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 82327bc1660..9f78d7db571 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -317,7 +317,7 @@ export interface IPtyService extends IPtyHostController { /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise<void>; updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void>; - updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<void>; + updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void>; installAutoReply(match: string, reply: string): Promise<void>; uninstallAllAutoReplies(): Promise<void>; uninstallAutoReply(match: string): Promise<void>; diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 21e2cf78d96..aab32f8cfcd 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -224,8 +224,8 @@ export class PtyHostService extends Disposable implements IPtyService { updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void> { return this._proxy.updateTitle(id, title, titleSource); } - updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<void> { - return this._proxy.updateIcon(id, icon, color); + updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void> { + return this._proxy.updateIcon(id, userInitiated, icon, color); } attachToProcess(id: number): Promise<void> { return this._proxy.attachToProcess(id); diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 69d05de8395..0d8f58a6dbd 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -225,8 +225,8 @@ export class PtyService extends Disposable implements IPtyService { this._throwIfNoPty(id).setTitle(title, titleSource); } - async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string; color?: { id: string } }, color?: string): Promise<void> { - this._throwIfNoPty(id).setIcon(icon, color); + async updateIcon(id: number, userInitiated: boolean, icon: URI | { light: URI; dark: URI } | { id: string; color?: { id: string } }, color?: string): Promise<void> { + this._throwIfNoPty(id).setIcon(userInitiated, icon, color); } async refreshProperty<T extends ProcessPropertyType>(id: number, type: T): Promise<IProcessPropertyMap[T]> { @@ -496,12 +496,14 @@ export class PersistentTerminalProcess extends Disposable { this._titleSource = titleSource; } - setIcon(icon: TerminalIcon, color?: string): void { + setIcon(userInitiated: boolean, icon: TerminalIcon, color?: string): void { if (!this._icon || 'id' in icon && 'id' in this._icon && icon.id !== this._icon.id || !this.color || color !== this._color) { this._serializer.freeRawReviveBuffer(); - this._interactionState.setValue(InteractionState.Session, 'setIcon'); + if (userInitiated) { + this._interactionState.setValue(InteractionState.Session, 'setIcon'); + } } this._icon = icon; this._color = color; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 99144a4ace0..cdde6fb2af3 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -6,7 +6,7 @@ import { hash } from 'vs/base/common/hash'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { joinPath } from 'vs/base/common/resources'; +import { basename, joinPath } from 'vs/base/common/resources'; import { isUndefined } from 'vs/base/common/types'; import { URI, UriDto } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -95,9 +95,10 @@ export interface IUserDataProfilesService { readonly onDidResetWorkspaces: Event<void>; - createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>; + createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>; createTransientProfile(workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>; - updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile>; + createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>; + updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise<IUserDataProfile>; removeProfile(profile: IUserDataProfile): Promise<void>; setProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profile: IUserDataProfile): Promise<void>; @@ -126,10 +127,10 @@ export function reviveProfile(profile: UriDto<IUserDataProfile>, scheme: string) export const EXTENSIONS_RESOURCE_NAME = 'extensions.json'; -export function toUserDataProfile(name: string, location: URI, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): IUserDataProfile { +export function toUserDataProfile(id: string, name: string, location: URI, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): IUserDataProfile { return { - id: hash(location.path).toString(16), - name: name, + id, + name, location: location, isDefault: false, globalStorageHome: joinPath(location, 'globalStorage'), @@ -213,10 +214,10 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf protected _profilesObject: UserDataProfilesObject | undefined; protected get profilesObject(): UserDataProfilesObject { if (!this._profilesObject) { - const profiles = this.enabled ? this.getStoredProfiles().map<IUserDataProfile>(storedProfile => toUserDataProfile(storedProfile.name, storedProfile.location, storedProfile.useDefaultFlags)) : []; + const profiles = this.enabled ? this.getStoredProfiles().map<IUserDataProfile>(storedProfile => toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, storedProfile.useDefaultFlags)) : []; let emptyWindow: IUserDataProfile | undefined; const workspaces = new ResourceMap<IUserDataProfile>(); - const defaultProfile = toUserDataProfile(localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); + const defaultProfile = toUserDataProfile(hash(this.environmentService.userRoamingDataHome.path).toString(16), localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); profiles.unshift({ ...defaultProfile, isDefault: true, extensionsResource: this.defaultProfileShouldIncludeExtensionsResourceAlways || profiles.length > 0 || this.transientProfilesObject.profiles.length > 0 ? defaultProfile.extensionsResource : undefined }); if (profiles.length) { const profileAssicaitions = this.getStoredProfileAssociations(); @@ -250,15 +251,19 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf nameIndex = index > nameIndex ? index : nameIndex; } const name = `${namePrefix} ${nameIndex + 1}`; - return this.createProfile(name, undefined, workspaceIdentifier, true); + return this.createProfile(hash(generateUuid()).toString(16), name, undefined, true, workspaceIdentifier); } - async createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier, transient?: boolean): Promise<IUserDataProfile> { + async createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { + return this.createProfile(hash(generateUuid()).toString(16), name, useDefaultFlags, false, workspaceIdentifier); + } + + async createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { if (!this.enabled) { throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } - const profile = await this.doCreateProfile(name, useDefaultFlags, transient); + const profile = await this.doCreateProfile(id, name, useDefaultFlags, !!transient); if (workspaceIdentifier) { await this.setProfileForWorkspace(workspaceIdentifier, profile); @@ -267,17 +272,17 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return profile; } - private async doCreateProfile(name: string, useDefaultFlags: UseDefaultProfileFlags | undefined, transient?: boolean): Promise<IUserDataProfile> { + private async doCreateProfile(id: string, name: string, useDefaultFlags: UseDefaultProfileFlags | undefined, transient: boolean): Promise<IUserDataProfile> { let profileCreationPromise = this.profileCreationPromises.get(name); if (!profileCreationPromise) { profileCreationPromise = (async () => { try { - const existing = this.profiles.find(p => p.name === name); + const existing = this.profiles.find(p => p.name === name || p.id === id); if (existing) { return existing; } - const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(generateUuid()).toString(16)), useDefaultFlags, transient); + const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), useDefaultFlags, transient); await this.fileService.createFolder(profile.location); const joiners: Promise<void>[] = []; @@ -300,7 +305,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return profileCreationPromise; } - async updateProfile(profileToUpdate: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> { + async updateProfile(profileToUpdate: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise<IUserDataProfile> { if (!this.enabled) { throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } @@ -310,7 +315,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } - profile = toUserDataProfile(name, profile.location, useDefaultFlags); + profile = toUserDataProfile(profile.id, name, profile.location, useDefaultFlags, transient ?? profile.isTransient); this.updateProfiles([], [], [profile]); return profile; diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index 4578e6d40a9..a5776c1fa3d 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -58,7 +58,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } if (args.profile) { const profile = this.profiles.find(p => p.name === args.profile); - return profile ? Promise.resolve(profile) : this.createProfile(args.profile); + return profile ? Promise.resolve(profile) : this.createNamedProfile(args.profile); } if (args['profile-temp']) { return this.createTransientProfile(); diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts index b9f50c294a6..dda0d6fa9b5 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts @@ -48,8 +48,13 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa this.onDidResetWorkspaces = this.channel.listen<void>('onDidResetWorkspaces'); } - async createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { - const result = await this.channel.call<UriDto<IUserDataProfile>>('createProfile', [name, useDefaultFlags, workspaceIdentifier]); + async createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { + const result = await this.channel.call<UriDto<IUserDataProfile>>('createNamedProfile', [name, useDefaultFlags, workspaceIdentifier]); + return reviveProfile(result, this.profilesHome.scheme); + } + + async createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { + const result = await this.channel.call<UriDto<IUserDataProfile>>('createProfile', [id, name, useDefaultFlags, transient, workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } @@ -66,8 +71,8 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa return this.channel.call('removeProfile', [profile]); } - async updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> { - const result = await this.channel.call<UriDto<IUserDataProfile>>('updateProfile', [profile, name, useDefaultFlags]); + async updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise<IUserDataProfile> { + const result = await this.channel.call<UriDto<IUserDataProfile>>('updateProfile', [profile, name, useDefaultFlags, transient]); return reviveProfile(result, this.profilesHome.scheme); } diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 22c5539cb09..a4cc6a8f50c 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -74,12 +74,32 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(testObject.profiles[0].extensionsResource, undefined); }); + test('create profile with id', async () => { + const profile = await testObject.createProfile('id', 'name'); + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(profile.id, 'id'); + assert.deepStrictEqual(profile.name, 'name'); + assert.deepStrictEqual(!!profile.isTransient, false); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + assert.deepStrictEqual(testObject.profiles[1].name, profile.name); + }); + + test('create profile with id, name and transient', async () => { + const profile = await testObject.createProfile('id', 'name', undefined, true); + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(profile.id, 'id'); + assert.deepStrictEqual(profile.name, 'name'); + assert.deepStrictEqual(!!profile.isTransient, true); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + test('create transient profiles', async () => { const profile1 = await testObject.createTransientProfile(); const profile2 = await testObject.createTransientProfile(); const profile3 = await testObject.createTransientProfile(); + const profile4 = await testObject.createProfile('id', 'name', undefined, true); - assert.deepStrictEqual(testObject.profiles.length, 4); + assert.deepStrictEqual(testObject.profiles.length, 5); assert.deepStrictEqual(profile1.name, 'Temp 1'); assert.deepStrictEqual(profile1.isTransient, true); assert.deepStrictEqual(testObject.profiles[1].id, profile1.id); @@ -89,10 +109,13 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(profile3.name, 'Temp 3'); assert.deepStrictEqual(profile3.isTransient, true); assert.deepStrictEqual(testObject.profiles[3].id, profile3.id); + assert.deepStrictEqual(profile4.name, 'name'); + assert.deepStrictEqual(profile4.isTransient, true); + assert.deepStrictEqual(testObject.profiles[4].id, profile4.id); }); test('create transient profile when a normal profile with Temp is already created', async () => { - await testObject.createProfile('Temp 1'); + await testObject.createNamedProfile('Temp 1'); const profile1 = await testObject.createTransientProfile(); assert.deepStrictEqual(profile1.name, 'Temp 2'); @@ -116,4 +139,44 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(testObject.profiles[0].extensionsResource, undefined); }); + test('update named profile', async () => { + const profile = await testObject.createNamedProfile('name'); + await testObject.updateProfile(profile, 'name changed'); + + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(testObject.profiles[1].name, 'name changed'); + assert.deepStrictEqual(!!testObject.profiles[1].isTransient, false); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + + test('persist transient profile', async () => { + const profile = await testObject.createTransientProfile(); + await testObject.updateProfile(profile, 'saved', undefined, false); + + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); + assert.deepStrictEqual(!!testObject.profiles[1].isTransient, false); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + + test('persist transient profile (2)', async () => { + const profile = await testObject.createProfile('id', 'name', undefined, true); + await testObject.updateProfile(profile, 'saved', undefined, false); + + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); + assert.deepStrictEqual(!!testObject.profiles[1].isTransient, false); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + + test('save transient profile', async () => { + const profile = await testObject.createTransientProfile(); + await testObject.updateProfile(profile, 'saved'); + + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); + assert.deepStrictEqual(!!testObject.profiles[1].isTransient, true); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + }); diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 23e7656a1ad..7078f0757a4 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -61,13 +61,13 @@ suite('UserDataProfileMainService', () => { }); test('default profile when there are profiles', async () => { - await testObject.createProfile('test'); + await testObject.createNamedProfile('test'); assert.strictEqual(testObject.defaultProfile.isDefault, true); assert.strictEqual(testObject.defaultProfile.extensionsResource?.toString(), joinPath(environmentService.userRoamingDataHome, 'extensions.json').toString()); }); test('default profile when profiles are removed', async () => { - const profile = await testObject.createProfile('test'); + const profile = await testObject.createNamedProfile('test'); await testObject.removeProfile(profile); assert.strictEqual(testObject.defaultProfile.isDefault, true); assert.strictEqual(testObject.defaultProfile.extensionsResource, undefined); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 3ad057b3cae..054565bd9d1 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -484,7 +484,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> { - const handles = await this.userDataSyncStoreService.getAllRefs(this.resource); + const handles = await this.userDataSyncStoreService.getAllResourceRefs(this.resource); return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); } @@ -665,11 +665,11 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> { if (isString(refOrLastSyncData)) { - const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData); + const content = await this.userDataSyncStoreService.resolveResourceContent(this.resource, refOrLastSyncData); return { ref: refOrLastSyncData, content }; } else { const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null; - return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, undefined, this.syncHeaders); + return this.userDataSyncStoreService.readResource(this.resource, lastSyncUserData, undefined, this.syncHeaders); } } @@ -677,7 +677,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa const machineId = await this.currentMachineIdPromise; const syncData: ISyncData = { version: this.version, machineId, content }; try { - ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, undefined, this.syncHeaders); + ref = await this.userDataSyncStoreService.writeResource(this.resource, JSON.stringify(syncData), ref, undefined, this.syncHeaders); return { ref, syncData }; } catch (error) { if (error instanceof UserDataSyncError && error.code === UserDataSyncErrorCode.TooLarge) { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 1f7f2860565..a3d8c085cec 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -493,7 +493,7 @@ export class UserDataSyncStoreTypeSynchronizer { private async doSync(userDataSyncStoreType: UserDataSyncStoreType, syncHeaders: IHeaders): Promise<void> { // Read the global state from remote - const globalStateUserData = await this.userDataSyncStoreClient.readResource(SyncResource.GlobalState, null, syncHeaders); + const globalStateUserData = await this.userDataSyncStoreClient.readResource(SyncResource.GlobalState, null, undefined, syncHeaders); const remoteGlobalState = this.parseGlobalState(globalStateUserData) || { storage: {} }; // Update the sync store type @@ -502,7 +502,7 @@ export class UserDataSyncStoreTypeSynchronizer { // Write the global state to remote const machineId = await getServiceMachineId(this.environmentService, this.fileService, this.storageService); const syncDataToUpdate: ISyncData = { version: GLOBAL_STATE_DATA_VERSION, machineId, content: stringify(remoteGlobalState, false) }; - await this.userDataSyncStoreClient.writeResource(SyncResource.GlobalState, JSON.stringify(syncDataToUpdate), globalStateUserData.ref, syncHeaders); + await this.userDataSyncStoreClient.writeResource(SyncResource.GlobalState, JSON.stringify(syncDataToUpdate), globalStateUserData.ref, undefined, syncHeaders); } private parseGlobalState({ content }: IUserData): IGlobalState | null { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 17ed95577de..506557b371c 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -145,10 +145,19 @@ export function getLastSyncResourceUri(syncResource: SyncResource, environmentSe return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); } +export type IUserDataResourceManifest = Record<ServerResource, string>; + +export interface IUserDataCollectionManifest { + [collectionId: string]: { + readonly latest: IUserDataResourceManifest; + }; +} + export interface IUserDataManifest { - readonly latest?: Record<ServerResource, string>; + readonly latest?: IUserDataResourceManifest; readonly session: string; readonly ref: string; + readonly collections?: IUserDataCollectionManifest; } export interface IResourceRefHandle { @@ -156,7 +165,7 @@ export interface IResourceRefHandle { created: number; } -export type ServerResource = SyncResource | 'machines' | 'editSessions' | 'profiles'; +export type ServerResource = SyncResource | 'machines' | 'editSessions'; export type UserDataSyncStoreType = 'insiders' | 'stable'; export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService'); @@ -179,11 +188,16 @@ export interface IUserDataSyncStoreService { setAuthToken(token: string, type: string): void; manifest(oldValue: IUserDataManifest | null, headers?: IHeaders): Promise<IUserDataManifest | null>; - read(resource: ServerResource, oldValue: IUserData | null, profile?: string, headers?: IHeaders): Promise<IUserData>; - write(resource: ServerResource, content: string, ref: string | null, profile?: string, headers?: IHeaders): Promise<string>; - delete(resource: ServerResource, ref: string | null, profile?: string): Promise<void>; - getAllRefs(resource: ServerResource, profile?: string): Promise<IResourceRefHandle[]>; - resolveContent(resource: ServerResource, ref: string, profile?: string, headers?: IHeaders): Promise<string | null>; + readResource(resource: ServerResource, oldValue: IUserData | null, collection?: string, headers?: IHeaders): Promise<IUserData>; + writeResource(resource: ServerResource, content: string, ref: string | null, collection?: string, headers?: IHeaders): Promise<string>; + deleteResource(resource: ServerResource, ref: string | null, collection?: string): Promise<void>; + getAllResourceRefs(resource: ServerResource, collection?: string): Promise<IResourceRefHandle[]>; + resolveResourceContent(resource: ServerResource, ref: string, collection?: string, headers?: IHeaders): Promise<string | null>; + + getAllCollections(headers?: IHeaders): Promise<string[]>; + createCollection(headers?: IHeaders): Promise<string>; + deleteCollection(collection?: string, headers?: IHeaders): Promise<void>; + clear(): Promise<void>; } @@ -231,6 +245,7 @@ export const enum UserDataSyncErrorCode { RequestProtocolNotSupported = 'RequestProtocolNotSupported', RequestPathNotEscaped = 'RequestPathNotEscaped', RequestHeadersNotObject = 'RequestHeadersNotObject', + NoCollection = 'NoCollection', NoRef = 'NoRef', EmptyResponse = 'EmptyResponse', TurnedOff = 'TurnedOff', diff --git a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts index 1158727d42c..6460ed5792d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -176,7 +176,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData private async writeMachinesData(machinesData: IMachinesData): Promise<void> { const content = JSON.stringify(machinesData); - const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null); + const ref = await this.userDataSyncStoreService.writeResource(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null); this.userData = { ref, content }; this._onDidChange.fire(); } @@ -197,7 +197,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData } } - return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData); + return this.userDataSyncStoreService.readResource(UserDataSyncMachinesService.RESOURCE, this.userData); } private parse(userData: IUserData): IMachinesData { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 4104c13c6ad..61e2b0df33d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -12,7 +12,6 @@ import { Mimes } from 'vs/base/common/mime'; import { isWeb } from 'vs/base/common/platform'; import { ConfigurationSyncStore } from 'vs/base/common/product'; import { joinPath, relativePath } from 'vs/base/common/resources'; -import { join } from 'vs/base/common/path'; import { isObject, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -231,12 +230,60 @@ export class UserDataSyncStoreClient extends Disposable { } } - async getAllResourceRefs(path: string): Promise<IResourceRefHandle[]> { + // #region Collection + + async getAllCollections(headers: IHeaders = {}): Promise<string[]> { + if (!this.userDataSyncStoreUrl) { + throw new Error('No settings sync store url configured.'); + } + + const url = joinPath(this.userDataSyncStoreUrl, 'collection').toString(); + headers = { ...headers }; + headers['Content-Type'] = 'application/json'; + + const context = await this.request(url, { type: 'GET', headers }, [], CancellationToken.None); + + return (await asJson<string[]>(context)) || []; + } + + async createCollection(headers: IHeaders = {}): Promise<string> { + if (!this.userDataSyncStoreUrl) { + throw new Error('No settings sync store url configured.'); + } + + const url = joinPath(this.userDataSyncStoreUrl, 'collection').toString(); + headers = { ...headers }; + headers['Content-Type'] = Mimes.text; + + const context = await this.request(url, { type: 'POST', headers }, [], CancellationToken.None); + const collectionId = await asTextOrError(context); + if (!collectionId) { + throw new UserDataSyncStoreError('Server did not return the collection id', url, UserDataSyncErrorCode.NoCollection, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]); + } + return collectionId; + } + + async deleteCollection(collection?: string, headers: IHeaders = {}): Promise<void> { + if (!this.userDataSyncStoreUrl) { + throw new Error('No settings sync store url configured.'); + } + + const url = collection ? joinPath(this.userDataSyncStoreUrl, 'collection', collection).toString() : joinPath(this.userDataSyncStoreUrl, 'collection').toString(); + headers = { ...headers }; + + await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None); + } + + // #endregion + + // #region Resource + + async getAllResourceRefs(resource: ServerResource, collection?: string): Promise<IResourceRefHandle[]> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const uri = joinPath(this.userDataSyncStoreUrl, 'resource', path); + const uri = this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource); const headers: IHeaders = {}; const context = await this.request(uri.toString(), { type: 'GET', headers }, [], CancellationToken.None); @@ -245,12 +292,12 @@ export class UserDataSyncStoreClient extends Disposable { return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } - async resolveResourceContent(path: string, ref: string, headers: IHeaders = {}): Promise<string | null> { + async resolveResourceContent(resource: ServerResource, ref: string, collection?: string, headers: IHeaders = {}): Promise<string | null> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStoreUrl, 'resource', path, ref).toString(); + const url = joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), ref).toString(); headers = { ...headers }; headers['Cache-Control'] = 'no-cache'; @@ -259,23 +306,34 @@ export class UserDataSyncStoreClient extends Disposable { return content; } - async deleteResource(path: string, ref: string | null): Promise<void> { + async deleteResource(resource: ServerResource, ref: string | null, collection?: string): Promise<void> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = ref !== null ? joinPath(this.userDataSyncStoreUrl, 'resource', path, ref).toString() : joinPath(this.userDataSyncStoreUrl, 'resource', path).toString(); + const url = ref !== null ? joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), ref).toString() : this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource).toString(); const headers: IHeaders = {}; await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None); } - async readResource(path: string, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> { + async deleteResources(): Promise<void> { + if (!this.userDataSyncStoreUrl) { + throw new Error('No settings sync store url configured.'); + } + + const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString(); + const headers: IHeaders = { 'Content-Type': Mimes.text }; + + await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None); + } + + async readResource(resource: ServerResource, oldValue: IUserData | null, collection?: string, headers: IHeaders = {}): Promise<IUserData> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStoreUrl, 'resource', path, 'latest').toString(); + const url = joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), 'latest').toString(); headers = { ...headers }; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -307,12 +365,12 @@ export class UserDataSyncStoreClient extends Disposable { return userData; } - async writeResource(path: string, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> { + async writeResource(resource: ServerResource, data: string, ref: string | null, collection?: string, headers: IHeaders = {}): Promise<string> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStoreUrl, 'resource', path).toString(); + const url = this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource).toString(); headers = { ...headers }; headers['Content-Type'] = Mimes.text; if (ref) { @@ -328,6 +386,8 @@ export class UserDataSyncStoreClient extends Disposable { return newRef; } + // #endregion + async manifest(oldValue: IUserDataManifest | null, headers: IHeaders = {}): Promise<IUserDataManifest | null> { if (!this.userDataSyncStoreUrl) { throw new Error('No settings sync store url configured.'); @@ -388,15 +448,17 @@ export class UserDataSyncStoreClient extends Disposable { throw new Error('No settings sync store url configured.'); } - const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString(); - const headers: IHeaders = { 'Content-Type': Mimes.text }; - - await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None); + await this.deleteResources(); + await this.deleteCollection(); // clear cached session. this.clearSession(); } + private getResourceUrl(userDataSyncStoreUrl: URI, collection: string | undefined, resource: ServerResource): URI { + return collection ? joinPath(userDataSyncStoreUrl, 'collection', collection, 'resource', resource) : joinPath(userDataSyncStoreUrl, 'resource', resource); + } + private clearSession(): void { this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.APPLICATION); this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.APPLICATION); @@ -551,32 +613,6 @@ export class UserDataSyncStoreService extends UserDataSyncStoreClient implements this._register(userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => this.updateUserDataSyncStoreUrl(userDataSyncStoreManagementService.userDataSyncStore?.url))); } - getAllRefs(resource: ServerResource, profile?: string): Promise<IResourceRefHandle[]> { - return this.getAllResourceRefs(profile ? this.getProfileResource(resource, profile) : resource); - } - - read(resource: ServerResource, oldValue: IUserData | null, profile?: string, headers?: IHeaders): Promise<IUserData> { - return this.readResource(profile ? this.getProfileResource(resource, profile) : resource, oldValue, headers); - } - - write(resource: ServerResource, content: string, ref: string | null, profile?: string, headers?: IHeaders): Promise<string> { - return this.writeResource(profile ? this.getProfileResource(resource, profile) : resource, content, ref, headers); - } - - delete(resource: ServerResource, ref: string | null, profile?: string): Promise<void> { - return this.deleteResource(profile ? this.getProfileResource(resource, profile) : resource, ref); - } - - resolveContent(resource: ServerResource, ref: string, profile?: string, headers?: IHeaders): Promise<string | null> { - return this.resolveResourceContent(profile ? this.getProfileResource(resource, profile) : resource, ref, headers); - } - - private getProfileResource(resource: ServerResource, profile: string): string { - if (resource === 'profiles') { - throw new Error(`Invalid Resource Argument: ${resource}`); - } - return join('profiles', profile, resource); - } } export class RequestsSession { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index c81de3f5134..ba4adbedc26 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -144,7 +144,7 @@ export class UserDataSyncClient extends Disposable { } read(resource: SyncResource): Promise<IUserData> { - return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null); + return this.instantiationService.get(IUserDataSyncStoreService).readResource(resource, null); } manifest(): Promise<IUserDataManifest | null> { @@ -219,6 +219,9 @@ export class UserDataSyncTestServer implements IRequestService { if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') { return this.clear(options.headers); } + if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'collection') { + return this.toResponse(204); + } return this.toResponse(501); } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 260b35b44b9..180502a1b2e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -332,6 +332,7 @@ suite('UserDataSyncService', () => { assert.deepStrictEqual(target.requests, [ // Manifest { type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} }, + { type: 'DELETE', url: `${target.url}/v1/collection`, headers: {} }, ]); }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 4f4c8fe12d3..08445ac0e43 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -120,7 +120,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(null); const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); target.reset(); await testObject.manifest(null); @@ -139,7 +139,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(null); const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); @@ -159,12 +159,12 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(null); const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); await testObject.manifest(null); target.reset(); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); assert.strictEqual(target.requestsWithAllHeaders.length, 1); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); @@ -180,12 +180,12 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(null); const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); await testObject.manifest(null); target.reset(); - await testObject.read(SyncResource.Settings, null); + await testObject.readResource(SyncResource.Settings, null); assert.strictEqual(target.requestsWithAllHeaders.length, 1); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); @@ -201,7 +201,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(null); const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); await testObject.manifest(null); await testObject.clear(); @@ -223,7 +223,7 @@ suite('UserDataSyncStoreService', () => { const testObject = client.instantiationService.get(IUserDataSyncStoreService); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -235,7 +235,7 @@ suite('UserDataSyncStoreService', () => { const client2 = disposableStore.add(new UserDataSyncClient(target)); await client2.setUp(); const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); - await testObject2.write(SyncResource.Settings, 'some content', null); + await testObject2.writeResource(SyncResource.Settings, 'some content', null); target.reset(); await testObject.manifest(null); @@ -255,7 +255,7 @@ suite('UserDataSyncStoreService', () => { const testObject = client.instantiationService.get(IUserDataSyncStoreService); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -267,7 +267,7 @@ suite('UserDataSyncStoreService', () => { const client2 = disposableStore.add(new UserDataSyncClient(target)); await client2.setUp(); const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); - await testObject2.write(SyncResource.Settings, 'some content', null); + await testObject2.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); @@ -288,7 +288,7 @@ suite('UserDataSyncStoreService', () => { const testObject = client.instantiationService.get(IUserDataSyncStoreService); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -319,7 +319,7 @@ suite('UserDataSyncStoreService', () => { const testObject = client.instantiationService.get(IUserDataSyncStoreService); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -349,7 +349,7 @@ suite('UserDataSyncStoreService', () => { const testObject = client.instantiationService.get(IUserDataSyncStoreService); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -363,7 +363,7 @@ suite('UserDataSyncStoreService', () => { await testObject2.clear(); await testObject.manifest(null); - await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.writeResource(SyncResource.Settings, 'some content', null); await testObject.manifest(null); target.reset(); await testObject.manifest(null); @@ -454,8 +454,8 @@ suite('UserDataSyncStoreService', () => { await client.sync(); const testObject = client.instantiationService.get(IUserDataSyncStoreService); - const expected = await testObject.read(SyncResource.Settings, null); - const actual = await testObject.read(SyncResource.Settings, expected); + const expected = await testObject.readResource(SyncResource.Settings, null); + const actual = await testObject.readResource(SyncResource.Settings, expected); assert.strictEqual(actual, expected); }); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index dea36b686a5..429b5ae994c 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -666,7 +666,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // shutdown as much as possible by destroying the window // and then calling the normal `quit` routine. if (this.environmentMainService.args['enable-smoke-test-driver']) { - this.destroyWindow(false, false); + await this.destroyWindow(false, false); this.lifecycleMainService.quit(); // still allow for an orderly shutdown return; } @@ -703,7 +703,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Handle choice if (result.response !== 1 /* keep waiting */) { const reopen = result.response === 0; - this.destroyWindow(reopen, result.checkboxChecked); + await this.destroyWindow(reopen, result.checkboxChecked); } } @@ -733,7 +733,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Handle choice const reopen = result.response === 0; - this.destroyWindow(reopen, result.checkboxChecked); + await this.destroyWindow(reopen, result.checkboxChecked); } break; } @@ -775,7 +775,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Delegate to windows service - const [window] = this.windowsMainService.open({ + const [window] = await this.windowsMainService.open({ context: OpenContext.API, userEnv: this._config.userEnv, cli: { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 8d42890f5f4..7825919d377 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -26,10 +26,11 @@ export interface IWindowsMainService { readonly onDidTriggerSystemContextMenu: Event<{ window: ICodeWindow; x: number; y: number }>; readonly onDidDestroyWindow: Event<ICodeWindow>; - open(openConfig: IOpenConfiguration): ICodeWindow[]; - openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; + open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]>; + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): Promise<ICodeWindow[]>; + openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise<ICodeWindow[]>; + openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts index ad0cc929fd5..c8d34be7505 100644 --- a/src/vs/platform/windows/electron-main/windowsFinder.ts +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -8,13 +8,13 @@ import { URI } from 'vs/base/common/uri'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; import { IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; -export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | undefined): ICodeWindow | undefined { +export async function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => Promise<IResolvedWorkspace | undefined>): Promise<ICodeWindow | undefined> { // First check for windows with workspaces that have a parent folder of the provided path opened for (const window of windows) { const workspace = window.openedWorkspace; if (isWorkspaceIdentifier(workspace)) { - const resolvedWorkspace = localWorkspaceResolver(workspace); + const resolvedWorkspace = await localWorkspaceResolver(workspace); // resolved workspace: folders are known and can be compared with if (resolvedWorkspace) { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 64484bd227f..4f1f9d43384 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { app, BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; -import { statSync } from 'fs'; +import { Promises } from 'vs/base/node/pfs'; import { hostname, release } from 'os'; import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -247,7 +247,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic })); } - openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): Promise<ICodeWindow[]> { const cli = this.environmentMainService.args; const remoteAuthority = options?.remoteAuthority || undefined; const forceEmpty = true; @@ -266,7 +266,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.handleWaitMarkerFile(openConfig, [window]); } - open(openConfig: IOpenConfiguration): ICodeWindow[] { + async open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]> { this.logService.trace('windowsManager#open'); if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { @@ -285,7 +285,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let emptyToOpen = 0; // Identify things to open from open config - const pathsToOpen = this.getPathsToOpen(openConfig); + const pathsToOpen = await this.getPathsToOpen(openConfig); this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { if (isSingleFolderWorkspacePathToOpen(path)) { @@ -332,7 +332,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (openConfig.initialStartup) { // Untitled workspaces are always restored - untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspacesSync()); + untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspaces()); workspacesToOpen.push(...untitledWorkspacesToRestore); // Empty windows with backups are always restored @@ -342,7 +342,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const { windows: usedWindows, filesOpenedInWindow } = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, emptyToOpen, filesToOpen, foldersToAdd); + const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, emptyToOpen, filesToOpen, foldersToAdd); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, emptyToOpen: ${emptyToOpen})`); @@ -438,7 +438,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - private doOpen( + private async doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], foldersToOpen: ISingleFolderWorkspacePathToOpen[], @@ -446,7 +446,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic emptyToOpen: number, filesToOpen: IFilesToOpen | undefined, foldersToAdd: ISingleFolderWorkspacePathToOpen[] - ): { windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined } { + ): Promise<{ windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined }> { // Keep track of used windows and remember // if files have been opened in one of them @@ -492,7 +492,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let windowToUseForFiles: ICodeWindow | undefined = undefined; if (fileToCheck?.fileUri && !openFilesInNewWindow) { if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { - windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : undefined); + windowToUseForFiles = await findWindowOnFile(windows, fileToCheck.fileUri, async workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspace(workspace.configPath) : undefined); } if (!windowToUseForFiles) { @@ -701,14 +701,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); } - private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { + private async getPathsToOpen(openConfig: IOpenConfiguration): Promise<IPathToOpen[]> { let pathsToOpen: IPathToOpen[]; let isCommandLineOrAPICall = false; let restoredWindows = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { - pathsToOpen = this.doExtractPathsFromAPI(openConfig); + pathsToOpen = await this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } @@ -719,7 +719,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { - pathsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + pathsToOpen = await this.doExtractPathsFromCLI(openConfig.cli); if (pathsToOpen.length === 0) { pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line } @@ -729,7 +729,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Extract paths: from previous session else { - pathsToOpen = this.doGetPathsFromLastSession(); + pathsToOpen = await this.doGetPathsFromLastSession(); if (pathsToOpen.length === 0) { pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore } @@ -746,7 +746,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; if (foldersToOpen.every(folderToOpen => isEqualAuthority(folderToOpen.remoteAuthority, remoteAuthority))) { // only if all folder have the same authority - const workspace = this.workspacesManagementMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); + const workspace = await this.workspacesManagementMainService.createUntitledWorkspace(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby pathsToOpen.push({ workspace, remoteAuthority }); @@ -761,48 +761,53 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Use `unshift` to ensure any new window to open comes last // for proper focus treatment. if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue<IWindowSettings | undefined>('window')?.restoreWindows === 'preserve') { - pathsToOpen.unshift(...this.doGetPathsFromLastSession().filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); + const lastSessionPaths = await this.doGetPathsFromLastSession(); + pathsToOpen.unshift(...lastSessionPaths.filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } return pathsToOpen; } - private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] { - const pathsToOpen: IPathToOpen[] = []; - const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode, remoteAuthority: openConfig.remoteAuthority }; - for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) { - const path = this.resolveOpenable(pathToOpen, pathResolveOptions); + private async doExtractPathsFromAPI(openConfig: IOpenConfiguration): Promise<IPathToOpen[]> { + const pathResolveOptions: IPathResolveOptions = { + gotoLineMode: openConfig.gotoLineMode, + remoteAuthority: openConfig.remoteAuthority + }; + + const pathsToOpen = await Promise.all(coalesce(openConfig.urisToOpen || []).map(async pathToOpen => { + const path = await this.resolveOpenable(pathToOpen, pathResolveOptions); // Path exists if (path) { path.label = pathToOpen.label; - pathsToOpen.push(path); + + return path; } // Path does not exist: show a warning box - else { - const uri = this.resourceFromOpenable(pathToOpen); - - const options: MessageBoxOptions = { - title: this.productService.nameLong, - type: 'info', - buttons: [mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], - defaultId: 0, - message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), - detail: uri.scheme === Schemas.file ? - localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri, { os: OS, tildify: this.environmentMainService })) : - localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)), - noLink: true - }; + const uri = this.resourceFromOpenable(pathToOpen); + + const options: MessageBoxOptions = { + title: this.productService.nameLong, + type: 'info', + buttons: [mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], + defaultId: 0, + message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), + detail: uri.scheme === Schemas.file ? + localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri, { os: OS, tildify: this.environmentMainService })) : + localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)), + noLink: true + }; + + this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - } - } + return undefined; + })); - return pathsToOpen; + return coalesce(pathsToOpen); } - private doExtractPathsFromCLI(cli: NativeParsedArgs): IPath[] { + private async doExtractPathsFromCLI(cli: NativeParsedArgs): Promise<IPath[]> { const pathsToOpen: IPathToOpen[] = []; const pathResolveOptions: IPathResolveOptions = { ignoreFileNotFound: true, @@ -819,39 +824,39 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // folder uris const folderUris = cli['folder-uri']; if (folderUris) { - for (const rawFolderUri of folderUris) { + const resolvedFolderUris = await Promise.all(folderUris.map(rawFolderUri => { const folderUri = this.cliArgToUri(rawFolderUri); - if (folderUri) { - const path = this.resolveOpenable({ folderUri }, pathResolveOptions); - if (path) { - pathsToOpen.push(path); - } + if (!folderUri) { + return undefined; } - } + + return this.resolveOpenable({ folderUri }, pathResolveOptions); + })); + + pathsToOpen.push(...coalesce(resolvedFolderUris)); } // file uris const fileUris = cli['file-uri']; if (fileUris) { - for (const rawFileUri of fileUris) { + const resolvedFileUris = await Promise.all(fileUris.map(rawFileUri => { const fileUri = this.cliArgToUri(rawFileUri); - if (fileUri) { - const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); - if (path) { - pathsToOpen.push(path); - } + if (!fileUri) { + return undefined; } - } + + return this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); + })); + + pathsToOpen.push(...coalesce(resolvedFileUris)); } // folder or file paths - const cliPaths = cli._; - for (const cliPath of cliPaths) { - const path = pathResolveOptions.remoteAuthority ? this.doResolvePathRemote(cliPath, pathResolveOptions) : this.doResolveFilePath(cliPath, pathResolveOptions); - if (path) { - pathsToOpen.push(path); - } - } + const resolvedCliPaths = await Promise.all(cli._.map(cliPath => { + return pathResolveOptions.remoteAuthority ? this.doResolveRemotePath(cliPath, pathResolveOptions) : this.doResolveFilePath(cliPath, pathResolveOptions); + })); + + pathsToOpen.push(...coalesce(resolvedCliPaths)); return pathsToOpen; } @@ -873,7 +878,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return undefined; } - private doGetPathsFromLastSession(): IPathToOpen[] { + private async doGetPathsFromLastSession(): Promise<IPathToOpen[]> { const restoreWindowsSetting = this.getRestoreWindowsSetting(); switch (restoreWindowsSetting) { @@ -899,32 +904,33 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic lastSessionWindows.push(this.windowsStateHandler.state.lastActiveWindow); } - const pathsToOpen: IPathToOpen[] = []; - for (const lastSessionWindow of lastSessionWindows) { + const pathsToOpen = await Promise.all(lastSessionWindows.map(async lastSessionWindow => { // Workspaces if (lastSessionWindow.workspace) { - const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority, rejectTransientWorkspaces: true /* https://github.com/microsoft/vscode/issues/119695 */ }); + const pathToOpen = await this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority, rejectTransientWorkspaces: true /* https://github.com/microsoft/vscode/issues/119695 */ }); if (isWorkspacePathToOpen(pathToOpen)) { - pathsToOpen.push(pathToOpen); + return pathToOpen; } } // Folders else if (lastSessionWindow.folderUri) { - const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + const pathToOpen = await this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { - pathsToOpen.push(pathToOpen); + return pathToOpen; } } // Empty window, potentially editors open to be restored else if (restoreWindowsSetting !== 'folders' && lastSessionWindow.backupPath) { - pathsToOpen.push({ backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }); + return { backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }; } - } - return pathsToOpen; + return undefined; + })); + + return coalesce(pathsToOpen); } } } @@ -945,7 +951,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } - private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = Object.create(null)): IPathToOpen | undefined { + private async resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = Object.create(null)): Promise<IPathToOpen | undefined> { // handle file:// openables with some extra validation const uri = this.resourceFromOpenable(openable); @@ -1008,7 +1014,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private doResolveFilePath(path: string, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { + private async doResolveFilePath(path: string, options: IPathResolveOptions): Promise<IPathToOpen<ITextEditorOptions> | undefined> { // Extract line/col information from path let lineNumber: number | undefined; @@ -1021,14 +1027,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic path = sanitizeFilePath(normalize(path), cwd()); try { - const pathStat = statSync(path); + const pathStat = await Promises.stat(path); // File if (pathStat.isFile()) { // Workspace (unless disabled via flag) if (!options.forceOpenWorkspaceAsFile) { - const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path)); + const workspace = await this.workspacesManagementMainService.resolveLocalWorkspace(URI.file(path)); if (workspace) { // If the workspace is transient and we are to ignore @@ -1096,7 +1102,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return undefined; } - private doResolvePathRemote(path: string, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { + private doResolveRemotePath(path: string, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { const first = path.charCodeAt(0); const remoteAuthority = options.remoteAuthority; @@ -1197,7 +1203,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { openFolderInNewWindow: !!openFolderInNewWindow, openFilesInNewWindow }; } - openExtensionDevelopmentHostWindow(extensionDevelopmentPaths: string[], openConfig: IOpenConfiguration): ICodeWindow[] { + async openExtensionDevelopmentHostWindow(extensionDevelopmentPaths: string[], openConfig: IOpenConfiguration): Promise<ICodeWindow[]> { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index f211c37ae0a..9b64d540ba4 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -28,7 +28,7 @@ suite('WindowsFinder', () => { }; const testWorkspaceFolders = toWorkspaceFolders([{ path: join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); - const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : undefined; }; + const localWorkspaceResolver = async (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : undefined; }; function createTestCodeWindow(options: { lastFocusTime: number; openedFolderUri?: URI; openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { return new class implements ICodeWindow { @@ -83,28 +83,28 @@ suite('WindowsFinder', () => { noVscodeFolderWindow, ]; - test('New window without folder when no windows exist', () => { - assert.strictEqual(findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), undefined); - assert.strictEqual(findWindowOnFile([], URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), undefined); + test('New window without folder when no windows exist', async () => { + assert.strictEqual(await findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), undefined); + assert.strictEqual(await findWindowOnFile([], URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), undefined); }); - test('Existing window with folder', () => { - assert.strictEqual(findWindowOnFile(windows, URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); + test('Existing window with folder', async () => { + assert.strictEqual(await findWindowOnFile(windows, URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); - assert.strictEqual(findWindowOnFile(windows, URI.file(join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); + assert.strictEqual(await findWindowOnFile(windows, URI.file(join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(join(fixturesFolder, 'vscode_folder', 'nested_folder')) }); - assert.strictEqual(findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + assert.strictEqual(await findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); - test('More specific existing window wins', () => { + test('More specific existing window wins', async () => { const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(join(fixturesFolder, 'no_vscode_folder')) }); const nestedFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }); - assert.strictEqual(findWindowOnFile([window, nestedFolderWindow], URI.file(join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); + assert.strictEqual(await findWindowOnFile([window, nestedFolderWindow], URI.file(join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); }); - test('Workspace folder wins', () => { + test('Workspace folder wins', async () => { const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); - assert.strictEqual(findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + assert.strictEqual(await findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); }); diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 7edf65f704b..de73f7f5572 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { BrowserWindow, MessageBoxOptions } from 'electron'; -import { existsSync, mkdirSync, readFileSync } from 'fs'; import { Emitter, Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -14,7 +13,7 @@ import { dirname, join } from 'vs/base/common/path'; import { basename, extUriBiasedIgnorePathCase, joinPath, originalFSPath } from 'vs/base/common/resources'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { Promises, readdirSync, rimrafSync, writeFileSync } from 'vs/base/node/pfs'; +import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; @@ -46,15 +45,12 @@ export interface IWorkspacesManagementMainService { enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | undefined>; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>; - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>; - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; + getUntitledWorkspaces(): IUntitledWorkspaceInfo[]; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | undefined; resolveLocalWorkspace(path: URI): Promise<IResolvedWorkspace | undefined>; getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>; @@ -64,14 +60,16 @@ export class WorkspacesManagementMainService extends Disposable implements IWork declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome = this.environmentMainService.untitledWorkspacesHome; // local URI that contains all untitled workspaces - private readonly _onDidDeleteUntitledWorkspace = this._register(new Emitter<IWorkspaceIdentifier>()); readonly onDidDeleteUntitledWorkspace: Event<IWorkspaceIdentifier> = this._onDidDeleteUntitledWorkspace.event; private readonly _onDidEnterWorkspace = this._register(new Emitter<IWorkspaceEnteredEvent>()); readonly onDidEnterWorkspace: Event<IWorkspaceEnteredEvent> = this._onDidEnterWorkspace.event; + private readonly untitledWorkspacesHome = this.environmentMainService.untitledWorkspacesHome; // local URI that contains all untitled workspaces + + private untitledWorkspaces: IUntitledWorkspaceInfo[] = []; + constructor( @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILogService private readonly logService: ILogService, @@ -83,8 +81,28 @@ export class WorkspacesManagementMainService extends Disposable implements IWork super(); } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | undefined { - return this.doResolveLocalWorkspace(uri, path => readFileSync(path, 'utf8')); + async initialize(): Promise<void> { + + // Reset + this.untitledWorkspaces = []; + + // Resolve untitled workspaces + try { + const untitledWorkspacePaths = (await Promises.readdir(this.untitledWorkspacesHome.fsPath)).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); + for (const untitledWorkspacePath of untitledWorkspacePaths) { + const workspace = getWorkspaceIdentifier(untitledWorkspacePath); + const resolvedWorkspace = await this.resolveLocalWorkspace(untitledWorkspacePath); + if (!resolvedWorkspace) { + await this.deleteUntitledWorkspace(workspace); + } else { + this.untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); + } + } + } catch (error) { + if (error.code !== 'ENOENT') { + this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); + } + } } resolveLocalWorkspace(uri: URI): Promise<IResolvedWorkspace | undefined> { @@ -158,15 +176,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork await Promises.mkdir(dirname(configPath), { recursive: true }); await Promises.writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); - return workspace; - } - - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - mkdirSync(dirname(configPath), { recursive: true }); - writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); + this.untitledWorkspaces.push({ workspace, remoteAuthority }); return workspace; } @@ -196,16 +206,16 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return isUntitledWorkspace(workspace.configPath, this.environmentMainService); } - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> { if (!this.isUntitledWorkspace(workspace)) { return; // only supported for untitled workspaces } // Delete from disk - this.doDeleteUntitledWorkspaceSync(workspace); + await this.doDeleteUntitledWorkspace(workspace); + // unset workspace from profiles if (this.userDataProfilesMainService.isEnabled()) { - // unset workspace from profiles this.userDataProfilesMainService.unsetWorkspace(workspace); } @@ -213,47 +223,28 @@ export class WorkspacesManagementMainService extends Disposable implements IWork this._onDidDeleteUntitledWorkspace.fire(workspace); } - async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> { - this.deleteUntitledWorkspaceSync(workspace); - } - - private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + private async doDeleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> { const configPath = originalFSPath(workspace.configPath); try { // Delete Workspace - rimrafSync(dirname(configPath)); + await Promises.rm(dirname(configPath)); // Mark Workspace Storage to be deleted const workspaceStoragePath = join(this.environmentMainService.workspaceStorageHome.fsPath, workspace.id); - if (existsSync(workspaceStoragePath)) { - writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); + if (await Promises.exists(workspaceStoragePath)) { + await Promises.writeFile(join(workspaceStoragePath, 'obsolete'), ''); } + + // Remove from list + this.untitledWorkspaces = this.untitledWorkspaces.filter(untitledWorkspace => untitledWorkspace.workspace.id !== workspace.id); } catch (error) { this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); } } - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { - const untitledWorkspaces: IUntitledWorkspaceInfo[] = []; - try { - const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); - for (const untitledWorkspacePath of untitledWorkspacePaths) { - const workspace = getWorkspaceIdentifier(untitledWorkspacePath); - const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); - if (!resolvedWorkspace) { - this.doDeleteUntitledWorkspaceSync(workspace); - } else { - untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); - } - } - } catch (error) { - if (error.code !== 'ENOENT') { - this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); - } - } - - return untitledWorkspaces; + getUntitledWorkspaces(): IUntitledWorkspaceInfo[] { + return this.untitledWorkspaces; } async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | undefined> { @@ -266,7 +257,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return undefined; // return early if the workspace is not valid } - const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); + const result = await this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); if (!result) { return undefined; } @@ -306,7 +297,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork return true; // OK } - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | undefined { + private async doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): Promise<IEnterWorkspaceResult | undefined> { if (!window.config) { return undefined; } @@ -316,12 +307,16 @@ export class WorkspacesManagementMainService extends Disposable implements IWork // Register window for backups and migrate current backups over let backupPath: string | undefined; if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackup({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + if (window.config.backupPath) { + backupPath = await this.backupMainService.registerWorkspaceBackup({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + } else { + backupPath = this.backupMainService.registerWorkspaceBackup({ workspace, remoteAuthority: window.remoteAuthority }); + } } // if the window was opened on an untitled workspace, delete it. if (isWorkspaceIdentifier(window.openedWorkspace) && this.isUntitledWorkspace(window.openedWorkspace)) { - this.deleteUntitledWorkspaceSync(window.openedWorkspace); + await this.deleteUntitledWorkspace(window.openedWorkspace); } // Update window configuration properly based on transition to workspace diff --git a/src/vs/platform/workspaces/node/workspaces.ts b/src/vs/platform/workspaces/node/workspaces.ts index 9ca6e95faa7..beffbbe2776 100644 --- a/src/vs/platform/workspaces/node/workspaces.ts +++ b/src/vs/platform/workspaces/node/workspaces.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createHash } from 'crypto'; -import { Stats, statSync } from 'fs'; +import { Stats } from 'fs'; import { Schemas } from 'vs/base/common/network'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { originalFSPath } from 'vs/base/common/resources'; @@ -53,13 +53,15 @@ export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: return createHash('md5').update(folderUri.toString()).digest('hex'); } - // Local: produce a hash from the path and include creation time as salt + // Local: we use the ctime as extra salt to the + // identifier so that folders getting recreated + // result in a different identifier. However, if + // the stat is not provided we return `undefined` + // to ensure identifiers are stable for the given + // URI. + if (!folderStat) { - try { - folderStat = statSync(folderUri.fsPath); - } catch (error) { - return undefined; // folder does not exist - } + return undefined; } let ctime: number | undefined; @@ -75,8 +77,6 @@ export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: } } - // we use the ctime as extra salt to the ID so that we catch the case of a folder getting - // deleted and recreated. in that case we do not want to carry over previous state return createHash('md5').update(folderUri.fsPath).update(ctime ? String(ctime) : '').digest('hex'); } diff --git a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts index d0a8bc804a7..42427b5ea6d 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts @@ -41,7 +41,7 @@ flakySuite('Workspaces', () => { fs.mkdirSync(path.join(testDir, 'f1')); const localExistingUri = URI.file(path.join(testDir, 'f1')); - const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri); + const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri, fs.statSync(localExistingUri.fsPath)); assert.ok(localExistingUriId?.id); }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index f45a4e289ff..362c33c1af7 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -53,7 +53,9 @@ flakySuite('WorkspacesManagementMainService', () => { isHotExitEnabled(): boolean { throw new Error('Method not implemented.'); } getEmptyWindowBackups(): IEmptyWindowBackupInfo[] { throw new Error('Method not implemented.'); } - registerWorkspaceBackup(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string { throw new Error('Method not implemented.'); } + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo): string; + registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom: string): Promise<string>; + registerWorkspaceBackup(workspaceInfo: unknown, migrateFrom?: unknown): string | Promise<string> { throw new Error('Method not implemented.'); } registerFolderBackup(folder: IFolderBackupInfo): string { throw new Error('Method not implemented.'); } registerEmptyWindowBackup(empty: IEmptyWindowBackupInfo): string { throw new Error('Method not implemented.'); } async getDirtyWorkspaces(): Promise<(IWorkspaceBackupInfo | IFolderBackupInfo)[]> { return []; } @@ -80,10 +82,6 @@ flakySuite('WorkspacesManagementMainService', () => { fs.writeFileSync(workspaceConfigPath, JSON.stringify(ws)); } - function createUntitledWorkspaceSync(folders: string[], names?: string[]) { - return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); - } - let testDir: string; let untitledWorkspacesHomePath: string; let environmentMainService: EnvironmentMainService; @@ -184,54 +182,6 @@ flakySuite('WorkspacesManagementMainService', () => { assert.strictEqual(ws.remoteAuthority, 'server'); }); - test('createWorkspaceSync (folders)', () => { - const workspace = createUntitledWorkspaceSync([cwd, tmpDir]); - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.strictEqual(ws.folders.length, 2); - assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, cwd); - assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, tmpDir); - - assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name); - assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name); - }); - - test('createWorkspaceSync (folders with names)', () => { - const workspace = createUntitledWorkspaceSync([cwd, tmpDir], ['currentworkingdirectory', 'tempdir']); - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.strictEqual(ws.folders.length, 2); - assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, cwd); - assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, tmpDir); - - assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[0]).name, 'currentworkingdirectory'); - assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[1]).name, 'tempdir'); - }); - - test('createUntitledWorkspaceSync (folders as other resource URIs)', () => { - const folder1URI = URI.parse('myscheme://server/work/p/f1'); - const folder2URI = URI.parse('myscheme://server/work/o/f3'); - - const workspace = service.createUntitledWorkspaceSync([{ uri: folder1URI }, { uri: folder2URI }]); - assert.ok(workspace); - assert.ok(fs.existsSync(workspace.configPath.fsPath)); - assert.ok(service.isUntitledWorkspace(workspace)); - - const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.strictEqual(ws.folders.length, 2); - assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[0]).uri, folder1URI.toString(true)); - assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[1]).uri, folder2URI.toString(true)); - - assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name); - assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name); - }); - test('resolveWorkspace', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); assert.ok(await service.resolveLocalWorkspace(workspace.configPath)); @@ -255,58 +205,35 @@ flakySuite('WorkspacesManagementMainService', () => { assert.ok(resolvedTransient?.transient); }); - test('resolveWorkspaceSync', async () => { - const workspace = await createUntitledWorkspace([cwd, tmpDir]); - assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath)); - - // make it a valid workspace path - const newPath = path.join(path.dirname(workspace.configPath.fsPath), `workspace.${WORKSPACE_EXTENSION}`); - fs.renameSync(workspace.configPath.fsPath, newPath); - workspace.configPath = URI.file(newPath); - - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.strictEqual(2, resolved!.folders.length); - assertEqualURI(resolved!.configPath, workspace.configPath); - assert.ok(resolved!.id); - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace - - const resolvedInvalid = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.ok(!resolvedInvalid); - - fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ transient: true, folders: [] })); // transient worksapce - const resolvedTransient = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.ok(resolvedTransient?.transient); - }); - - test('resolveWorkspaceSync (support relative paths)', async () => { + test('resolveWorkspace (support relative paths)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + const resolved = await service.resolveLocalWorkspace(workspace.configPath); assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); - test('resolveWorkspaceSync (support relative paths #2)', async () => { + test('resolveWorkspace (support relative paths #2)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + const resolved = await service.resolveLocalWorkspace(workspace.configPath); assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'other'))); }); - test('resolveWorkspaceSync (support relative paths #3)', async () => { + test('resolveWorkspace (support relative paths #3)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] })); - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + const resolved = await service.resolveLocalWorkspace(workspace.configPath); assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); - test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => { + test('resolveWorkspace (support invalid JSON via fault tolerant parsing)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma - const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); + const resolved = await service.resolveLocalWorkspace(workspace.configPath); assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib'))); }); @@ -366,7 +293,7 @@ flakySuite('WorkspacesManagementMainService', () => { const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); assert.strictEqual(0, newContent.indexOf('// this is a comment')); - service.deleteUntitledWorkspaceSync(workspace); + await service.deleteUntitledWorkspace(workspace); }); test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => { @@ -379,7 +306,7 @@ flakySuite('WorkspacesManagementMainService', () => { const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0)); - service.deleteUntitledWorkspaceSync(workspace); + await service.deleteUntitledWorkspace(workspace); }); (!isWindows ? test.skip : test)('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { @@ -397,34 +324,37 @@ flakySuite('WorkspacesManagementMainService', () => { assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, folder2Location); assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, 'inner/more'); - service.deleteUntitledWorkspaceSync(workspace); + await service.deleteUntitledWorkspace(workspace); }); - test('deleteUntitledWorkspaceSync (untitled)', async () => { + test('deleteUntitledWorkspace (untitled)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); assert.ok(fs.existsSync(workspace.configPath.fsPath)); - service.deleteUntitledWorkspaceSync(workspace); + await service.deleteUntitledWorkspace(workspace); assert.ok(!fs.existsSync(workspace.configPath.fsPath)); }); - test('deleteUntitledWorkspaceSync (saved)', async () => { + test('deleteUntitledWorkspace (saved)', async () => { const workspace = await createUntitledWorkspace([cwd, tmpDir]); - service.deleteUntitledWorkspaceSync(workspace); + await service.deleteUntitledWorkspace(workspace); }); - test('getUntitledWorkspaceSync', async function () { - let untitled = service.getUntitledWorkspacesSync(); + test('getUntitledWorkspace', async function () { + await service.initialize(); + let untitled = service.getUntitledWorkspaces(); assert.strictEqual(untitled.length, 0); const untitledOne = await createUntitledWorkspace([cwd, tmpDir]); assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); - untitled = service.getUntitledWorkspacesSync(); + await service.initialize(); + untitled = service.getUntitledWorkspaces(); assert.strictEqual(1, untitled.length); assert.strictEqual(untitledOne.id, untitled[0].workspace.id); - service.deleteUntitledWorkspaceSync(untitledOne); - untitled = service.getUntitledWorkspacesSync(); + await service.deleteUntitledWorkspace(untitledOne); + await service.initialize(); + untitled = service.getUntitledWorkspaces(); assert.strictEqual(0, untitled.length); }); }); diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index db42c9ffe3b..d7b1c9b307e 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -12,7 +12,7 @@ import { cwd } from 'vs/base/common/process'; import { dirname, extname, resolve, join } from 'vs/base/common/path'; import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions, ErrorReporter } from 'vs/platform/environment/node/argv'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import { createWaitMarkerFileSync } from 'vs/platform/environment/node/wait'; import { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; import { hasStdinWithoutTty, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; @@ -306,7 +306,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise<vo console.log('At least one file must be provided to wait for.'); return; } - waitMarkerFilePath = createWaitMarkerFile(verbose); + waitMarkerFilePath = createWaitMarkerFileSync(verbose); } sendToPipe({ diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index d6b554ebb17..51986a3ad3c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookData as NotebookData, NotebookExtensionDescription, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookData as NotebookData, NotebookExtensionDescription, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookContentProvider, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookShape, MainContext, MainThreadNotebookShape } from '../common/extHost.protocol'; @@ -67,15 +67,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { transientOptions: contentOptions }; }, - save: async (uri: URI, token: CancellationToken) => { - return this._proxy.$saveNotebook(viewType, uri, token); - }, - saveAs: async (uri: URI, target: URI, token: CancellationToken) => { - return this._proxy.$saveNotebookAs(viewType, uri, target, token); - }, - backup: async (uri: URI, token: CancellationToken) => { - return this._proxy.$backupNotebook(viewType, uri, token); - } + backup: async (uri: URI, token: CancellationToken) => '' }; const disposable = new DisposableStore(); @@ -86,19 +78,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { this._notebookProviders.set(viewType, { controller, disposable }); } - async $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata }): Promise<void> { - const provider = this._notebookProviders.get(viewType); - - if (provider && options) { - provider.controller.options = options; - this._notebookService.listNotebookDocuments().forEach(document => { - if (document.viewType === viewType) { - document.transientOptions = provider.controller.options; - } - }); - } - } - async $unregisterNotebookProvider(viewType: string): Promise<void> { const entry = this._notebookProviders.get(viewType); if (entry) { @@ -107,7 +86,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): void { const registration = this._notebookService.registerNotebookSerializer(viewType, extension, { options, diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 136d77a68c0..30702b46cd8 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -90,6 +90,10 @@ abstract class MainThreadKernel implements INotebookKernel { this.implementsExecutionOrder = data.supportsExecutionOrder; event.hasExecutionOrder = true; } + if (data.supportsInterrupt !== undefined) { + this.implementsInterrupt = data.supportsInterrupt; + event.hasInterruptHandler = true; + } this._onDidChange.fire(event); } diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts index 1c49ff84612..7fc600da2fd 100644 --- a/src/vs/workbench/api/browser/mainThreadSecretState.ts +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -36,9 +36,11 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre } async $getPassword(extensionId: string, key: string): Promise<string | undefined> { + this.logService.trace(`Getting password for ${extensionId} extension:`, key); const fullKey = await this.getFullKey(extensionId); const password = await this.credentialsService.getPassword(fullKey, key); if (!password) { + this.logService.trace('No password found for:', key); return undefined; } @@ -63,6 +65,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre try { const value = JSON.parse(decrypted); if (value.extensionId === extensionId) { + this.logService.trace('Password found for:', key); return value.content; } } catch (parseError) { @@ -79,6 +82,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre } } + this.logService.trace('No password found for:', key); return undefined; } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 9435d9b46fe..4fcd4b8c103 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -172,7 +172,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible))); this._register(treeView.onDidChangeCheckboxState(items => { this._proxy.$changeCheckboxState(treeViewId, <CheckboxUpdate[]>items.map(item => { - return { treeItemHandle: item.handle, newState: item.checkboxChecked ?? false }; + return { treeItemHandle: item.handle, newState: item.checkbox?.isChecked ?? false }; })); })); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9a7f2faa5b5..3de62a38d34 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -157,7 +157,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extensionStoragePaths)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 98e42625518..d355265276e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -962,7 +962,6 @@ export interface INotebookCellStatusBarListDto { export interface MainThreadNotebookShape extends IDisposable { $registerNotebookProvider(extension: notebookCommon.NotebookExtensionDescription, viewType: string, options: notebookCommon.TransientOptions, registration: notebookCommon.INotebookContributionData | undefined): Promise<void>; - $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: notebookCommon.TransientCellMetadata; transientDocumentMetadata: notebookCommon.TransientDocumentMetadata }): Promise<void>; $unregisterNotebookProvider(viewType: string): Promise<void>; $registerNotebookSerializer(handle: number, extension: notebookCommon.NotebookExtensionDescription, viewType: string, options: notebookCommon.TransientOptions, registration: notebookCommon.INotebookContributionData | undefined): void; @@ -2056,9 +2055,6 @@ export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditors $releaseNotebookCellStatusBarItems(id: number): void; $openNotebook(viewType: string, uri: UriComponents, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise<SerializableObjectWithBuffers<NotebookDataDto>>; - $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean>; - $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean>; - $backupNotebook(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise<string>; $dataToNotebook(handle: number, data: VSBuffer, token: CancellationToken): Promise<SerializableObjectWithBuffers<NotebookDataDto>>; $notebookToData(handle: number, data: SerializableObjectWithBuffers<NotebookDataDto>, token: CancellationToken): Promise<VSBuffer>; diff --git a/src/vs/workbench/api/common/extHostBulkEdits.ts b/src/vs/workbench/api/common/extHostBulkEdits.ts index 144b992a360..e60b2ba1167 100644 --- a/src/vs/workbench/api/common/extHostBulkEdits.ts +++ b/src/vs/workbench/api/common/extHostBulkEdits.ts @@ -29,13 +29,12 @@ export class ExtHostBulkEdits { } applyWorkspaceEdit(edit: vscode.WorkspaceEdit, extension: IExtensionDescription, isRefactoring?: boolean): Promise<boolean> { - const allowSnippetTextEdit = isProposedApiEnabled(extension, 'snippetWorkspaceEdit'); const allowIsRefactoring = isProposedApiEnabled(extension, 'workspaceEditIsRefactoring'); if (isRefactoring && !allowIsRefactoring) { console.warn(`Extension '${extension.identifier.value}' uses a proposed API 'workspaceEditIsRefactoring' which is NOT enabled for it`); isRefactoring = undefined; } - const dto = WorkspaceEdit.from(edit, this._versionInformationProvider, allowSnippetTextEdit); + const dto = WorkspaceEdit.from(edit, this._versionInformationProvider); return this._proxy.$tryApplyWorkspaceEdit(dto, undefined, isRefactoring); } } diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 79aa5cd22f4..05b6fb85b9b 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -16,7 +16,6 @@ import { FileOperation } from 'vs/platform/files/common/files'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; class FileSystemWatcher implements vscode.FileSystemWatcher { @@ -250,11 +249,11 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer const dto: IWorkspaceEditDto = { edits: [] }; - for (const [extension, edit] of edits) { + for (const [, edit] of edits) { const { edits } = typeConverter.WorkspaceEdit.from(edit, { getTextDocumentVersion: uri => this._extHostDocumentsAndEditors.getDocument(uri)?.version, getNotebookDocumentVersion: () => undefined, - }, isProposedApiEnabled(extension, 'snippetWorkspaceEdit')); + }); dto.edits = dto.edits.concat(edits); } return { edit: dto, extensionNames: Array.from(extensionNames) }; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index a2ee6cb6c45..49384cee4af 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -446,7 +446,7 @@ class CodeActionAdapter { title: candidate.title, command: candidate.command && this._commands.toInternal(candidate.command, disposables), diagnostics: candidate.diagnostics && candidate.diagnostics.map(typeConvert.Diagnostic.from), - edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit, undefined, isProposedApiEnabled(this._extension, 'snippetWorkspaceEdit')), + edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit, undefined), kind: candidate.kind && candidate.kind.value, isPreferred: candidate.isPreferred, disabled: candidate.disabled?.reason @@ -467,7 +467,7 @@ class CodeActionAdapter { } const resolvedItem = (await this._provider.resolveCodeAction(item, token)) ?? item; return resolvedItem?.edit - ? typeConvert.WorkspaceEdit.from(resolvedItem.edit, undefined, isProposedApiEnabled(this._extension, 'snippetWorkspaceEdit')) + ? typeConvert.WorkspaceEdit.from(resolvedItem.edit, undefined) : undefined; } @@ -522,7 +522,7 @@ class DocumentPasteEditProvider { return { insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, - additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined, true) : undefined, + additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, }; } } @@ -1804,7 +1804,7 @@ class DocumentOnDropEditAdapter { } return { insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, - additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined, true) : undefined, + additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, }; } } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 6a1df44dc0e..4a3c51fa79b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -7,7 +7,6 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; -import { hash } from 'vs/base/common/hash'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { MarshalledId } from 'vs/base/common/marshallingIds'; @@ -20,7 +19,6 @@ import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatu import { ApiCommand, ApiCommandArgument, ApiCommandResult, CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { INotebookExclusiveDocumentFilter, INotebookContributionData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -75,7 +73,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { commands: ExtHostCommands, private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, - private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments); @@ -157,14 +154,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._notebookContentProviders.set(viewType, { extension, provider }); - let listener: IDisposable | undefined; - if (provider.onDidChangeNotebookContentOptions) { - listener = provider.onDidChangeNotebookContentOptions(() => { - const internalOptions = typeConverters.NotebookDocumentContentOptions.from(provider.options); - this._notebookProxy.$updateNotebookProviderOptions(viewType, internalOptions); - }); - } - this._notebookProxy.$registerNotebookProvider( { id: extension.identifier, location: extension.extensionLocation }, viewType, @@ -173,7 +162,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { ); return new extHostTypes.Disposable(() => { - listener?.dispose(); this._notebookContentProviders.delete(viewType); this._notebookProxy.$unregisterNotebookProvider(viewType); }); @@ -356,36 +344,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean> { - const document = this.getNotebookDocument(URI.revive(uri)); - const { provider } = this._getProviderData(viewType); - await provider.saveNotebook(document.apiNotebook, token); - return true; - } - - async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean> { - const document = this.getNotebookDocument(URI.revive(uri)); - const { provider } = this._getProviderData(viewType); - await provider.saveNotebookAs(URI.revive(target), document.apiNotebook, token); - return true; - } - - private _backupIdPool: number = 0; - - async $backupNotebook(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise<string> { - const document = this.getNotebookDocument(URI.revive(uri)); - const provider = this._getProviderData(viewType); - - const storagePath = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); - const fileName = String(hash([document.uri.toString(), this._backupIdPool++])); - const backupUri = URI.joinPath(storagePath, fileName); - - const backup = await provider.provider.backupNotebook(document.apiNotebook, { destination: backupUri }, cancellation); - document.updateBackup(backup); - return backup.id; - } - - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, data: INotebookEditorAddData) { if (this._editors.has(editorId)) { diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 240ae6eede7..c3ebc53b3d6 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -141,7 +141,6 @@ export class ExtHostNotebookDocument { private _metadata: Record<string, any>; private _versionId: number = 0; private _isDirty: boolean = false; - private _backup?: vscode.NotebookDocumentBackup; private _disposed: boolean = false; constructor( @@ -190,16 +189,6 @@ export class ExtHostNotebookDocument { return this._notebook; } - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - acceptDocumentPropertiesChanged(data: extHostProtocol.INotebookDocumentPropertiesChangeData) { if (data.metadata) { this._metadata = Object.freeze({ ...this._metadata, ...data.metadata }); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 0f5ebe088fb..22dd7d190ad 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -23,6 +23,8 @@ import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBu import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Promises } from 'vs/base/common/async'; +import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; +import { ViewColumn } from 'vs/workbench/api/common/extHostTypeConverters'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -177,14 +179,14 @@ export class ExtHostTerminal { return this._id; } - private _serializeParentTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, parentTerminal?: ExtHostTerminalIdentifier): TerminalLocation | vscode.TerminalEditorLocationOptions | { parentTerminal: ExtHostTerminalIdentifier } | undefined { + private _serializeParentTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, parentTerminal?: ExtHostTerminalIdentifier): TerminalLocation | { viewColumn: EditorGroupColumn; preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | undefined { if (typeof location === 'object') { if ('parentTerminal' in location && location.parentTerminal && parentTerminal) { return { parentTerminal }; } if ('viewColumn' in location) { - return { viewColumn: location.viewColumn, preserveFocus: location.preserveFocus }; + return { viewColumn: ViewColumn.from(location.viewColumn), preserveFocus: location.preserveFocus }; } return undefined; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 9a74d786aae..8a560d56add 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { CheckboxUpdate, DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; -import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, TreeCommand, TreeViewPaneHandleArg } from 'vs/workbench/common/views'; +import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, TreeCommand, TreeViewPaneHandleArg, ITreeItemCheckboxState } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; import { TreeItemCollapsibleState, TreeItemCheckboxState, ThemeIcon, MarkdownString as MarkdownStringType, TreeItem } from 'vs/workbench/api/common/extHostTypes'; @@ -91,8 +91,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { const dragMimeTypes = options.dragAndDropController?.dragMimeTypes ?? []; const hasHandleDrag = !!options.dragAndDropController?.handleDrag; const hasHandleDrop = !!options.dragAndDropController?.handleDrop; - const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop }); const treeView = this.createExtHostTreeView(viewId, options, extension); + const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop }); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, get onDidExpandElement() { return treeView.onDidExpandElement; }, @@ -750,9 +750,20 @@ class ExtHostTreeView<T> extends Disposable { return command ? { ...this.commands.toInternal(command, disposable), originalId: command.command } : undefined; } - private getCheckbox(extensionTreeItem: vscode.TreeItem2): boolean | undefined { - return (extensionTreeItem.checkboxState !== undefined) ? - extensionTreeItem.checkboxState === TreeItemCheckboxState.Checked : undefined; + private getCheckbox(extensionTreeItem: vscode.TreeItem2): ITreeItemCheckboxState | undefined { + if (!extensionTreeItem.checkboxState) { + return undefined; + } + let checkboxState: TreeItemCheckboxState; + let tooltip: string | undefined = undefined; + if (typeof extensionTreeItem.checkboxState === 'number') { + checkboxState = extensionTreeItem.checkboxState; + } + else { + checkboxState = extensionTreeItem.checkboxState.state; + tooltip = extensionTreeItem.checkboxState.tooltip; + } + return { isChecked: checkboxState === TreeItemCheckboxState.Checked, tooltip }; } private validateTreeItem(extensionTreeItem: vscode.TreeItem) { @@ -780,7 +791,7 @@ class ExtHostTreeView<T> extends Disposable { themeIcon: this.getThemeIcon(extensionTreeItem), collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState, accessibilityInformation: extensionTreeItem.accessibilityInformation, - checkboxChecked: this.getCheckbox(extensionTreeItem) + checkbox: this.getCheckbox(extensionTreeItem), }; return { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index aba6d2d85a2..3f6ffad9546 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -569,7 +569,7 @@ export namespace WorkspaceEdit { getNotebookDocumentVersion(uri: URI): number | undefined; } - export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider, allowSnippetTextEdit?: boolean): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; @@ -605,11 +605,6 @@ export namespace WorkspaceEdit { metadata: entry.metadata }); } else if (entry._type === types.FileEditType.Snippet) { - // snippet text edits - if (!allowSnippetTextEdit) { - console.warn(`DROPPING snippet text edit because proposal IS NOT ENABLED`, entry); - continue; - } result.edits.push(<languages.IWorkspaceTextEdit>{ resource: entry.uri, textEdit: { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d7f804b0aff..926fde45ea1 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -10,14 +10,14 @@ import { MarkdownString as BaseMarkdownString } from 'vs/base/common/htmlContent import { ResourceMap } from 'vs/base/common/map'; import { Mimes, normalizeMimeType } from 'vs/base/common/mime'; import { nextCharLength } from 'vs/base/common/strings'; -import { isString, isStringArray } from 'vs/base/common/types'; +import { isNumber, isObject, isString, isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRelativePatternDto } from 'vs/workbench/api/common/extHost.protocol'; -import { CellEditType, ICellPartialMetadataEdit, IDocumentMetadataEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, ICellPartialMetadataEdit, IDocumentMetadataEdit, isTextStreamMime } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -1321,6 +1321,7 @@ export class CodeActionKind { public static Refactor: CodeActionKind; public static RefactorExtract: CodeActionKind; public static RefactorInline: CodeActionKind; + public static RefactorMove: CodeActionKind; public static RefactorRewrite: CodeActionKind; public static Source: CodeActionKind; public static SourceOrganizeImports: CodeActionKind; @@ -1347,6 +1348,7 @@ CodeActionKind.QuickFix = CodeActionKind.Empty.append('quickfix'); CodeActionKind.Refactor = CodeActionKind.Empty.append('refactor'); CodeActionKind.RefactorExtract = CodeActionKind.Refactor.append('extract'); CodeActionKind.RefactorInline = CodeActionKind.Refactor.append('inline'); +CodeActionKind.RefactorMove = CodeActionKind.Refactor.append('move'); CodeActionKind.RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); CodeActionKind.Source = CodeActionKind.Empty.append('source'); CodeActionKind.SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); @@ -2519,7 +2521,10 @@ export class TreeItem { } if (treeItemThing.checkboxState !== undefined) { checkProposedApiEnabled(extension, 'treeItemCheckbox'); - if (treeItemThing.checkboxState !== TreeItemCheckboxState.Checked && treeItemThing.checkboxState !== TreeItemCheckboxState.Unchecked) { + const checkbox = isNumber(treeItemThing.checkboxState) ? treeItemThing.checkboxState : + isObject(treeItemThing.checkboxState) && isNumber(treeItemThing.checkboxState.state) ? treeItemThing.checkboxState.state : undefined; + const tooltip = !isNumber(treeItemThing.checkboxState) && isObject(treeItemThing.checkboxState) ? treeItemThing.checkboxState.tooltip : undefined; + if (checkbox === undefined || (checkbox !== TreeItemCheckboxState.Checked && checkbox !== TreeItemCheckboxState.Unchecked) || (tooltip !== undefined && !isString(tooltip))) { console.log('INVALID tree item, invalid checkboxState', treeItemThing.checkboxState); return false; } @@ -3515,7 +3520,8 @@ export class NotebookCellOutput { for (let i = 0; i < items.length; i++) { const item = items[i]; const normalMime = normalizeMimeType(item.mime); - if (!seen.has(normalMime)) { + // We can have multiple text stream mime types in the same output. + if (!seen.has(normalMime) || isTextStreamMime(normalMime)) { seen.add(normalMime); continue; } diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index c61334ba316..09e785c307b 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -19,8 +19,6 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { isEqual } from 'vs/base/common/resources'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import { generateUuid } from 'vs/base/common/uuid'; import { Event } from 'vs/base/common/event'; import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -54,12 +52,7 @@ suite('NotebookCell#Document', function () { }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - const extHostStoragePaths = new class extends mock<IExtensionStoragePaths>() { - override workspaceValue() { - return URI.from({ scheme: 'test', path: generateUuid() }); - } - }; - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); const reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 54eb5519c6f..d159691ba03 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -7,7 +7,6 @@ import * as assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadCommandsShape, MainThreadNotebookDocumentsShape, MainThreadNotebookKernelsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -19,7 +18,6 @@ import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebo import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { NotebookCellOutput, NotebookCellOutputItem } from 'vs/workbench/api/common/extHostTypes'; import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -90,13 +88,8 @@ suite('NotebookKernel', function () { }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - const extHostStoragePaths = new class extends mock<IExtensionStoragePaths>() { - override workspaceValue() { - return URI.from({ scheme: 'test', path: generateUuid() }); - } - }; extHostCommands = new ExtHostCommands(rpcProtocol, new NullLogService()); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); diff --git a/src/vs/workbench/browser/checkbox.ts b/src/vs/workbench/browser/checkbox.ts index dcea67bf6b1..3b11fe0f2c6 100644 --- a/src/vs/workbench/browser/checkbox.ts +++ b/src/vs/workbench/browser/checkbox.ts @@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { attachToggleStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITreeItem } from 'vs/workbench/common/views'; +import { ITreeItem, ITreeItemCheckboxState } from 'vs/workbench/common/views'; export class CheckboxStateHandler extends Disposable { private readonly _onDidChangeCheckboxState = this._register(new Emitter<ITreeItem[]>()); @@ -38,23 +38,23 @@ export class TreeItemCheckbox extends Disposable { } public render(node: ITreeItem) { - if (node.checkboxChecked !== undefined) { + if (node.checkbox) { if (!this.toggle) { this.createCheckbox(node); } else { - this.toggle.checked = node.checkboxChecked; + this.toggle.checked = node.checkbox.isChecked; this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined); } } } private createCheckbox(node: ITreeItem) { - if (node.checkboxChecked !== undefined) { + if (node.checkbox) { this.toggle = new Toggle({ - isChecked: node.checkboxChecked, - title: localize('check', "Check"), - icon: node.checkboxChecked ? Codicon.check : undefined + isChecked: node.checkbox.isChecked, + title: this.createCheckboxTitle(node.checkbox), + icon: node.checkbox.isChecked ? Codicon.check : undefined }); this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass); @@ -75,14 +75,19 @@ export class TreeItemCheckbox extends Disposable { } private setCheckbox(node: ITreeItem) { - if (this.toggle && node.checkboxChecked !== undefined) { - node.checkboxChecked = this.toggle.checked; + if (this.toggle && node.checkbox) { + node.checkbox.isChecked = this.toggle.checked; this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined); - this.toggle.checked = this.toggle.checked; + this.toggle.setTitle(this.createCheckboxTitle(node.checkbox)); this.checkboxStateHandler.setCheckboxState(node); } } + private createCheckboxTitle(checkbox: ITreeItemCheckboxState) { + return checkbox.tooltip ? checkbox.tooltip : + checkbox.isChecked ? localize('checked', 'Checked') : localize('unchecked', 'Unchecked'); + } + private removeCheckbox() { const children = this.checkboxContainer.children; for (const child of children) { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 9304edeb49a..20edefb9528 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -31,6 +31,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExecuteCommandAction extends Action { @@ -483,7 +484,8 @@ export class RevertAndCloseEditorAction extends Action { constructor( id: string, label: string, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @ILogService private readonly logService: ILogService ) { super(id, label); } @@ -498,10 +500,13 @@ export class RevertAndCloseEditorAction extends Action { try { await this.editorService.revert({ editor, groupId: group.id }); } catch (error) { + this.logService.error(error); + // if that fails, since we are about to close the editor, we accept that // the editor cannot be reverted and instead do a soft revert that just // enables us to close the editor. With this, a user can always close a // dirty editor even when reverting fails. + await this.editorService.revert({ editor, groupId: group.id }, { soft: true }); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 02c836c98c8..3049500d7f8 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -52,6 +52,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isLinux, isNative, isWindows } from 'vs/base/common/platform'; +import { ILogService } from 'vs/platform/log/common/log'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -145,7 +146,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: EditorServiceImpl, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ILogService private readonly logService: ILogService ) { super(themeService); @@ -1600,10 +1602,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return editor.isDirty(); // veto if still dirty } catch (error) { + this.logService.error(error); + // if that fails, since we are about to close the editor, we accept that // the editor cannot be reverted and instead do a soft revert that just // enables us to close the editor. With this, a user can always close a // dirty editor even when reverting fails. + await editor.revert(this.id, { soft: true }); return editor.isDirty(); // veto if still dirty diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 9e3f74f3bd9..a1d39f06c68 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -71,8 +71,8 @@ class SideBySideEditorLanguageSupport implements ILanguageSupport { constructor(private primary: ILanguageSupport, private secondary: ILanguageSupport) { } - setLanguageId(languageId: string): void { - [this.primary, this.secondary].forEach(editor => editor.setLanguageId(languageId)); + setLanguageId(languageId: string, source?: string): void { + [this.primary, this.secondary].forEach(editor => editor.setLanguageId(languageId, source)); } } @@ -1237,7 +1237,7 @@ export class ChangeLanguageAction extends Action { // Change language if (typeof languageSelection !== 'undefined') { - languageSupport.setLanguageId(languageSelection.languageId); + languageSupport.setLanguageId(languageSelection.languageId, ChangeLanguageAction.ID); if (resource?.scheme === Schemas.untitled) { type SetUntitledDocumentLanguageEvent = { to: string; from: string; modelPreference: string }; diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 23cd87e04ef..9ed1f853826 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -151,6 +151,7 @@ display: block; } +.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon, .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .custom-view-tree-node-item-icon { background-size: 16px; background-position: left center; @@ -184,6 +185,7 @@ .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel::after { padding-right: 0px; + margin-right: 4px; } .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .actions { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 8ede647b896..30b8f3772cc 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -650,7 +650,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } }, expandOnlyOnTwistieClick: (e: ITreeItem) => { - return !!e.command || e.checkboxChecked !== undefined || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick'; + return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick'; }, collapseByDefault: (e: ITreeItem): boolean => { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; @@ -858,11 +858,17 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> { const tree = this.tree; - if (tree) { + if (!tree) { + return; + } + try { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; await Promise.all(itemOrItems.map(element => { return tree.expand(element, false); })); + } catch (e) { + // The extension could have changed the tree during the reveal. + // Because of that, we ignore errors. } } @@ -1228,7 +1234,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS } private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData, disposableStore: DisposableStore) { - if (node.checkboxChecked !== undefined) { + if (node.checkbox) { // The first time we find a checkbox we want to rerender the visible tree to adapt the alignment if (!this._hasCheckbox) { this._hasCheckbox = true; diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 7b05513ce94..c182aa14e4a 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -13,7 +13,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ILanguageDetectionService, LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { ThrottledDelayer } from 'vs/base/common/async'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { localize } from 'vs/nls'; @@ -78,15 +78,15 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel private _hasLanguageSetExplicitly: boolean = false; get hasLanguageSetExplicitly(): boolean { return this._hasLanguageSetExplicitly; } - setLanguageId(languageId: string): void { + setLanguageId(languageId: string, source?: string): void { // Remember that an explicit language was set this._hasLanguageSetExplicitly = true; - this.setLanguageIdInternal(languageId); + this.setLanguageIdInternal(languageId, source); } - private setLanguageIdInternal(languageId: string): void { + private setLanguageIdInternal(languageId: string, source?: string): void { if (!this.isResolved()) { return; } @@ -95,7 +95,20 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel return; } - this.modelService.setMode(this.textEditorModel, this.languageService.createById(languageId)); + this.modelService.setMode(this.textEditorModel, this.languageService.createById(languageId), source); + } + + protected installModelListeners(model: ITextModel): void { + + // Setup listener for lower level language changes + const disposable = this._register(model.onDidChangeLanguage((e) => { + if (e.source === LanguageDetectionLanguageEventSource) { + return; + } + + this._hasLanguageSetExplicitly = true; + disposable.dispose(); + })); } getLanguageId(): string | undefined { @@ -117,7 +130,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle); if (lang && !this.isDisposed()) { - this.setLanguageIdInternal(lang); + this.setLanguageIdInternal(lang, LanguageDetectionLanguageEventSource); const languageName = this.languageService.getLanguageName(lang); if (languageName) { diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index 7f30ec3e32c..b83ea8152a9 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -130,10 +130,10 @@ export class TextResourceEditorInput extends AbstractTextResourceEditorInput imp } } - setLanguageId(languageId: string): void { + setLanguageId(languageId: string, source?: string): void { this.setPreferredLanguageId(languageId); - this.cachedModel?.setLanguageId(languageId); + this.cachedModel?.setLanguageId(languageId, source); } setPreferredLanguageId(languageId: string): void { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index acdb3fc1c9e..a1f9ace2e05 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -745,6 +745,11 @@ export interface ITreeItemLabel { export type TreeCommand = Command & { originalId?: string }; +export interface ITreeItemCheckboxState { + isChecked: boolean; + tooltip?: string; +} + export interface ITreeItem { handle: string; @@ -775,7 +780,7 @@ export interface ITreeItem { accessibilityInformation?: IAccessibilityInformation; - checkboxChecked?: boolean; + checkbox?: ITreeItemCheckboxState; } export class ResolvableTreeItem implements ITreeItem { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index c7efa028427..dfcdd381ef0 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -13,6 +13,7 @@ import { IAction } from 'vs/base/common/actions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; +import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { HistoryNavigator } from 'vs/base/common/history'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -62,7 +63,7 @@ import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from 'vs/workbench import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ReplFilter, ReplFilterActionViewItem, ReplFilterState } from 'vs/workbench/contrib/debug/browser/replFilter'; import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplRawObjectsRenderer, ReplSimpleElementsRenderer, ReplVariablesRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; -import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, getStateLabel, IDebugConfiguration, IDebugService, IDebugSession, IReplElement, REPL_VIEW_ID, State } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, getStateLabel, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State } from 'vs/workbench/contrib/debug/common/debug'; import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -91,6 +92,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private history: HistoryNavigator<string>; private tree!: WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>; + private replOptions: ReplOptions; private previousTreeScrollHeight: number = 0; private replDelegate!: ReplDelegate; private container!: HTMLElement; @@ -141,6 +143,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.filterState = new ReplFilterState(this); this.filter.filterQuery = this.filterState.filterText = this.storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, ''); this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); + this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getBackgroundColor())); + this._register(this.replOptions.onDidChange(() => this.onDidStyleChange())); codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}); this.multiSessionRepl.set(this.isMultiSessionView); @@ -191,8 +195,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.treeContainer.innerText = ''; dom.clearNode(this.treeContainer); this.createReplTree(); - } else if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { - this.onDidStyleChange(); } if (e.affectsConfiguration('debug.console.acceptSuggestionOnEnter')) { const config = this.configurationService.getValue<IDebugConfiguration>('debug'); @@ -202,16 +204,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } })); - this._register(this.themeService.onDidColorThemeChange(e => { - this.onDidStyleChange(); - })); - - this._register(this.viewDescriptorService.onDidChangeLocation(e => { - if (e.views.some(v => v.id === this.id)) { - this.onDidStyleChange(); - } - })); - this._register(this.editorService.onDidActiveEditorChange(() => { this.setMode(); })); @@ -346,16 +338,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private onDidStyleChange(): void { if (this.styleElement) { - const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console; - const fontSize = debugConsole.fontSize; - const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `${debugConsole.fontFamily}`; - const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; - const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); - this.replInput.updateOptions({ - fontSize, - lineHeight: debugConsole.lineHeight, - fontFamily: debugConsole.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : debugConsole.fontFamily + fontSize: this.replOptions.replConfiguration.fontSize, + lineHeight: this.replOptions.replConfiguration.lineHeight, + fontFamily: this.replOptions.replConfiguration.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : this.replOptions.replConfiguration.fontFamily }); const replInputLineHeight = this.replInput.getOption(EditorOption.lineHeight); @@ -367,13 +353,14 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } .repl .repl-input-wrapper .monaco-editor .lines-content { - background-color: ${backgroundColor}; + background-color: ${this.replOptions.replConfiguration.backgroundColor}; } `; - this.container.style.setProperty(`--vscode-repl-font-family`, fontFamily); - this.container.style.setProperty(`--vscode-repl-font-size`, `${fontSize}px`); - this.container.style.setProperty(`--vscode-repl-font-size-for-twistie`, `${fontSize * 1.4 / 2 - 8}px`); - this.container.style.setProperty(`--vscode-repl-line-height`, lineHeight); + const cssFontFamily = this.replOptions.replConfiguration.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : this.replOptions.replConfiguration.fontFamily; + this.container.style.setProperty(`--vscode-repl-font-family`, cssFontFamily); + this.container.style.setProperty(`--vscode-repl-font-size`, `${this.replOptions.replConfiguration.fontSize}px`); + this.container.style.setProperty(`--vscode-repl-font-size-for-twistie`, `${this.replOptions.replConfiguration.fontSizeForTwistie}px`); + this.container.style.setProperty(`--vscode-repl-line-height`, this.replOptions.replConfiguration.cssLineHeight); this.tree.rerender(); @@ -566,7 +553,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } private createReplTree(): void { - this.replDelegate = new ReplDelegate(this.configurationService); + this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions); const wordWrap = this.configurationService.getValue<IDebugConfiguration>('debug').console.wordWrap; this.treeContainer.classList.toggle('word-wrap', wordWrap); const linkDetector = this.instantiationService.createInstance(LinkDetector); @@ -601,7 +588,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this._register(this.tree.onDidChangeContentHeight(() => { if (this.tree.scrollHeight !== this.previousTreeScrollHeight) { - const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight; + // Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight. + // Consider the tree to be scrolled all the way down if it is within 2px of the bottom. + const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2; if (lastElementWasVisible) { setTimeout(() => { // Can't set scrollTop during this event listener, the list might overwrite the change @@ -750,6 +739,54 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } } +class ReplOptions extends Disposable implements IReplOptions { + private static readonly lineHeightEm = 1.4; + + private readonly _onDidChange = this._register(new Emitter<void>()); + readonly onDidChange = this._onDidChange.event; + + private _replConfig!: IReplConfiguration; + public get replConfiguration(): IReplConfiguration { + return this._replConfig; + } + + constructor( + viewId: string, + private readonly backgroundColorDelegate: () => string, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService + ) { + super(); + + this._register(this.themeService.onDidColorThemeChange(e => this.update())); + this._register(this.viewDescriptorService.onDidChangeLocation(e => { + if (e.views.some(v => v.id === viewId)) { + this.update(); + } + })); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { + this.update(); + } + })); + this.update(); + } + + private update() { + const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console; + this._replConfig = { + fontSize: debugConsole.fontSize, + fontFamily: debugConsole.fontFamily, + lineHeight: debugConsole.lineHeight ? debugConsole.lineHeight : ReplOptions.lineHeightEm * debugConsole.fontSize, + cssLineHeight: debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : `${ReplOptions.lineHeightEm}em`, + backgroundColor: this.themeService.getColorTheme().getColor(this.backgroundColorDelegate()), + fontSizeForTwistie: debugConsole.fontSize * ReplOptions.lineHeightEm / 2 - 8 + }; + this._onDidChange.fire(); + } +} + // Repl actions and commands class AcceptReplInputAction extends EditorAction { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index b714e1e1181..94b7ecd5b13 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -22,7 +22,7 @@ import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { debugConsoleEvaluationInput } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, IReplElement, IReplElementSource } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, IReplElement, IReplElementSource, IReplOptions } from 'vs/workbench/contrib/debug/common/debug'; import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, SimpleReplElement } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -295,7 +295,10 @@ function isNestedVariable(element: IReplElement) { export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> { - constructor(private configurationService: IConfigurationService) { + constructor( + private readonly configurationService: IConfigurationService, + private readonly replOptions: IReplOptions + ) { super(); } @@ -310,8 +313,7 @@ export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> { } protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { - const config = this.configurationService.getValue<IDebugConfiguration>('debug'); - const rowHeight = Math.ceil(1.3 * config.console.fontSize); + const lineHeight = this.replOptions.replConfiguration.lineHeight; const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; @@ -321,10 +323,10 @@ export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> { const value = element.value; const valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 70)); - return valueRows * rowHeight; + return valueRows * lineHeight; } - return rowHeight; + return lineHeight; } getTemplateId(element: IReplElement): string { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index ec6bb1652ae..dbd8289acf0 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -6,6 +6,7 @@ import { IAction } from 'vs/base/common/actions'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -1142,3 +1143,16 @@ export interface IBreakpointEditorContribution extends editorCommon.IEditorContr closeBreakpointWidget(): void; getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[]; } + +export interface IReplConfiguration { + readonly fontSize: number; + readonly fontFamily: string; + readonly lineHeight: number; + readonly cssLineHeight: string; + readonly backgroundColor: Color | undefined; + readonly fontSizeForTwistie: number; +} + +export interface IReplOptions { + readonly replConfiguration: IReplConfiguration; +} diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index e557f402a9a..9136ed15e08 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -89,7 +89,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes throw new Error('Please sign in to store your edit session.'); } - return this.storeClient!.writeResource('editSessions', JSON.stringify(editSession), null, createSyncHeaders(generateUuid())); + return this.storeClient!.writeResource('editSessions', JSON.stringify(editSession), null, undefined, createSyncHeaders(generateUuid())); } /** @@ -108,9 +108,9 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes const headers = createSyncHeaders(generateUuid()); try { if (ref !== undefined) { - content = await this.storeClient?.resolveResourceContent('editSessions', ref, headers); + content = await this.storeClient?.resolveResourceContent('editSessions', ref, undefined, headers); } else { - const result = await this.storeClient?.readResource('editSessions', null, headers); + const result = await this.storeClient?.readResource('editSessions', null, undefined, headers); content = result?.content; ref = result?.ref; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 604aabbbbb8..6f0c73a98c1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -930,15 +930,23 @@ export class ExtensionEditor extends EditorPane { if (gallery) { append(moreInfo, $('.more-info-entry', undefined, - $('div', undefined, localize('release date', "Released on")), + $('div', undefined, localize('published', "Published")), $('div', undefined, new Date(gallery.releaseDate).toLocaleString(locale, { hourCycle: 'h23' })) ), $('.more-info-entry', undefined, - $('div', undefined, localize('last updated', "Last updated")), + $('div', undefined, localize('last released', "Last released")), $('div', undefined, new Date(gallery.lastUpdated).toLocaleString(locale, { hourCycle: 'h23' })) ) ); } + if (extension.local && extension.local.installedTimestamp) { + append(moreInfo, + $('.more-info-entry', undefined, + $('div', undefined, localize('last updated', "Last updated")), + $('div', undefined, new Date(extension.local.installedTimestamp).toLocaleString(locale, { hourCycle: 'h23' })) + ) + ); + } append(moreInfo, $('.more-info-entry', undefined, $('div', undefined, localize('id', "Identifier")), diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 6e15de5fffd..0352dcda3f8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,11 +14,11 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi 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, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; -import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; +import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; @@ -941,7 +941,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@sort:publishedDate ')) + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recentlyPublished ')) }); const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); @@ -985,6 +985,23 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); this.registerExtensionAction({ + id: 'workbench.extensions.action.recentlyUpdatedExtensions', + title: { value: localize('recentlyUpdatedExtensions', "Show Recently Updated Extensions"), original: 'Show Recently Updated Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 7, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('recently updated filter', "Recently Updated") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recentlyUpdated')) + }); + + this.registerExtensionAction({ id: LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, title: { value: localize('showWorkspaceUnsupportedExtensions', "Show Extensions Unsupported By Workspace"), original: 'Show Extensions Unsupported By Workspace' }, category: ExtensionsLocalizedLabel, @@ -1080,24 +1097,25 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi MenuRegistry.appendMenuItem(extensionsFilterSubMenu, <ISubmenuItem>{ submenu: extensionsSortSubMenu, title: localize('sorty by', "Sort By"), - when: CONTEXT_HAS_GALLERY, + when: ContextKeyExpr.and(ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext)), group: '4_sort', order: 1, }); [ - { id: 'installs', title: localize('sort by installs', "Install Count") }, - { id: 'rating', title: localize('sort by rating', "Rating") }, - { id: 'name', title: localize('sort by name', "Name") }, - { id: 'publishedDate', title: localize('sort by date', "Published Date") }, - ].map(({ id, title }, index) => { + { id: 'installs', title: localize('sort by installs', "Install Count"), precondition: BuiltInExtensionsContext.negate() }, + { id: 'rating', title: localize('sort by rating', "Rating"), precondition: BuiltInExtensionsContext.negate() }, + { id: 'name', title: localize('sort by name', "Name"), precondition: BuiltInExtensionsContext.negate() }, + { id: 'publishedDate', title: localize('sort by published date', "Published Date"), precondition: BuiltInExtensionsContext.negate() }, + { id: 'updateDate', title: localize('sort by update date', "Updated Date"), precondition: ContextKeyExpr.and(SearchMarketplaceExtensionsContext.negate(), RecommendedExtensionsContext.negate(), BuiltInExtensionsContext.negate()) }, + ].map(({ id, title, precondition }, index) => { this.registerExtensionAction({ id: `extensions.sort.${id}`, title, - precondition: DefaultViewsContext.toNegated(), + precondition: precondition, menu: [{ id: extensionsSortSubMenu, - when: CONTEXT_HAS_GALLERY, + when: ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext), order: index, }], toggled: ExtensionsSortByContext.isEqualTo(id), @@ -1117,7 +1135,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi category: ExtensionsLocalizedLabel, icon: clearSearchResultsIcon, f1: true, - precondition: DefaultViewsContext.toNegated(), + precondition: SearchHasTextContext, menu: { id: MenuId.ViewContainerTitle, when: ContextKeyExpr.equals('viewContainer', VIEWLET_ID), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 27a701f84cb..50c81efc666 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -16,7 +16,7 @@ import { append, $, Dimension, hide, show, DragAndDropObserver } from 'vs/base/b import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey } from '../common/extensions'; import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -61,17 +61,22 @@ import { extractEditorsAndFilesDropData } from 'vs/platform/dnd/browser/dnd'; import { extname } from 'vs/base/common/resources'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false); -const SearchIntalledExtensionsContext = new RawContextKey<boolean>('searchInstalledExtensions', false); +export const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true); +export const ExtensionsSortByContext = new RawContextKey<string>('extensionsSortByValue', ''); +export const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false); +export const SearchHasTextContext = new RawContextKey<boolean>('extensionSearchHasText', false); +const SearchInstalledExtensionsContext = new RawContextKey<boolean>('searchInstalledExtensions', false); +const SearchRecentlyUpdatedExtensionsContext = new RawContextKey<boolean>('searchRecentlyUpdatedExtensions', false); const SearchOutdatedExtensionsContext = new RawContextKey<boolean>('searchOutdatedExtensions', false); const SearchEnabledExtensionsContext = new RawContextKey<boolean>('searchEnabledExtensions', false); const SearchDisabledExtensionsContext = new RawContextKey<boolean>('searchDisabledExtensions', false); const HasInstalledExtensionsContext = new RawContextKey<boolean>('hasInstalledExtensions', true); -const BuiltInExtensionsContext = new RawContextKey<boolean>('builtInExtensions', false); +export const BuiltInExtensionsContext = new RawContextKey<boolean>('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey<boolean>('searchBuiltInExtensions', false); const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey<boolean>('searchUnsupportedWorkspaceExtensions', false); const SearchDeprecatedExtensionsContext = new RawContextKey<boolean>('searchDeprecatedExtensions', false); -const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false); +export const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false); +const SortByUpdateDateContext = new RawContextKey<boolean>('sortByUpdateDate', false); export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -221,7 +226,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'extensions.recommendedList', name: localize('recommendedExtensions', "Recommended"), ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{ flexibleHeight: true }]), - when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), + when: ContextKeyExpr.and(DefaultViewsContext, SortByUpdateDateContext.negate(), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, order: 3, canToggleVisibility: true @@ -288,6 +293,16 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }); /* + * View used for searching recently updated extensions + */ + viewDescriptors.push({ + id: 'workbench.views.extensions.searchRecentlyUpdated', + name: localize('recently updated', "Recently Updated"), + ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchRecentlyUpdatedExtensions')), + }); + + /* * View used for searching enabled extensions */ viewDescriptors.push({ @@ -443,7 +458,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private defaultViewsContextKey: IContextKey<boolean>; private sortByContextKey: IContextKey<string>; private searchMarketplaceExtensionsContextKey: IContextKey<boolean>; + private searchHasTextContextKey: IContextKey<boolean>; + private sortByUpdateDateContextKey: IContextKey<boolean>; private searchInstalledExtensionsContextKey: IContextKey<boolean>; + private searchRecentlyUpdatedExtensionsContextKey: IContextKey<boolean>; private searchOutdatedExtensionsContextKey: IContextKey<boolean>; private searchEnabledExtensionsContextKey: IContextKey<boolean>; private searchDisabledExtensionsContextKey: IContextKey<boolean>; @@ -486,7 +504,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); - this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); + this.searchHasTextContextKey = SearchHasTextContext.bindTo(contextKeyService); + this.sortByUpdateDateContextKey = SortByUpdateDateContext.bindTo(contextKeyService); + this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); + this.searchRecentlyUpdatedExtensionsContextKey = SearchRecentlyUpdatedExtensionsContext.bindTo(contextKeyService); this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); this.searchDeprecatedExtensionsContextKey = SearchDeprecatedExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); @@ -633,7 +654,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE .replace(/@tag:/g, 'tag:') .replace(/@ext:/g, 'ext:') .replace(/@featured/g, 'featured') - .replace(/@popular/g, this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '@sort:installs') + .replace(/@popular/g, this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '@popular') : ''; } @@ -651,7 +672,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE const value = this.normalizedQuery(); this.contextKeyService.bufferChangeEvents(() => { const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value); + this.searchHasTextContextKey.set(value.trim() !== ''); this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value)); + this.searchRecentlyUpdatedExtensionsContextKey.set(ExtensionsListView.isSearchRecentlyUpdatedQuery(value)); this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value)); this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); @@ -661,7 +684,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); - this.defaultViewsContextKey.set(!value); + this.sortByUpdateDateContextKey.set(ExtensionsListView.isSortUpdateDateQuery(value)); + this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value)); }); return this.progress(Promise.all(this.panes.map(view => diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 1ed384b4e22..2e0cfa1e40c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { isCancellationError, getErrorMessage } from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -90,8 +90,23 @@ interface IQueryResult { readonly disposables: DisposableStore; } +const enum LocalSortBy { + UpdateDate = 'UpdateDate', +} + +function isLocalSortBy(value: any): value is LocalSortBy { + switch (value as LocalSortBy) { + case LocalSortBy.UpdateDate: return true; + } +} + +type SortBy = LocalSortBy | GallerySortBy; +type IQueryOptions = Omit<IGalleryQueryOptions, 'sortBy'> & { sortBy?: SortBy }; + export class ExtensionsListView extends ViewPane { + private static RECENT_UPDATE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days + private bodyTemplate: { messageContainer: HTMLElement; messageSeverityIcon: HTMLElement; @@ -238,10 +253,11 @@ export class ExtensionsListView extends ViewPane { }; switch (parsedQuery.sortBy) { - case 'installs': options.sortBy = SortBy.InstallCount; break; - case 'rating': options.sortBy = SortBy.WeightedRating; break; - case 'name': options.sortBy = SortBy.Title; break; - case 'publishedDate': options.sortBy = SortBy.PublishedDate; break; + case 'installs': options.sortBy = GallerySortBy.InstallCount; break; + case 'rating': options.sortBy = GallerySortBy.WeightedRating; break; + case 'name': options.sortBy = GallerySortBy.Title; break; + case 'publishedDate': options.sortBy = GallerySortBy.PublishedDate; break; + case 'updateDate': options.sortBy = LocalSortBy.UpdateDate; break; } const request = createCancelablePromise(async token => { @@ -324,11 +340,21 @@ export class ExtensionsListView extends ViewPane { return { model, disposables: new DisposableStore() }; } - if (ExtensionsListView.isLocalExtensionsQuery(query.value)) { + if (ExtensionsListView.isLocalExtensionsQuery(query.value, query.sortBy)) { return this.queryLocal(query, options); } - const model = await this.queryGallery(query, options, token); + if (ExtensionsListView.isSearchPopularQuery(query.value)) { + query.value = query.value.replace('@popular', ''); + options.sortBy = !options.sortBy ? GallerySortBy.InstallCount : options.sortBy; + } + else if (ExtensionsListView.isSearchRecentlyPublishedQuery(query.value)) { + query.value = query.value.replace('@recentlyPublished', ''); + options.sortBy = !options.sortBy ? GallerySortBy.PublishedDate : options.sortBy; + } + + const galleryQueryOptions: IGalleryQueryOptions = { ...options, sortBy: isLocalSortBy(options.sortBy) ? undefined : options.sortBy }; + const model = await this.queryGallery(query, galleryQueryOptions, token); return { model, disposables: new DisposableStore() }; } @@ -415,6 +441,10 @@ export class ExtensionsListView extends ViewPane { extensions = await this.filterDeprecatedExtensions(local, query, options); } + else if (/@recentlyUpdated/i.test(query.value)) { + extensions = this.filterRecentlyUpdatedExtensions(local, query, options); + } + return { extensions, canIncludeInstalledExtensions }; } @@ -633,6 +663,22 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(local, options); } + private filterRecentlyUpdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { + let { value, categories } = this.parseCategories(query.value); + const currentTime = Date.now(); + local = local.filter(e => !e.isBuiltin && e.local?.installedTimestamp !== undefined && currentTime - e.local.installedTimestamp < ExtensionsListView.RECENT_UPDATE_DURATION); + + value = value.replace(/@recentlyUpdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); + + const result = local.filter(e => + (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1) && + (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category)))); + + options.sortBy = options.sortBy ?? LocalSortBy.UpdateDate; + + return this.sortExtensions(result, options); + } + private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined { const oldExtensions = [...extensions]; const findPreviousExtensionIndex = (from: number): number => { @@ -659,10 +705,10 @@ export class ExtensionsListView extends ViewPane { return hasChanged ? extensions : undefined; } - private async queryGallery(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> { + private async queryGallery(query: Query, options: IGalleryQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> { const hasUserDefinedSortOrder = options.sortBy !== undefined; if (!hasUserDefinedSortOrder && !query.value.trim()) { - options.sortBy = SortBy.InstallCount; + options.sortBy = GallerySortBy.InstallCount; } if (this.isRecommendationsQuery(query)) { @@ -733,11 +779,17 @@ export class ExtensionsListView extends ViewPane { private sortExtensions(extensions: IExtension[], options: IQueryOptions): IExtension[] { switch (options.sortBy) { - case SortBy.InstallCount: + case GallerySortBy.InstallCount: extensions = extensions.sort((e1, e2) => typeof e2.installCount === 'number' && typeof e1.installCount === 'number' ? e2.installCount - e1.installCount : NaN); break; - case SortBy.AverageRating: - case SortBy.WeightedRating: + case LocalSortBy.UpdateDate: + extensions = extensions.sort((e1, e2) => + typeof e2.local?.installedTimestamp === 'number' && typeof e1.local?.installedTimestamp === 'number' ? e2.local.installedTimestamp - e1.local.installedTimestamp : + typeof e2.local?.installedTimestamp === 'number' ? 1 : + typeof e1.local?.installedTimestamp === 'number' ? -1 : NaN); + break; + case GallerySortBy.AverageRating: + case GallerySortBy.WeightedRating: extensions = extensions.sort((e1, e2) => typeof e2.rating === 'number' && typeof e1.rating === 'number' ? e2.rating - e1.rating : NaN); break; default: @@ -1018,7 +1070,7 @@ export class ExtensionsListView extends ViewPane { this.list = null; } - static isLocalExtensionsQuery(query: string): boolean { + static isLocalExtensionsQuery(query: string, sortBy?: string): boolean { return this.isInstalledExtensionsQuery(query) || this.isOutdatedExtensionsQuery(query) || this.isEnabledExtensionsQuery(query) @@ -1027,7 +1079,9 @@ export class ExtensionsListView extends ViewPane { || this.isSearchBuiltInExtensionsQuery(query) || this.isBuiltInGroupExtensionsQuery(query) || this.isSearchDeprecatedExtensionsQuery(query) - || this.isSearchWorkspaceUnsupportedExtensionsQuery(query); + || this.isSearchWorkspaceUnsupportedExtensionsQuery(query) + || this.isSearchRecentlyUpdatedQuery(query) + || this.isSortInstalledExtensionsQuery(query, sortBy); } static isSearchBuiltInExtensionsQuery(query: string): boolean { @@ -1090,6 +1144,26 @@ export class ExtensionsListView extends ViewPane { return /@recommended:languages/i.test(query); } + static isSortInstalledExtensionsQuery(query: string, sortBy?: string): boolean { + return (sortBy !== undefined && sortBy !== '' && query === '') || (!sortBy && /^@sort:\S*$/i.test(query)); + } + + static isSearchPopularQuery(query: string): boolean { + return /@popular/i.test(query); + } + + static isSearchRecentlyPublishedQuery(query: string): boolean { + return /@recentlyPublished/i.test(query); + } + + static isSearchRecentlyUpdatedQuery(query: string): boolean { + return /@recentlyUpdated/i.test(query); + } + + static isSortUpdateDateQuery(query: string): boolean { + return /@sort:updateDate/i.test(query); + } + override focus(): void { super.focus(); if (!this.list) { @@ -1116,7 +1190,7 @@ export class ServerInstalledExtensionsView extends ExtensionsListView { override async show(query: string): Promise<IPagedModel<IExtension>> { query = query ? query : '@installed'; - if (!ExtensionsListView.isLocalExtensionsQuery(query)) { + if (!ExtensionsListView.isLocalExtensionsQuery(query) || ExtensionsListView.isSortInstalledExtensionsQuery(query)) { query = query += ' @installed'; } return super.show(query.trim()); @@ -1128,7 +1202,8 @@ export class EnabledExtensionsView extends ExtensionsListView { override async show(query: string): Promise<IPagedModel<IExtension>> { query = query || '@enabled'; - return ExtensionsListView.isEnabledExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + return ExtensionsListView.isEnabledExtensionsQuery(query) ? super.show(query) : + ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@enabled ' + query) : this.showEmptyModel(); } } @@ -1136,7 +1211,8 @@ export class DisabledExtensionsView extends ExtensionsListView { override async show(query: string): Promise<IPagedModel<IExtension>> { query = query || '@disabled'; - return ExtensionsListView.isDisabledExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + return ExtensionsListView.isDisabledExtensionsQuery(query) ? super.show(query) : + ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@disabled ' + query) : this.showEmptyModel(); } } diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index 7c37b541e8a..d921466976b 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -13,9 +13,9 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'workspaceUnsupported', 'deprecated', 'sort', 'category', 'tag', 'ext', 'id'] as const; + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'recentlyUpdated', 'recentlyPublished', 'workspaceUnsupported', 'deprecated', 'sort', 'category', 'tag', 'ext', 'id'] as const; const subcommands = { - 'sort': ['installs', 'rating', 'name', 'publishedDate'], + 'sort': ['installs', 'rating', 'name', 'publishedDate', 'updateDate'], 'category': EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`), 'tag': [''], 'ext': [''], diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index ca1a3a5ff3f..32c2becd92f 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -187,8 +187,6 @@ export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.comm export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.extensions.action.listWorkspaceUnsupportedExtensions'; // Context Keys -export const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true); -export const ExtensionsSortByContext = new RawContextKey<string>('extensionsSortByValue', ''); export const HasOutdatedExtensionsContext = new RawContextKey<boolean>('hasOutdatedExtensions', false); // Context Menu Groups diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 9d665e5b934..6e14b1bccec 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -11,7 +11,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - DidUninstallExtensionEvent, InstallExtensionEvent, SortBy, InstallExtensionResult, getTargetPlatform, IExtensionInfo, UninstallExtensionEvent + DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, getTargetPlatform, IExtensionInfo, UninstallExtensionEvent, SortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService, ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; @@ -57,13 +57,13 @@ suite('ExtensionsListView Tests', () => { uninstallEvent: Emitter<UninstallExtensionEvent>, didUninstallEvent: Emitter<DidUninstallExtensionEvent>; - const localEnabledTheme = aLocalExtension('first-enabled-extension', { categories: ['Themes', 'random'] }); - const localEnabledLanguage = aLocalExtension('second-enabled-extension', { categories: ['Programming languages'] }); - const localDisabledTheme = aLocalExtension('first-disabled-extension', { categories: ['themes'] }); - const localDisabledLanguage = aLocalExtension('second-disabled-extension', { categories: ['programming languages'] }); - const localRandom = aLocalExtension('random-enabled-extension', { categories: ['random'] }); - const builtInTheme = aLocalExtension('my-theme', { contributes: { themes: ['my-theme'] } }, { type: ExtensionType.System }); - const builtInBasic = aLocalExtension('my-lang', { contributes: { grammars: [{ language: 'my-language' }] } }, { type: ExtensionType.System }); + const localEnabledTheme = aLocalExtension('first-enabled-extension', { categories: ['Themes', 'random'] }, { installedTimestamp: 123456 }); + const localEnabledLanguage = aLocalExtension('second-enabled-extension', { categories: ['Programming languages'] }, { installedTimestamp: Date.now() }); + const localDisabledTheme = aLocalExtension('first-disabled-extension', { categories: ['themes'] }, { installedTimestamp: 234567 }); + const localDisabledLanguage = aLocalExtension('second-disabled-extension', { categories: ['programming languages'] }, { installedTimestamp: Date.now() - 50000 }); + const localRandom = aLocalExtension('random-enabled-extension', { categories: ['random'] }, { installedTimestamp: 345678 }); + const builtInTheme = aLocalExtension('my-theme', { contributes: { themes: ['my-theme'] } }, { type: ExtensionType.System, installedTimestamp: 222 }); + const builtInBasic = aLocalExtension('my-lang', { contributes: { grammars: [{ language: 'my-language' }] } }, { type: ExtensionType.System, installedTimestamp: 666666 }); const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A'); const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B'); @@ -206,6 +206,8 @@ suite('ExtensionsListView Tests', () => { assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@enabled'), true); assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@disabled'), true); assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@outdated'), true); + assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@sort:name'), true); + assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@sort:updateDate'), true); assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@installed searchText'), true); assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@enabled searchText'), true); assert.strictEqual(ExtensionsListView.isLocalExtensionsQuery('@disabled searchText'), true); @@ -347,6 +349,26 @@ suite('ExtensionsListView Tests', () => { }); }); + test('Test local query with sorting order', async () => { + await testableView.show('@recentlyUpdated').then(result => { + assert.strictEqual(result.length, 2, 'Unexpected number of results for @recentlyUpdated'); + const actual = [result.get(0).name, result.get(1).name]; + const expected = [localEnabledLanguage.manifest.name, localDisabledLanguage.manifest.name]; + for (let i = 0; i < actual.length; i++) { + assert.strictEqual(actual[i], expected[i], 'Unexpected default sort order of extensions for @recentlyUpdate query'); + } + }); + + await testableView.show('@installed @sort:updateDate').then(result => { + assert.strictEqual(result.length, 5, 'Unexpected number of results for @sort:updateDate. Expected all localy installed Extension which are not builtin'); + const actual = [result.get(0).local?.installedTimestamp, result.get(1).local?.installedTimestamp, result.get(2).local?.installedTimestamp, result.get(3).local?.installedTimestamp, result.get(4).local?.installedTimestamp]; + const expected = [localEnabledLanguage.installedTimestamp, localDisabledLanguage.installedTimestamp, localRandom.installedTimestamp, localDisabledTheme.installedTimestamp, localEnabledTheme.installedTimestamp]; + for (let i = 0; i < result.length; i++) { + assert.strictEqual(actual[i], expected[i], 'Unexpected extension sorting for @sort:updateDate query.'); + } + }); + }); + test('Test @recommended:workspace query', () => { const workspaceRecommendedExtensions = [ workspaceRecommendationA, diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 016bacbe060..bfbc9108188 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -246,10 +246,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return this.preferredLanguageId; } - setLanguageId(languageId: string): void { + setLanguageId(languageId: string, source?: string): void { this.setPreferredLanguageId(languageId); - this.model?.setLanguageId(languageId); + this.model?.setLanguageId(languageId, source); } setPreferredLanguageId(languageId: string): void { diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index e4d7aa3d2ac..e052feebca7 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -5,6 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { parse } from 'vs/base/common/marshalling'; @@ -136,14 +137,6 @@ export class InteractiveDocumentContribution extends Disposable implements IWork transientOptions: contentOptions }; }, - save: async (uri: URI) => { - // trigger backup always - return false; - }, - saveAs: async (uri: URI, target: URI, token: CancellationToken) => { - // return this._proxy.$saveNotebookAs(viewType, uri, target, token); - return false; - }, backup: async (uri: URI, token: CancellationToken) => { const doc = notebookService.listNotebookDocuments().find(document => document.uri.toString() === uri.toString()); if (doc) { @@ -728,6 +721,21 @@ registerAction2(class extends Action2 { if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { editorService.activeEditorPane?.focus(); } + else { + // find and open the most recent interactive window + const openEditors = editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + const interactiveWindow = Iterable.find(openEditors, identifier => { return identifier.editor.typeId === InteractiveEditorInput.ID; }); + if (interactiveWindow) { + const editorInput = interactiveWindow.editor as InteractiveEditorInput; + const currentGroup = interactiveWindow.groupId; + const editor = await editorService.openEditor(editorInput, currentGroup); + const editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + editorService.activeEditorPane?.focus(); + } + } + } } }); diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts index 30b4d7f15ab..d5605600b90 100644 --- a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -11,7 +11,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { ILanguageDetectionService, LanguageDetectionHintConfig } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ILanguageDetectionService, LanguageDetectionHintConfig, LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { ThrottledDelayer } from 'vs/base/common/async'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -139,7 +139,7 @@ registerAction2(class extends Action2 { if (editorUri) { const lang = await languageDetectionService.detectLanguage(editorUri); if (lang) { - editor.getModel()?.setMode(lang); + editor.getModel()?.setMode(lang, LanguageDetectionLanguageEventSource); } else { notificationService.warn(localize('noDetection', "Unable to detect editor language")); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 8fd7ea76eb1..dfafb936be0 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -194,8 +194,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements return Boolean(this._outTextModel?.isDirty()); } - setLanguageId(languageId: string, _setExplicitly?: boolean): void { - this._model?.setLanguageId(languageId); + setLanguageId(languageId: string, source?: string): void { + this._model?.setLanguageId(languageId, source); } // implement get/set languageId diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index a7dedab28a2..357ffdfa1fb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -400,12 +400,12 @@ export class MergeEditorModel extends EditorModel { public readonly hasUnhandledConflicts = this.unhandledConflictsCount.map(value => /** @description hasUnhandledConflicts */ value > 0); - public setLanguageId(languageId: string): void { + public setLanguageId(languageId: string, source?: string): void { const language = this.languageService.createById(languageId); - this.modelService.setMode(this.base, language); - this.modelService.setMode(this.input1.textModel, language); - this.modelService.setMode(this.input2.textModel, language); - this.modelService.setMode(this.resultTextModel, language); + this.modelService.setMode(this.base, language, source); + this.modelService.setMode(this.input1.textModel, language, source); + this.modelService.setMode(this.input2.textModel, language, source); + this.modelService.setMode(this.resultTextModel, language, source); } public getInitialResultValue(): string { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 70d6930241c..eefafb7edab 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -5,7 +5,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { UriComponents } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -27,6 +27,7 @@ import { Schemas } from 'vs/base/common/network'; const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; const CANCEL_NOTEBOOK_COMMAND_ID = 'notebook.cancelExecution'; +const INTERRUPT_NOTEBOOK_COMMAND_ID = 'notebook.interruptExecution'; const CANCEL_CELL_COMMAND_ID = 'notebook.cell.cancelExecution'; const EXECUTE_CELL_FOCUS_CONTAINER_COMMAND_ID = 'notebook.cell.executeAndFocusContainer'; const EXECUTE_CELL_SELECT_BELOW = 'notebook.cell.executeAndSelectBelow'; @@ -488,22 +489,61 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { } }); -registerAction2(class CancelNotebook extends NotebookAction { +class CancelNotebook extends NotebookAction { + override getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + return getContextFromUri(accessor, context) ?? getContextFromActiveEditor(accessor.get(IEditorService)); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise<void> { + return context.notebookEditor.cancelNotebookCells(); + } +} + +registerAction2(class CancelAllNotebook extends CancelNotebook { constructor() { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, - title: localize('notebookActions.cancelNotebook', "Stop Execution"), + title: { + value: localize('notebookActions.cancelNotebook', "Stop Execution"), + original: 'Stop Execution' + }, icon: icons.stopIcon, - description: { - description: localize('notebookActions.cancelNotebook', "Stop Execution"), - args: [ - { - name: 'uri', - description: 'The document uri', - constraint: URI - } - ] + menu: [ + { + id: MenuId.EditorTitle, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), + ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) + ) + }, + { + id: MenuId.NotebookToolbar, + order: -1, + group: 'navigation/execute', + when: ContextKeyExpr.and( + NOTEBOOK_HAS_RUNNING_CELL, + NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), + ContextKeyExpr.equals('config.notebook.globalToolbar', true) + ) + } + ] + }); + } +}); + +registerAction2(class InterruptNotebook extends CancelNotebook { + constructor() { + super({ + id: INTERRUPT_NOTEBOOK_COMMAND_ID, + title: { + value: localize('notebookActions.interruptNotebook', "Interrupt"), + original: 'Interrupt' }, + icon: icons.stopIcon, menu: [ { id: MenuId.EditorTitle, @@ -529,14 +569,6 @@ registerAction2(class CancelNotebook extends NotebookAction { ] }); } - - override getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { - return getContextFromUri(accessor, context) ?? getContextFromActiveEditor(accessor.get(IEditorService)); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise<void> { - return context.notebookEditor.cancelNotebookCells(); - } }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 69e2a7e41ff..583ba78776d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,7 +37,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookRendererInfo, isTextStreamMime, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -46,6 +46,7 @@ import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/w import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, RendererMetadata, ToWebviewMessage } from './webviewMessages'; +import { compressOutputItemStreams } from 'vs/workbench/contrib/notebook/browser/view/renderers/stdOutErrorPreProcessor'; export interface ICachedInset<K extends ICommonCellInfo> { outputId: string; @@ -1278,12 +1279,14 @@ var requirejs = (function() { let updatedContent: ICreationContent | undefined = undefined; if (content.type === RenderOutputType.Extension) { const output = content.source.model; - const first = output.outputs.find(op => op.mime === content.mimeType)!; + const firstBuffer = isTextStreamMime(content.mimeType) ? + compressOutputItemStreams(content.mimeType, output.outputs) : + output.outputs.find(op => op.mime === content.mimeType)!.data.buffer; updatedContent = { type: RenderOutputType.Extension, outputId: outputCache.outputId, - mimeType: first.mime, - valueBytes: first.data.buffer, + mimeType: content.mimeType, + valueBytes: firstBuffer, metadata: output.metadata, }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/stdOutErrorPreProcessor.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/stdOutErrorPreProcessor.ts new file mode 100644 index 00000000000..7f20c6316fc --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/stdOutErrorPreProcessor.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import type { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + + +/** + * Given a stream of individual stdout outputs, this function will return the compressed lines, escaping some of the common terminal escape codes. + * E.g. some terminal escape codes would result in the previous line getting cleared, such if we had 3 lines and + * last line contained such a code, then the result string would be just the first two lines. + */ +export function compressOutputItemStreams(mimeType: string, outputs: IOutputItemDto[]) { + const buffers: Uint8Array[] = []; + let startAppending = false; + + // Pick the first set of outputs with the same mime type. + for (const output of outputs) { + if (output.mime === mimeType) { + if ((buffers.length === 0 || startAppending)) { + buffers.push(output.data.buffer); + startAppending = true; + } + } else if (startAppending) { + startAppending = false; + } + } + compressStreamBuffer(buffers); + return VSBuffer.concat(buffers.map(buffer => VSBuffer.wrap(buffer))).buffer; +} +const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`; +const MOVE_CURSOR_1_LINE_COMMAND_BYTES = MOVE_CURSOR_1_LINE_COMMAND.split('').map(c => c.charCodeAt(0)); +const LINE_FEED = 10; +function compressStreamBuffer(streams: Uint8Array[]) { + streams.forEach((stream, index) => { + if (index === 0 || stream.length < MOVE_CURSOR_1_LINE_COMMAND.length) { + return; + } + + const previousStream = streams[index - 1]; + + // Remove the previous line if required. + const command = stream.subarray(0, MOVE_CURSOR_1_LINE_COMMAND.length); + if (command[0] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[0] && command[1] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[1] && command[2] === MOVE_CURSOR_1_LINE_COMMAND_BYTES[2]) { + const lastIndexOfLineFeed = previousStream.lastIndexOf(LINE_FEED); + if (lastIndexOfLineFeed === -1) { + return; + } + streams[index - 1] = previousStream.subarray(0, lastIndexOfLineFeed); + streams[index] = stream.subarray(MOVE_CURSOR_1_LINE_COMMAND.length); + } + }); + return streams; +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index 294f43e558b..35506ece47f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -30,6 +30,7 @@ export class NotebookEditorContextKeys { private readonly _disposables = new DisposableStore(); private readonly _viewModelDisposables = new DisposableStore(); private readonly _cellOutputsListeners: IDisposable[] = []; + private readonly _selectedKernelDisposables = new DisposableStore(); constructor( private readonly _editor: INotebookEditorDelegate, @@ -174,6 +175,13 @@ export class NotebookEditorContextKeys { this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); this._notebookKernelSelected.set(Boolean(selected)); this._notebookKernel.set(selected?.id ?? ''); + + this._selectedKernelDisposables.clear(); + if (selected) { + this._selectedKernelDisposables.add(selected.onDidChange(() => { + this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + })); + } } private _updateForNotebookOptions(): void { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 361c0baa2b1..af7f455c8af 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -947,3 +947,11 @@ export interface NotebookExtensionDescription { readonly id: ExtensionIdentifier; readonly location: UriComponents | undefined; } + +/** + * Whether the provided mime type is a text streamn like `stdout`, `stderr`. + */ +export function isTextStreamMime(mimeType: string) { + return ['application/vnd.code.notebook.stdout', 'application/x.notebook.stdout', 'application/x.notebook.stream', 'application/vnd.code.notebook.stderr', 'application/x.notebook.stderr'].includes(mimeType); +} + diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 7cd345036a7..362123693b2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -377,7 +377,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook if (!this.isResolved()) { return; } - const success = await this._contentProvider.save(this.notebook.uri, CancellationToken.None); + const success = false; this._logService.debug(`[notebook editor model] save(${versionId}) - document saved saved, start updating file stats`, this.resource.toString(true), success); this._lastResolvedFileStat = await this._resolveStats(this.resource); if (success) { @@ -407,7 +407,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook return undefined; } - const success = await this._contentProvider.saveAs(this.notebook.uri, targetResource, CancellationToken.None); + const success = false; this._logService.debug(`[notebook editor model] saveAs - document saved, start updating file stats`, this.resource.toString(true), success); this._lastResolvedFileStat = await this._resolveStats(this.resource); if (!success) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 756377bdd86..a32d1ba2815 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -30,6 +30,7 @@ export interface INotebookKernelChangeEvent { kind?: true; supportedLanguages?: true; hasExecutionOrder?: true; + hasInterruptHandler?: true; } export interface INotebookKernel { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 0c37ffe127c..f67d9be987d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -22,8 +22,6 @@ export interface INotebookContentProvider { options: TransientOptions; open(uri: URI, backupId: string | VSBuffer | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise<{ data: NotebookData; transientOptions: TransientOptions }>; - save(uri: URI, token: CancellationToken): Promise<boolean>; - saveAs(uri: URI, target: URI, token: CancellationToken): Promise<boolean>; backup(uri: URI, token: CancellationToken): Promise<string | VSBuffer>; } diff --git a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts index d001722562c..ac2fe448888 100644 --- a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts @@ -13,6 +13,8 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { EditorExtensions, IEditorSerializer, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; import { PerfviewContrib, PerfviewInput } from 'vs/workbench/contrib/performance/browser/perfviewEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { InstantiationService, Trace } from 'vs/platform/instantiation/common/instantiationService'; +import { EventProfiling } from 'vs/base/common/event'; // -- startup performance view @@ -55,3 +57,74 @@ registerAction2(class extends Action2 { return editorService.openEditor(instaService.createInstance(PerfviewInput), { pinned: true }); } }); + + +registerAction2(class PrintServiceCycles extends Action2 { + + constructor() { + super({ + id: 'perf.insta.printAsyncCycles', + title: { value: localize('cycles', "Print Service Cycles"), original: 'Print Service Cycles' }, + category: CATEGORIES.Developer, + f1: true + }); + } + + run(accessor: ServicesAccessor) { + const instaService = accessor.get(IInstantiationService); + if (instaService instanceof InstantiationService) { + const cycle = instaService._globalGraph?.findCycleSlow(); + if (cycle) { + console.warn(`CYCLE`, cycle); + } else { + console.warn(`YEAH, no more cycles`); + } + } + } +}); + +registerAction2(class PrintServiceTraces extends Action2 { + + constructor() { + super({ + id: 'perf.insta.printTraces', + title: { value: localize('insta.trace', "Print Service Traces"), original: 'Print Service Traces' }, + category: CATEGORIES.Developer, + f1: true + }); + } + + run() { + if (Trace.all.size === 0) { + console.log('Enable via `instantiationService.ts#_enableAllTracing`'); + return; + } + + for (const item of Trace.all) { + console.log(item); + } + } +}); + + +registerAction2(class PrintEventProfiling extends Action2 { + + constructor() { + super({ + id: 'perf.event.profiling', + title: { value: localize('emitter', "Print Emitter Profiles"), original: 'Print Emitter Profiles' }, + category: CATEGORIES.Developer, + f1: true + }); + } + + run(): void { + if (EventProfiling.all.size === 0) { + console.log('USE `EmitterOptions._profName` to enable profiling'); + return; + } + for (const item of EventProfiling.all) { + console.log(`${item.name}: ${item.invocationCount}invocations COST ${item.elapsedOverall}ms, ${item.listenerCount} listeners, avg cost is ${item.durations.reduce((a, b) => a + b, 0) / item.durations.length}ms`); + } + } +}); diff --git a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index 2074eef20bd..afc896d37fb 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts @@ -62,7 +62,7 @@ export class ApplyFileSnippetAction extends SnippetsAction { }]); // set language if possible - modelService.setMode(editor.getModel(), langService.createById(selection.langId)); + modelService.setMode(editor.getModel(), langService.createById(selection.langId), ApplyFileSnippetAction.Id); } } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 18cd0e959dd..a6f47c08802 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -80,6 +80,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { TerminalExitReason } from 'vs/platform/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -261,7 +262,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @ILogService private readonly _logService: ILogService, @IThemeService private readonly _themeService: IThemeService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -1953,7 +1955,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _getGroupedTasks(filter?: ITaskFilter): Promise<TaskMap> { const type = filter?.type; - const name = filter?.task; const needsRecentTasksMigration = this._needsRecentTasksMigration(); await this._activateTaskProviders(filter?.type); const validTypes: IStringDictionary<boolean> = Object.create(null); @@ -2006,9 +2007,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if ((task.type !== 'shell') && (task.type !== 'process')) { this._showOutput(); } - if (task.getDefinition(true)?._key === name || task._label === name) { - return done({ tasks: [task], extension: taskSet.extension }); - } break; } } @@ -2639,7 +2637,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return entries; } private async _showTwoLevelQuickPick(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, type?: string, name?: string) { - return TaskQuickPick.show(this, this._configurationService, this._quickInputService, this._notificationService, this._dialogService, this._themeService, placeHolder, defaultEntry, type, name); + return this._instantiationService.createInstance(TaskQuickPick).show(placeHolder, defaultEntry, type, name); } private async _showQuickPick(tasks: Promise<Task[]> | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[], type?: string, name?: string): Promise<ITaskQuickPickEntry | undefined | null> { @@ -2696,7 +2694,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer picker.busy = true; pickEntries.then(entries => { picker.busy = false; - picker.items = entries.filter(e => e.type === type); }); picker.show(); @@ -2783,17 +2780,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _runTaskCommand(arg?: any): void { const identifier = this._getTaskIdentifier(arg); const type = arg && typeof arg !== 'string' && 'type' in arg ? arg.type : undefined; - let task = arg && typeof arg !== 'string' && 'task' in arg ? arg.task : arg === 'string' ? arg : undefined; - if (identifier) { - this._getGroupedTasks({ task, type }).then(async (grouped) => { - const resolver = this._createResolver(grouped); - const tasks = grouped.all(); - const folderURIs: (URI | string)[] = this._contextService.getWorkspace().folders.map(folder => folder.uri); - if (this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - folderURIs.push(this._contextService.getWorkspace().configuration!); - } - folderURIs.push(USER_TASKS_GROUP_KEY); - // match by identifier + const task = arg && typeof arg !== 'string' && 'task' in arg ? arg.task : arg === 'string' ? arg : undefined; + this._getGroupedTasks().then(async (grouped) => { + const tasks = grouped.all(); + const resolver = this._createResolver(grouped); + const folderURIs: (URI | string)[] = this._contextService.getWorkspace().folders.map(folder => folder.uri); + if (this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + folderURIs.push(this._contextService.getWorkspace().configuration!); + } + folderURIs.push(USER_TASKS_GROUP_KEY); + if (identifier) { for (const uri of folderURIs) { const task = await resolver.resolve(uri, identifier); if (task) { @@ -2801,22 +2797,26 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } } - // match by label - if (!!task) { - const taskToRun = tasks.find(g => g._label === task); - if (taskToRun) { - this.run(taskToRun).then(undefined, () => { }); - return; + } + const exactMatchTask = tasks.find(t => task && (t.getDefinition(true)?.configurationProperties?.identifier === task || t.configurationProperties?.identifier === task || t._label === task)); + const filteredTasks = tasks.filter(t => t._label.includes(task)); + if (exactMatchTask) { + const id = exactMatchTask.configurationProperties?.identifier || exactMatchTask.getDefinition(true)?.configurationProperties?.identifier; + if (id) { + for (const uri of folderURIs) { + const task = await resolver.resolve(uri, id); + if (task) { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, () => { }); + return; + } } } - if (task && !tasks.some(g => g._label.includes(task))) { - task = undefined; - } - // if task is defined, will be used as a filter - this._doRunTaskCommand(tasks, type, task); - }); - } - this._doRunTaskCommand(); + } else if (filteredTasks?.length > 1) { + return this._doRunTaskCommand(tasks, type, task); + } else { + return this._doRunTaskCommand(); + } + }); } private _tasksAndGroupedTasks(filter?: ITaskFilter): { tasks: Promise<Task[]>; grouped: Promise<TaskMap> } { @@ -3268,7 +3268,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._isTaskEntry(selection)) { this._configureTask(selection.task); } else if (this._isSettingEntry(selection)) { - const taskQuickPick = new TaskQuickPick(this, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService); + const taskQuickPick = this._instantiationService.createInstance(TaskQuickPick); taskQuickPick.handleSettingOption(selection.settingType); } else if (selection.folder && (this._contextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this._openTaskFile(selection.folder.toResource('.vscode/tasks.json'), TaskSourceKind.Workspace); diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index de37cba16a0..dc5551150c3 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -21,6 +21,8 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getColorClass, getColorStyleElement } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { TaskQuickPickEntryType } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; +import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin'; +import { IStorageService } from 'vs/platform/storage/common/storage'; export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip'; @@ -42,16 +44,19 @@ const SHOW_ALL: string = nls.localize('taskQuickPick.showAll', "Show All Tasks.. export const configureTaskIcon = registerIcon('tasks-list-configure', Codicon.gear, nls.localize('configureTaskIcon', 'Configuration icon in the tasks selection list.')); const removeTaskIcon = registerIcon('tasks-remove', Codicon.close, nls.localize('removeTaskIcon', 'Icon for remove in the tasks selection list.')); +const runTaskStorageKey = 'runTaskStorageKey'; + export class TaskQuickPick extends Disposable { private _sorter: TaskSorter; private _topLevelEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] | undefined; constructor( - private _taskService: ITaskService, - private _configurationService: IConfigurationService, - private _quickInputService: IQuickInputService, - private _notificationService: INotificationService, - private _themeService: IThemeService, - private _dialogService: IDialogService) { + @ITaskService private _taskService: ITaskService, + @IConfigurationService private _configurationService: IConfigurationService, + @IQuickInputService private _quickInputService: IQuickInputService, + @INotificationService private _notificationService: INotificationService, + @IThemeService private _themeService: IThemeService, + @IDialogService private _dialogService: IDialogService, + @IStorageService private _storageService: IStorageService) { super(); this._sorter = this._taskService.createSorter(); } @@ -225,8 +230,6 @@ export class TaskQuickPick extends Disposable { picker.placeholder = placeHolder; picker.matchOnDescription = true; picker.ignoreFocusOut = false; - picker.show(); - picker.onDidTriggerItemButton(async (context) => { const task = context.item.task; if (context.button.iconClass === ThemeIcon.asClassName(removeTaskIcon)) { @@ -238,7 +241,7 @@ export class TaskQuickPick extends Disposable { if (indexToRemove >= 0) { picker.items = [...picker.items.slice(0, indexToRemove), ...picker.items.slice(indexToRemove + 1)]; } - } else { + } else if (context.button.iconClass === ThemeIcon.asClassName(configureTaskIcon)) { this._quickInputService.cancel(); if (ContributedTask.is(task)) { this._taskService.customize(task, undefined, true); @@ -255,6 +258,9 @@ export class TaskQuickPick extends Disposable { } } }); + if (name) { + picker.value = name; + } let firstLevelTask: Task | ConfiguringTask | string | undefined | null = startAtType; if (!firstLevelTask) { // First show recent tasks configured tasks. Other tasks will be available at a second level @@ -264,15 +270,18 @@ export class TaskQuickPick extends Disposable { return this._toTask(topLevelEntriesResult.isSingleConfigured); } const taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] = topLevelEntriesResult.entries; - if (name) { - picker.value = name; - } firstLevelTask = await this._doPickerFirstLevel(picker, taskQuickPickEntries); } do { + if (Types.isString(firstLevelTask)) { + if (name) { + await this._doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries); + picker.dispose(); + return undefined; + } + const selectedEntry = await this.doPickerSecondLevel(picker, firstLevelTask); // Proceed to second level of quick pick - const selectedEntry = await this.doPickerSecondLevel(picker, firstLevelTask, name); if (selectedEntry && !selectedEntry.settingType && selectedEntry.task === null) { // The user has chosen to go back to the first level firstLevelTask = await this._doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries); @@ -299,6 +308,7 @@ export class TaskQuickPick extends Disposable { private async _doPickerFirstLevel(picker: IQuickPick<ITaskTwoLevelQuickPickEntry>, taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]): Promise<Task | ConfiguringTask | string | null | undefined> { picker.items = taskQuickPickEntries; + showWithPinnedItems(this._storageService, runTaskStorageKey, picker, true); const firstLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => { Event.once(picker.onDidAccept)(async () => { resolve(picker.selectedItems ? picker.selectedItems[0] : undefined); @@ -317,7 +327,7 @@ export class TaskQuickPick extends Disposable { picker.value = name || ''; picker.items = await this._getEntriesForProvider(type); } - picker.show(); + await picker.show(); picker.busy = false; const secondLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => { Event.once(picker.onDidAccept)(async () => { @@ -400,11 +410,4 @@ export class TaskQuickPick extends Disposable { } return resolvedTask; } - - static async show(taskService: ITaskService, configurationService: IConfigurationService, - quickInputService: IQuickInputService, notificationService: INotificationService, - dialogService: IDialogService, themeService: IThemeService, placeHolder: string, defaultEntry?: ITaskQuickPickEntry, type?: string, name?: string) { - const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService, themeService, dialogService); - return taskQuickPick.show(placeHolder, defaultEntry, type, name); - } } diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts index 7c3731d171d..dd605f15b54 100644 --- a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -18,6 +18,7 @@ import { isString } from 'vs/base/common/types'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQuickAccessItem> { @@ -30,7 +31,8 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ @IQuickInputService private _quickInputService: IQuickInputService, @INotificationService private _notificationService: INotificationService, @IDialogService private _dialogService: IDialogService, - @IThemeService private _themeService: IThemeService + @IThemeService private _themeService: IThemeService, + @IStorageService private _storageService: IStorageService ) { super(TasksQuickAccessProvider.PREFIX, { noResultsPick: { @@ -44,7 +46,7 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ return []; } - const taskQuickPick = new TaskQuickPick(this._taskService, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService); + const taskQuickPick = new TaskQuickPick(this._taskService, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService, this._storageService); const topLevelPicks = await taskQuickPick.getTopLevelEntries(); const taskPicks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = []; diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 44eee05cdac..eb0befa657e 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -125,7 +125,8 @@ export class TaskService extends AbstractTaskService { logService, themeService, lifecycleService, - remoteAgentService + remoteAgentService, + instantiationService ); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index e1efdfa0089..16238ea7baf 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -289,8 +289,8 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack await this._remoteTerminalChannel?.updateTitle(id, title, titleSource); } - async updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<void> { - await this._remoteTerminalChannel?.updateIcon(id, icon, color); + async updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void> { + await this._remoteTerminalChannel?.updateIcon(id, userInitiated, icon, color); } async getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index b17009729d6..ee8797508f0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -144,8 +144,8 @@ export interface ITerminalService extends ITerminalInstanceHost { onDidMaximumDimensionsChange: Event<ITerminalInstance>; onDidRequestStartExtensionTerminal: Event<IStartExtensionTerminalRequest>; onDidChangeInstanceTitle: Event<ITerminalInstance | undefined>; - onDidChangeInstanceIcon: Event<ITerminalInstance | undefined>; - onDidChangeInstanceColor: Event<ITerminalInstance | undefined>; + onDidChangeInstanceIcon: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; + onDidChangeInstanceColor: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; onDidChangeInstancePrimaryStatus: Event<ITerminalInstance>; onDidInputInstanceData: Event<ITerminalInstance>; onDidRegisterProcessSupport: Event<void>; @@ -524,7 +524,7 @@ export interface ITerminalInstance { /** * An event that fires when the terminal instance's icon changes. */ - onIconChanged: Event<ITerminalInstance>; + onIconChanged: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; /** * An event that fires when the terminal instance is disposed. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 43eae42dee2..eae0e132b05 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -319,7 +319,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onLinksReady = this._onLinksReady.event; private readonly _onTitleChanged = this._register(new Emitter<ITerminalInstance>()); readonly onTitleChanged = this._onTitleChanged.event; - private readonly _onIconChanged = this._register(new Emitter<ITerminalInstance>()); + private readonly _onIconChanged = this._register(new Emitter<{ instance: ITerminalInstance; userInitiated: boolean }>()); readonly onIconChanged = this._onIconChanged.event; private readonly _onData = this._register(new Emitter<string>()); readonly onData = this._onData.event; @@ -1464,7 +1464,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } if (originalIcon !== this.shellLaunchConfig.icon || this.shellLaunchConfig.color) { this._icon = this._shellLaunchConfig.attachPersistentProcess?.icon || this._shellLaunchConfig.icon; - this._onIconChanged.fire(this); + this._onIconChanged.fire({ instance: this, userInitiated: false }); } } @@ -2211,7 +2211,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); if (result) { this._icon = result.icon; - this._onIconChanged.fire(this); + this._onIconChanged.fire({ instance: this, userInitiated: true }); } } @@ -2248,7 +2248,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (result) { this.shellLaunchConfig.color = result.id; - this._onIconChanged.fire(this); + this._onIconChanged.fire({ instance: this, userInitiated: true }); } quickPick.hide(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 7e7e9acb703..d4a0e680568 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -38,8 +39,9 @@ import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/wo import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, StartupKind, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -132,10 +134,10 @@ export class TerminalService implements ITerminalService { get onDidChangeInstances(): Event<void> { return this._onDidChangeInstances.event; } private readonly _onDidChangeInstanceTitle = new Emitter<ITerminalInstance | undefined>(); get onDidChangeInstanceTitle(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceTitle.event; } - private readonly _onDidChangeInstanceIcon = new Emitter<ITerminalInstance | undefined>(); - get onDidChangeInstanceIcon(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceIcon.event; } - private readonly _onDidChangeInstanceColor = new Emitter<ITerminalInstance | undefined>(); - get onDidChangeInstanceColor(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceColor.event; } + private readonly _onDidChangeInstanceIcon = new Emitter<{ instance: ITerminalInstance; userInitiated: boolean }>(); + get onDidChangeInstanceIcon(): Event<{ instance: ITerminalInstance; userInitiated: boolean }> { return this._onDidChangeInstanceIcon.event; } + private readonly _onDidChangeInstanceColor = new Emitter<{ instance: ITerminalInstance; userInitiated: boolean }>(); + get onDidChangeInstanceColor(): Event<{ instance: ITerminalInstance; userInitiated: boolean }> { return this._onDidChangeInstanceColor.event; } private readonly _onDidChangeActiveInstance = new Emitter<ITerminalInstance | undefined>(); get onDidChangeActiveInstance(): Event<ITerminalInstance | undefined> { return this._onDidChangeActiveInstance.event; } private readonly _onDidChangeInstancePrimaryStatus = new Emitter<ITerminalInstance>(); @@ -159,6 +161,7 @@ export class TerminalService implements ITerminalService { @IInstantiationService private _instantiationService: IInstantiationService, @IRemoteAgentService private _remoteAgentService: IRemoteAgentService, @IViewsService private _viewsService: IViewsService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @@ -498,7 +501,7 @@ export class TerminalService implements ITerminalService { // terminal ID will be stale and the process will be leaked. this.onDidReceiveProcessId(() => this._saveState()); this.onDidChangeInstanceTitle(instance => this._updateTitle(instance)); - this.onDidChangeInstanceIcon(instance => this._updateIcon(instance)); + this.onDidChangeInstanceIcon(e => this._updateIcon(e.instance, e.userInitiated)); } private _handleInstanceContextKeys(): void { @@ -673,7 +676,7 @@ export class TerminalService implements ITerminalService { } @debounce(500) - private _updateTitle(instance?: ITerminalInstance): void { + private _updateTitle(instance: ITerminalInstance | undefined): void { if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.title || instance.isDisposed) { return; } @@ -685,11 +688,11 @@ export class TerminalService implements ITerminalService { } @debounce(500) - private _updateIcon(instance?: ITerminalInstance): void { + private _updateIcon(instance: ITerminalInstance, userInitiated: boolean): void { if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.icon || instance.isDisposed) { return; } - this._primaryBackend?.updateIcon(instance.persistentProcessId, instance.icon, instance.color); + this._primaryBackend?.updateIcon(instance.persistentProcessId, userInitiated, instance.icon, instance.color); } refreshActiveGroup(): void { @@ -1111,11 +1114,7 @@ export class TerminalService implements ITerminalService { private _getEditorOptions(location?: ITerminalLocationOptions): TerminalEditorLocation | undefined { if (location && typeof location === 'object' && 'viewColumn' in location) { - // When ACTIVE_GROUP is used, resolve it to an actual group to ensure the is created in - // the active group even if it is locked - if (location.viewColumn === ACTIVE_GROUP) { - location.viewColumn = this._editorGroupsService.activeGroup.index; - } + location.viewColumn = columnToEditorGroup(this._editorGroupsService, this._configurationService, location.viewColumn); return location; } return undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index b9690635096..966f327339c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -398,8 +398,8 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { // Register listeners to update the tab this._register(this._terminalService.onDidChangeInstancePrimaryStatus(e => this.updateLabel(e))); this._register(this._terminalGroupService.onDidChangeActiveInstance(() => this.updateLabel())); - this._register(this._terminalService.onDidChangeInstanceIcon(e => this.updateLabel(e))); - this._register(this._terminalService.onDidChangeInstanceColor(e => this.updateLabel(e))); + this._register(this._terminalService.onDidChangeInstanceIcon(e => this.updateLabel(e.instance))); + this._register(this._terminalService.onDidChangeInstanceColor(e => this.updateLabel(e.instance))); this._register(this._terminalService.onDidChangeInstanceTitle(e => { if (e === this._terminalGroupService.activeInstance) { this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator); diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 4568a2a7dcc..377973047c3 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -276,8 +276,8 @@ export class RemoteTerminalChannelClient implements IPtyHostController { return this._channel.call('$updateTitle', [id, title, titleSource]); } - updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<string> { - return this._channel.call('$updateIcon', [id, icon, color]); + updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<string> { + return this._channel.call('$updateIcon', [id, userInitiated, icon, color]); } refreshProperty<T extends ProcessPropertyType>(id: number, property: T): Promise<IProcessPropertyMap[T]> { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 25eb53c77e9..0e68e807b59 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -127,7 +127,7 @@ export interface ITerminalBackend { getShellEnvironment(): Promise<IProcessEnvironment | undefined>; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void>; updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void>; - updateIcon(id: number, icon: TerminalIcon, color?: string): Promise<void>; + updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void>; getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>; reduceConnectionGraceTime(): Promise<void>; requestDetachInstance(workspaceId: string, instanceId: number): Promise<IProcessDetails | undefined>; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 410cb5de5ae..42fe6ff3212 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -131,8 +131,8 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke await this._localPtyService.updateTitle(id, title, titleSource); } - async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string; color?: { id: string } }, color?: string): Promise<void> { - await this._localPtyService.updateIcon(id, icon, color); + async updateIcon(id: number, userInitiated: boolean, icon: URI | { light: URI; dark: URI } | { id: string; color?: { id: string } }, color?: string): Promise<void> { + await this._localPtyService.updateIcon(id, userInitiated, icon, color); } updateProperty<T extends ProcessPropertyType>(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 29254634a6d..95646185780 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -277,6 +277,10 @@ export class GettingStartedPage extends EditorPane { })); this.recentlyOpened = workspacesService.getRecentlyOpened(); + this._register(workspacesService.onDidChangeRecentlyOpened(() => { + this.recentlyOpened = workspacesService.getRecentlyOpened(); + rerender(); + })); } // remove when 'workbench.welcomePage.preferReducedMotion' deprecated diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 16385a9895a..8d694700fd9 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -1435,7 +1435,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); - userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(toUserDataProfile('custom', joinPath(environmentService.userRoamingDataHome, 'profiles', 'temp')), userDataProfilesService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(toUserDataProfile('custom', 'custom', joinPath(environmentService.userRoamingDataHome, 'profiles', 'temp')), userDataProfilesService)); workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -1531,7 +1531,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue", "configurationService.profiles.testSetting": "profileValue" }')); await testObject.reloadConfiguration(); - const profile = toUserDataProfile('custom2', joinPath(environmentService.userRoamingDataHome, 'profiles', 'custom2')); + const profile = toUserDataProfile('custom2', 'custom2', joinPath(environmentService.userRoamingDataHome, 'profiles', 'custom2')); await fileService.writeFile(profile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue2", "configurationService.profiles.testSetting": "profileValue2" }')); const promise = Event.toPromise(testObject.onDidChangeConfiguration); await userDataProfileService.updateCurrentProfile(profile, false); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 8ff9cecf63b..fabbda76cb7 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -167,7 +167,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<void> { - const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder'); + const title = nls.localize('openFileOrFolder.title', 'Open File or Folder'); const availableFileSystems = this.addFileSchemaIfNeeded(schema); const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index e6b24f4013c..3c8446b2b12 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -52,7 +52,6 @@ export const allApiProposals = Object.freeze({ scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', - snippetWorkspaceEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts', tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', diff --git a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts index 36ffefdd42c..d036cb033b1 100644 --- a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts +++ b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts @@ -8,6 +8,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const ILanguageDetectionService = createDecorator<ILanguageDetectionService>('ILanguageDetectionService'); +export const LanguageDetectionLanguageEventSource = 'languageDetection'; + export interface ILanguageDetectionService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/search/common/ignoreFile.ts b/src/vs/workbench/services/search/common/ignoreFile.ts index 769c8aa8273..9de868fdeff 100644 --- a/src/vs/workbench/services/search/common/ignoreFile.ts +++ b/src/vs/workbench/services/search/common/ignoreFile.ts @@ -133,7 +133,13 @@ export class IgnoreFile { line = '**/' + line; } else { if (firstSep === 0) { - line = line.slice(1); + if (dirPath.slice(-1) === '/') { + line = line.slice(1); + } + } else { + if (dirPath.slice(-1) !== '/') { + line = '/' + line; + } } line = dirPath + line; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 6672a7126d3..6dc44f932c3 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -197,8 +197,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.modelService.setMode(this.textEditorModel, languageSelection); } - override setLanguageId(languageId: string): void { - super.setLanguageId(languageId); + override setLanguageId(languageId: string, source?: string): void { + super.setLanguageId(languageId, source); this.preferredLanguageId = languageId; } @@ -556,15 +556,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private installModelListeners(model: ITextModel): void { + protected override installModelListeners(model: ITextModel): void { // See https://github.com/microsoft/vscode/issues/30189 // This code has been extracted to a different method because it caused a memory leak // where `value` was captured in the content change listener closure scope. - // Listen to text model events this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); this._register(model.onDidChangeLanguage(() => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings + + super.installModelListeners(model); } private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 8cb717b2fca..e961ed75e2f 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -468,7 +468,7 @@ export interface ILanguageSupport { /** * Sets the language id of the object. */ - setLanguageId(languageId: string, setExplicitly?: boolean): void; + setLanguageId(languageId: string, source?: string): void; } export interface ITextFileEditorModelSaveEvent extends IWorkingCopySaveEvent { diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts index 326624d3ae0..c3d68a86cf7 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts @@ -108,8 +108,8 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp return this.model.setEncoding(encoding); } - setLanguageId(languageId: string): void { - this.model.setLanguageId(languageId); + setLanguageId(languageId: string, source?: string): void { + this.model.setLanguageId(languageId, source); } getLanguageId(): string | undefined { diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 5d3e956cadb..1d3657cf103 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -190,14 +190,14 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#region Language - override setLanguageId(languageId: string): void { + override setLanguageId(languageId: string, source?: string): void { const actualLanguage: string | undefined = languageId === UntitledTextEditorModel.ACTIVE_EDITOR_LANGUAGE_ID ? this.editorService.activeTextEditorLanguageId : languageId; this.preferredLanguageId = actualLanguage; if (actualLanguage) { - super.setLanguageId(actualLanguage); + super.setLanguageId(actualLanguage, source); } } @@ -333,8 +333,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt // Listen to text model events const textEditorModel = assertIsDefined(this.textEditorModel); - this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(textEditorModel, e))); - this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange(true))); // language change can have impact on config + this.installModelListeners(textEditorModel); // Only adjust name and dirty state etc. if we // actually created the untitled model @@ -358,6 +357,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return super.resolve(); } + protected override installModelListeners(model: ITextModel): void { + this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e))); + this._register(model.onDidChangeLanguage(() => this.onConfigurationChange(true))); // language change can have impact on config + + super.installModelListeners(model); + } + private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void { // mark the untitled text editor as non-dirty once its content becomes empty and we do diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 2ab69b9299f..b9bd7078da9 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -20,6 +20,7 @@ import { EditorInputCapabilities } from 'vs/workbench/common/editor'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isReadable, isReadableStream } from 'vs/base/common/stream'; import { readableToBuffer, streamToBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; suite('Untitled text editors', () => { @@ -334,6 +335,55 @@ suite('Untitled text editors', () => { registration.dispose(); }); + // Issue #159202 + test('remembers that language was set explicitly if set by another source (i.e. ModelService)', async () => { + const language = 'untitled-input-test'; + + const registration = accessor.languageService.registerLanguage({ + id: language, + }); + + const service = accessor.untitledTextEditorService; + const model = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, model); + await input.resolve(); + + assert.ok(!input.model.hasLanguageSetExplicitly); + accessor.modelService.setMode(model.textEditorModel!, accessor.languageService.createById(language)); + assert.ok(input.model.hasLanguageSetExplicitly); + + assert.strictEqual(model.getLanguageId(), language); + + model.dispose(); + registration.dispose(); + }); + + test('Language is not set explicitly if set by language detection source', async () => { + const language = 'untitled-input-test'; + + const registration = accessor.languageService.registerLanguage({ + id: language, + }); + + const service = accessor.untitledTextEditorService; + const model = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, model); + await input.resolve(); + + assert.ok(!input.model.hasLanguageSetExplicitly); + accessor.modelService.setMode( + model.textEditorModel!, + accessor.languageService.createById(language), + // This is really what this is testing + LanguageDetectionLanguageEventSource); + assert.ok(!input.model.hasLanguageSetExplicitly); + + assert.strictEqual(model.getLanguageId(), language); + + model.dispose(); + registration.dispose(); + }); + test('service#onDidChangeEncoding', async () => { const service = accessor.untitledTextEditorService; const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index e9cd97c1bc0..1e8af9e7fb7 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -53,7 +53,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse } async createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise<IUserDataProfile> { - const profile = await this.userDataProfilesService.createProfile(name, useDefaultFlags, this.getWorkspaceIdentifier()); + const profile = await this.userDataProfilesService.createNamedProfile(name, useDefaultFlags, this.getWorkspaceIdentifier()); await this.enterProfile(profile, !!fromExisting); return profile; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e0465423c62..b955a8c3efb 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1608,7 +1608,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput setPreferredDescription(description: string): void { } setPreferredEncoding(encoding: string) { } setPreferredContents(contents: string): void { } - setLanguageId(languageId: string) { } + setLanguageId(languageId: string, source?: string) { } setPreferredLanguageId(languageId: string) { } setForceOpenAsBinary(): void { } setFailToOpen(): void { @@ -2010,7 +2010,7 @@ export class TestUserDataProfileService implements IUserDataProfileService { readonly _serviceBrand: undefined; readonly onDidUpdateCurrentProfile = Event.None; readonly onDidChangeCurrentProfile = Event.None; - readonly currentProfile = toUserDataProfile('test', URI.file('tests').with({ scheme: 'vscode-tests' })); + readonly currentProfile = toUserDataProfile('test', 'test', URI.file('tests').with({ scheme: 'vscode-tests' })); async updateCurrentProfile(): Promise<void> { } } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 2dad23406e6..d1c831212b1 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -2276,6 +2276,18 @@ declare module 'vscode' { static readonly RefactorInline: CodeActionKind; /** + * Base kind for refactoring move actions: `refactor.move` + * + * Example move actions: + * + * - Move a function to a new file + * - Move a property between classes + * - Move method to base class + * - ... + */ + static readonly RefactorMove: CodeActionKind; + + /** * Base kind for refactoring rewrite actions: `refactor.rewrite` * * Example rewrite actions: @@ -2284,7 +2296,6 @@ declare module 'vscode' { * - Add or remove parameter * - Encapsulate field * - Make method static - * - Move method to base class * - ... */ static readonly RefactorRewrite: CodeActionKind; @@ -3427,6 +3438,54 @@ declare module 'vscode' { } /** + * A snippet edit represents an interactive edit that is performed by + * the editor. + * + * *Note* that a snippet edit can always be performed as a normal {@link TextEdit text edit}. + * This will happen when no matching editor is open or when a {@link WorkspaceEdit workspace edit} + * contains snippet edits for multiple files. In that case only those that match the active editor + * will be performed as snippet edits and the others as normal text edits. + */ + export class SnippetTextEdit { + + /** + * Utility to create a replace snippet edit. + * + * @param range A range. + * @param snippet A snippet string. + * @return A new snippet edit object. + */ + static replace(range: Range, snippet: SnippetString): SnippetTextEdit; + + /** + * Utility to create an insert snippet edit. + * + * @param position A position, will become an empty range. + * @param snippet A snippet string. + * @return A new snippet edit object. + */ + static insert(position: Position, snippet: SnippetString): SnippetTextEdit; + + /** + * The range this edit applies to. + */ + range: Range; + + /** + * The {@link SnippetString snippet} this edit will perform. + */ + snippet: SnippetString; + + /** + * Create a new snippet edit. + * + * @param range A range. + * @param snippet A snippet string. + */ + constructor(range: Range, snippet: SnippetString); + } + + /** * A notebook edit represents edits that should be applied to the contents of a notebook. */ export class NotebookEdit { @@ -3571,12 +3630,36 @@ declare module 'vscode' { has(uri: Uri): boolean; /** - * Set (and replace) text edits for a resource. + * Set (and replace) notebook edits for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: NotebookEdit[]): void; + + /** + * Set (and replace) notebook edits with metadata for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: [NotebookEdit, WorkspaceEditEntryMetadata][]): void; + + /** + * Set (and replace) text edits or snippet edits for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: (TextEdit | SnippetTextEdit)[]): void; + + /** + * Set (and replace) text edits or snippet edits with metadata for a resource. * * @param uri A resource identifier. * @param edits An array of edits. */ - set(uri: Uri, edits: TextEdit[] | NotebookEdit[]): void; + set(uri: Uri, edits: [TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata][]): void; /** * Get the text edits for a resource. diff --git a/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts b/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts index b1e9211ceb2..d336c2ccf54 100644 --- a/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts @@ -8,29 +8,6 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/147248 /** @deprecated */ - interface NotebookDocumentBackup { - /** - * Unique identifier for the backup. - * - * This id is passed back to your extension in `openNotebook` when opening a notebook editor from a backup. - */ - readonly id: string; - - /** - * Delete the current backup. - * - * This is called by the editor when it is clear the current backup is no longer needed, such as when a new backup - * is made or when the file is saved. - */ - delete(): void; - } - - /** @deprecated */ - interface NotebookDocumentBackupContext { - readonly destination: Uri; - } - - /** @deprecated */ interface NotebookDocumentOpenContext { readonly backupId?: string; readonly untitledDocumentData?: Uint8Array; @@ -39,26 +16,13 @@ declare module 'vscode' { // todo@API use openNotebookDOCUMENT to align with openCustomDocument etc? // todo@API rename to NotebookDocumentContentProvider /** @deprecated */ - export interface NotebookContentProvider { - readonly options?: NotebookDocumentContentOptions; - readonly onDidChangeNotebookContentOptions?: Event<NotebookDocumentContentOptions>; - /** * Content providers should always use {@link FileSystemProvider file system providers} to * resolve the raw content for `uri` as the resource is not necessarily a file on disk. */ openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext, token: CancellationToken): NotebookData | Thenable<NotebookData>; - - // todo@API use NotebookData instead - saveNotebook(document: NotebookDocument, token: CancellationToken): Thenable<void>; - - // todo@API use NotebookData instead - saveNotebookAs(targetResource: Uri, document: NotebookDocument, token: CancellationToken): Thenable<void>; - - // todo@API use NotebookData instead - backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, token: CancellationToken): Thenable<NotebookDocumentBackup>; } export namespace workspace { diff --git a/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts b/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts index c78f1f7a3b7..986f7fac85c 100644 --- a/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts @@ -14,8 +14,12 @@ declare module 'vscode' { } export namespace workspace { - // SPECIAL overload with NotebookRegistrationData + /** + * SPECIAL overload with NotebookRegistrationData + * @deprecated + */ export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions, registrationData?: NotebookRegistrationData): Disposable; + // SPECIAL overload with NotebookRegistrationData export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions, registration?: NotebookRegistrationData): Disposable; } diff --git a/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts b/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts deleted file mode 100644 index eb67ff251ba..00000000000 --- a/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/145374 - - // TODO@API - WorkspaceEditEntryMetadata - - export class SnippetTextEdit { - - /** - * Utility to create an replace snippet edit. - * - * @param range A range. - * @param snippet A snippet string. - * @return A new snippet edit object. - */ - static replace(range: Range, snippet: SnippetString): SnippetTextEdit; - - /** - * Utility to create an insert snippet edit. - * - * @param position A position, will become an empty range. - * @param snippet A snippet string. - * @return A new snippet edit object. - */ - static insert(position: Position, snippet: SnippetString): SnippetTextEdit; - - /** - * The range this edit applies to. - */ - range: Range; - - /** - * The {@link SnippetString snippet} this edit will perform. - */ - snippet: SnippetString; - - /** - * Create a new snippet edit. - * - * @param range A range. - * @param snippet A snippet string. - */ - constructor(range: Range, snippet: SnippetString); - } - - interface WorkspaceEdit { - - /** - * Set (and replace) notebook edits for a resource. - * - * @param uri A resource identifier. - * @param edits An array of edits. - */ - set(uri: Uri, edits: NotebookEdit[]): void; - - /** - * Set (and replace) notebook edits with metadata for a resource. - * - * @param uri A resource identifier. - * @param edits An array of edits. - */ - set(uri: Uri, edits: [NotebookEdit, WorkspaceEditEntryMetadata][]): void; - - /** - * Set (and replace) text edits or snippet edits for a resource. - * - * @param uri A resource identifier. - * @param edits An array of edits. - */ - set(uri: Uri, edits: (TextEdit | SnippetTextEdit)[]): void; - - /** - * Set (and replace) text edits or snippet edits with metadata for a resource. - * - * @param uri A resource identifier. - * @param edits An array of edits. - */ - set(uri: Uri, edits: [TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata][]): void; - } -} diff --git a/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts b/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts index a1c5cb1df12..597e95db9da 100644 --- a/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts +++ b/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts @@ -9,7 +9,7 @@ declare module 'vscode' { /** * [TreeItemCheckboxState](#TreeItemCheckboxState) of the tree item. */ - checkboxState?: TreeItemCheckboxState; + checkboxState?: TreeItemCheckboxState | { state: TreeItemCheckboxState; tooltip?: string }; } /** diff --git a/yarn.lock b/yarn.lock index 83cc7d0a1ed..4c8f3c14e8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11499,10 +11499,10 @@ windows-mutex@0.4.1: bindings "^1.2.1" nan "^2.14.0" -windows-process-tree@0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.3.tgz#7c178815f02bf4cfbcac1f93b2f3a3cc10bc9245" - integrity sha512-rkiAMP0AS27xikFyn7i4gPbOK16UdjY8X/C6eo37CnfNLqTvK2eEaT+Dh0e5xnvmlsi0lEKd60O+4ajzfDkq7A== +windows-process-tree@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.4.tgz#6bc4b8010129c30ff95bcd333b9f94744dd3c4fb" + integrity sha512-rtSX73i9OnkDxSdBP9c1YBunEwheZdO/hjRwNk9uSoWqO92x0zDRGfIIK0MtUn8gZZD+2kPEVpj5MmfNl7JpJA== dependencies: nan "^2.13.2" @@ -11628,40 +11628,40 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-canvas@0.2.0-beta.23: - version "0.2.0-beta.23" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.23.tgz#f5ee0db3b029ea705ef3c1228825c28ec6368b48" - integrity sha512-414qLxMlOzC3LyAt1qHmvrcW2VIPAsFQkXTGcSzX42XCOTF4lA9Jf8ePVNgokQAyvlGK3j3K0y0d7lTTR5I/Zw== +xterm-addon-canvas@0.2.0-beta.26: + version "0.2.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0-beta.26.tgz#db6d134177bac58d24e02d11c123f0cefb0e95b9" + integrity sha512-OZctolm/iUjSG11iYERJSu9ax2GBXe96ASYcHfJAeq19IMHadQvD3AWaJl25/MMChmvJ0qT1Q/+6p0ElgfV77Q== -xterm-addon-search@0.10.0-beta.6: - version "0.10.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.6.tgz#a475d793a13b378f56b439b8c7eeeff2095831ae" - integrity sha512-fDS0dbM/ZuVBfieWyXJgFvQwNk95rpVbaBRcVpUM9sM/R5+ePQr+uhcaicfuWAku7urP7P/QNnkeAkeQjf8E6w== +xterm-addon-search@0.10.0-beta.7: + version "0.10.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.7.tgz#77812514c4aa84668d9e247a9172618ccd2517d7" + integrity sha512-58dFGbLQc3C0Iww/Jq65HcXC9/RL+57duY5+rijts6KBZqAlGQCN3f2ORFKRvJEQDTgxOcnK9o9welyKK+PQ3Q== -xterm-addon-serialize@0.8.0-beta.6: - version "0.8.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.6.tgz#fe21a74a0ca3ecdf12843136115f074a431b3876" - integrity sha512-hb3TRqvg36MW5H4ZnYjw4EHb55iZ4rOOuH+Hx4ZTBDI1pszPtryFqXbS93NBLKgsOqDovIDsH8fWvNfhPdGmsQ== +xterm-addon-serialize@0.8.0-beta.7: + version "0.8.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.7.tgz#73a71834a687c825ff3d3c824229fbd856d1570f" + integrity sha512-cghmB/2DYwX4HvjGMWmbxYO3NrvgfYWrQt0QGb0oToZh1gOgoEkUxZVZiOl5WlqFYpI+jHXXX48XgfFONZ1rMA== xterm-addon-unicode11@0.4.0-beta.5: version "0.4.0-beta.5" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.5.tgz#3900e66f10d2e506133b61d7421aab6878d32665" integrity sha512-+g+fuxAd/tkCEJ/jhdnebXKtdPrhsu4VKWNnB/3qM35GbuGQOasmYFYnKL+HYZMpbQ6YqeZcXTVg/wnCTttz0g== -xterm-addon-webgl@0.13.0-beta.49: - version "0.13.0-beta.49" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.49.tgz#0cbffccccec06f5638ddc793ae7a0ff8ce0a891b" - integrity sha512-c1/8hLrw3PuPAnyPVLNg8i2FDkyu5SkU654DPEEgKgHHeAh3sfil28LleBpPhpP24531i7XNt1LLHCGMJ+gkFw== +xterm-addon-webgl@0.13.0-beta.55: + version "0.13.0-beta.55" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.55.tgz#d116fbb8d2e2bbfa562876f90d1aeedf48cc7eb8" + integrity sha512-i595z+lcbJaxLM7WTk845440lfyc3RERn/yWqTql+gnoA1YoP3gAnl/qdluFrKndM8sQGWmCsz9qACANXRjLbA== -xterm-headless@5.0.0-beta.5: - version "5.0.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.0.0-beta.5.tgz#e29b6c5081f31f887122b7263ba996b0c46b3c22" - integrity sha512-CMQ1+prBNF92oBMeZzc2rfTcmOaCGfwwSaoPYNTjyziZT6mZsEg7amajYkb0YAnqJ29MFm4kPGZbU78/dX4k2A== +xterm-headless@4.20.0-beta.74: + version "4.20.0-beta.74" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.74.tgz#1eade8cdfbf4389cadf0ae8b8b2cb536862323d2" + integrity sha512-WwHcSrnHGbqcRKJTDJgEJT4y4X5KPJxcMbi5RGj/T1FoXg/uYU23DO1RtvJV8ZnRKLbcY/Ru0wWf7ZGDrEk1DA== -xterm@5.0.0-beta.54: - version "5.0.0-beta.54" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.54.tgz#2c353221f289af22327aae6318bc6422c636fd41" - integrity sha512-wRzs1NbVCkZUzAqvglQcDVreT7RLLFkpdBi0oOLbZXgTaYr/Be93aCuuEjOVp7lnV0hi1gEP5K9Ugn621QffNw== +xterm@5.0.0-beta.60: + version "5.0.0-beta.60" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.60.tgz#1d16d6828f125c18c6d6e7db8769d1d848707a35" + integrity sha512-wkMXXfmwF9jIBtjSoEy7nyh54lDJz4wE0CuYzyBP/cjbTnjAkheeZcY9cJBlDRtP4NoZ7EhsA9GyXNeIrviiJg== y18n@^3.2.1: version "3.2.2" |