From 9c91758ac5d7fed9046b0fb5b408ca201b1a055c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 26 Aug 2025 22:05:52 -0700 Subject: [PATCH 1/8] Tweak userDescription for searchExtensions tool (#263530) --- .../workbench/contrib/extensions/common/searchExtensionsTool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index c19309b4698c0..4d803b9f6f789 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -21,7 +21,7 @@ export const SearchExtensionsToolData: IToolData = { icon: ThemeIcon.fromId(Codicon.extensions.id), displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), - userDescription: localize('searchExtensionsTool.userDescription', 'Search for extensions in the Visual Studio Code Extensions Marketplace'), + userDescription: localize('searchExtensionsTool.userDescription', 'Search for VS Code extensions'), source: ToolDataSource.Internal, inputSchema: { type: 'object', From c54c058bf2b5ab39f536fb44255dee12a8367b90 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 27 Aug 2025 07:06:31 +0200 Subject: [PATCH 2/8] chat - adopt new generic icons for warning/error (#263532) --- src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 1f79a9b0765f2..28f5c046de8cb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1577,11 +1577,11 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben if (signedOut) { primaryActionId = CHAT_SETUP_ACTION_ID; primaryActionTitle = localize('signInToChatSetup', "Sign in to use Copilot..."); - primaryActionIcon = Codicon.copilotNotConnected; + primaryActionIcon = Codicon.chatSparkleError; } else if (chatQuotaExceeded && free) { primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; primaryActionTitle = localize('chatQuotaExceededButton', "Copilot Free plan chat messages quota reached. Click for details."); - primaryActionIcon = Codicon.copilotWarning; + primaryActionIcon = Codicon.chatSparkleWarning; } } return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, instantiationService.createInstance(MenuItemAction, { From aab0a5930ada23b385d9aa41651e9e85eed887c6 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 26 Aug 2025 22:48:07 -0700 Subject: [PATCH 3/8] clean up todo tool and update telemetry (#263510) --- .../chat/common/tools/manageTodoListTool.ts | 166 +++++++++--------- 1 file changed, 87 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts index 04b43f81eacf4..1799d71692201 100644 --- a/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts @@ -122,97 +122,31 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { this.logService.debug(`ManageTodoListTool: Invoking with options ${JSON.stringify(args)}`); try { + // Determine operation: in writeOnly mode, always write; otherwise use args.operation + const operation = this.writeOnly ? 'write' : args.operation; - // In write-only mode, we always perform a write operation - if (this.writeOnly && !args.chatSessionId) { - if (!args.todoList) { - return { - content: [{ - kind: 'text', - value: 'Error: todoList is required for write operation' - }] - }; - } - - const todoList: IChatTodo[] = args.todoList.map((parsedTodo) => ({ - id: parsedTodo.id, - title: parsedTodo.title, - description: parsedTodo.description, - status: parsedTodo.status - })); - this.chatTodoListService.setTodos(chatSessionId, todoList); + if (!operation) { return { content: [{ kind: 'text', - value: 'Successfully wrote todo list' + value: 'Error: operation parameter is required' }] }; } - // Regular mode: check operation parameter - const operation = args.operation; - if (operation === undefined) { + if (operation === 'read') { + return this.handleReadOperation(chatSessionId); + } else if (operation === 'write') { + return this.handleWriteOperation(args, chatSessionId); + } else { return { content: [{ kind: 'text', - value: 'Error: operation parameter is required' + value: 'Error: Unknown operation' }] }; } - switch (operation) { - case 'read': { - const todoItems = this.chatTodoListService.getTodos(chatSessionId); - const readResult = this.handleRead(todoItems, chatSessionId); - this.telemetryService.publicLog2( - 'todoListToolInvoked', - { - operation: 'read', - todoItemCount: todoItems.length, - chatSessionId: chatSessionId - } - ); - return { - content: [{ - kind: 'text', - value: readResult - }] - }; - } - case 'write': { - const todoList: IChatTodo[] = args.todoList.map((parsedTodo) => ({ - id: parsedTodo.id, - title: parsedTodo.title, - description: parsedTodo.description, - status: parsedTodo.status - })); - this.chatTodoListService.setTodos(chatSessionId, todoList); - this.telemetryService.publicLog2( - 'todoListToolInvoked', - { - operation: 'write', - todoItemCount: todoList.length, - chatSessionId: chatSessionId - } - ); - return { - content: [{ - kind: 'text', - value: 'Successfully wrote todo list' - }] - }; - } - default: { - const errorResult = 'Error: Unknown operation'; - return { - content: [{ - kind: 'text', - value: errorResult - }] - }; - } - } - } catch (error) { const errorMessage = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; return { @@ -258,6 +192,76 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { return `# Todo List\n\n${markdownTaskList}`; } + private handleReadOperation(chatSessionId: string): IToolResult { + const todoItems = this.chatTodoListService.getTodos(chatSessionId); + const readResult = this.handleRead(todoItems, chatSessionId); + const statusCounts = this.calculateStatusCounts(todoItems); + + this.telemetryService.publicLog2( + 'todoListToolInvoked', + { + operation: 'read', + notStartedCount: statusCounts.notStartedCount, + inProgressCount: statusCounts.inProgressCount, + completedCount: statusCounts.completedCount, + chatSessionId: chatSessionId + } + ); + + return { + content: [{ + kind: 'text', + value: readResult + }] + }; + } + + private handleWriteOperation(args: IManageTodoListToolInputParams, chatSessionId: string): IToolResult { + if (!args.todoList) { + return { + content: [{ + kind: 'text', + value: 'Error: todoList is required for write operation' + }] + }; + } + + const todoList: IChatTodo[] = args.todoList.map((parsedTodo) => ({ + id: parsedTodo.id, + title: parsedTodo.title, + description: parsedTodo.description, + status: parsedTodo.status + })); + + this.chatTodoListService.setTodos(chatSessionId, todoList); + const statusCounts = this.calculateStatusCounts(todoList); + + this.telemetryService.publicLog2( + 'todoListToolInvoked', + { + operation: 'write', + notStartedCount: statusCounts.notStartedCount, + inProgressCount: statusCounts.inProgressCount, + completedCount: statusCounts.completedCount, + chatSessionId: chatSessionId + } + ); + + return { + content: [{ + kind: 'text', + value: 'Successfully wrote todo list' + }] + }; + } + + private calculateStatusCounts(todos: IChatTodo[]): { notStartedCount: number; inProgressCount: number; completedCount: number } { + const notStartedCount = todos.filter(todo => todo.status === 'not-started').length; + const inProgressCount = todos.filter(todo => todo.status === 'in-progress').length; + const completedCount = todos.filter(todo => todo.status === 'completed').length; + return { notStartedCount, inProgressCount, completedCount }; + } + private formatTodoListAsMarkdownTaskList(todoList: IChatTodo[]): string { if (todoList.length === 0) { return ''; @@ -290,14 +294,18 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { type TodoListToolInvokedEvent = { operation: 'read' | 'write'; - todoItemCount: number; + notStartedCount: number; + inProgressCount: number; + completedCount: number; chatSessionId: string | undefined; }; type TodoListToolInvokedClassification = { operation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The operation performed on the todo list (read or write).' }; - todoItemCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of items in the todo list operation.' }; + notStartedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with not-started status.' }; + inProgressCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with in-progress status.' }; + completedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with completed status.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' }; owner: 'bhavyaus'; - comment: 'Provides insight into the usage of the todo list tool.'; + comment: 'Provides insight into the usage of the todo list tool including detailed task status distribution.'; }; From 7947180d11a01e7c6517904790a28fc0b57855e7 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 26 Aug 2025 23:23:06 -0700 Subject: [PATCH 4/8] Update welcome view rendering (#263536) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 8331f8faab005..1f566e4131603 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -592,6 +592,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.createInput(this.container, { renderFollowups, renderStyle }); } + this.renderWelcomeViewContentIfNeeded(); this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle }); const scrollDownButton = this._register(new Button(this.listContainer, { From 4a31639f5f6e1ba6f2521ca73430f3d59eb97524 Mon Sep 17 00:00:00 2001 From: Robo Date: Wed, 27 Aug 2025 16:38:20 +0900 Subject: [PATCH 5/8] ci: make artifact name unique for cpuprofile (#263543) --- build/azure-pipelines/win32/product-build-win32.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 5063d429d99ce..2e7a18b8f5cef 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -321,7 +321,7 @@ steps: - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: .build/node-cpuprofile - artifactName: node-cpuprofile-$(VSCODE_ARCH) + artifactName: $(ARTIFACT_PREFIX)node-cpuprofile-$(VSCODE_ARCH) sbomEnabled: false condition: succeededOrFailed() displayName: Publish Codesign cpu profile From 5cd72cdafec61c0550f26700a9cdaac2173a642a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:02:07 +0000 Subject: [PATCH 6/8] Bump actions/checkout from 4 to 5 (#263345) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/monaco-editor.yml | 2 +- .github/workflows/pr-darwin-test.yml | 2 +- .github/workflows/pr-linux-cli-test.yml | 2 +- .github/workflows/pr-linux-test.yml | 2 +- .github/workflows/pr-node-modules.yml | 8 ++++---- .github/workflows/pr-win32-test.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/telemetry.yml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index 56c30d0ba7428..95ed1811177e3 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index a6f6912cd39b1..655c8e9e6c317 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: arm64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/pr-linux-cli-test.yml b/.github/workflows/pr-linux-cli-test.yml index 7466c639cae8d..1b9a52a821e72 100644 --- a/.github/workflows/pr-linux-cli-test.yml +++ b/.github/workflows/pr-linux-cli-test.yml @@ -16,7 +16,7 @@ jobs: RUSTUP_TOOLCHAIN: ${{ inputs.rustup_toolchain }} steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Rust run: | diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 323f72348a919..593c6ad9f4d62 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 39ec42c0cb4f1..a8dfc6a22e7f0 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -13,7 +13,7 @@ jobs: runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 @@ -94,7 +94,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 @@ -168,7 +168,7 @@ jobs: VSCODE_ARCH: arm64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 @@ -231,7 +231,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index a4bc311b787c2..d040a43a1b58f 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b43eff9f41da4..f3568b598732d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,7 +21,7 @@ jobs: runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index 84a2ffaaf9360..1f43144f1dccd 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -7,7 +7,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - - uses: 'actions/checkout@v4' + - uses: 'actions/checkout@v5' with: persist-credentials: false From 6e776feea2506c36e346d4086e74ec95fa6779d1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:16:33 +0000 Subject: [PATCH 7/8] Engineering - update GitHub actions code owners (#263549) --- .github/CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7f2aca6110795..c669df3815559 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,13 +1,13 @@ # GitHub actions required reviewers .github/workflows/monaco-editor.yml @hediet @alexdima @lszomoru @joaomoreno -.github/workflows/no-package-lock-changes.yml @lszomoru @joaomoreno -.github/workflows/no-yarn-lock-changes.yml @lszomoru @joaomoreno -.github/workflows/pr-darwin-test.yml @lszomoru @joaomoreno -.github/workflows/pr-linux-cli-test.yml @lszomoru @joaomoreno -.github/workflows/pr-linux-test.yml @lszomoru @joaomoreno -.github/workflows/pr-node-modules.yml @lszomoru @joaomoreno -.github/workflows/pr-win32-test.yml @lszomoru @joaomoreno @TylerLeonhardt -.github/workflows/pr.yml @lszomoru @joaomoreno +.github/workflows/no-package-lock-changes.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/no-yarn-lock-changes.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr-darwin-test.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr-linux-cli-test.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr-linux-test.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr-node-modules.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr-win32-test.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 +.github/workflows/pr.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 .github/workflows/telemetry.yml @lramos15 @lszomoru @joaomoreno src/vs/base/browser/ui/tree/** @joaomoreno @benibenj src/vs/base/browser/ui/list/** @joaomoreno @benibenj From d793cce20e8a5ed7a04d603c20913eb0fe66c07f Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 27 Aug 2025 10:57:08 +0200 Subject: [PATCH 8/8] Extract some types to be able to reuse them in copilot chat (#263553) * Extract some types to be able to reuse them in copilot chat * Avoid exporting interfaces which trips up the transpiler used for speeding up the CI --- src/vs/workbench/api/common/extHostTypes.ts | 1481 ++--------------- .../api/common/extHostTypes/codeActionKind.ts | 50 + .../api/common/extHostTypes/diagnostic.ts | 107 ++ .../api/common/extHostTypes/es5ClassCompat.ts | 32 + .../api/common/extHostTypes/location.ts | 49 + .../api/common/extHostTypes/markdownString.ts | 81 + .../api/common/extHostTypes/notebooks.ts | 176 ++ .../api/common/extHostTypes/position.ts | 193 +++ .../api/common/extHostTypes/range.ts | 165 ++ .../api/common/extHostTypes/selection.ts | 92 + .../api/common/extHostTypes/snippetString.ts | 101 ++ .../common/extHostTypes/snippetTextEdit.ts | 42 + .../api/common/extHostTypes/textEdit.ts | 97 ++ .../api/common/extHostTypes/workspaceEdit.ts | 221 +++ 14 files changed, 1510 insertions(+), 1377 deletions(-) create mode 100644 src/vs/workbench/api/common/extHostTypes/codeActionKind.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/diagnostic.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/es5ClassCompat.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/location.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/markdownString.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/notebooks.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/position.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/range.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/selection.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/snippetString.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/snippetTextEdit.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/textEdit.ts create mode 100644 src/vs/workbench/api/common/extHostTypes/workspaceEdit.ts diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4e0b9ad429aca..df4760ccde24c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -6,12 +6,10 @@ /* eslint-disable local/code-no-native-private */ import type * as vscode from 'vscode'; -import { asArray, coalesceInPlace, equals } from '../../../base/common/arrays.js'; +import { asArray } from '../../../base/common/arrays.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { illegalArgument, SerializedError } from '../../../base/common/errors.js'; import { IRelativePattern } from '../../../base/common/glob.js'; -import { MarkdownString as BaseMarkdownString, MarkdownStringTrustedOptions } from '../../../base/common/htmlContent.js'; -import { ResourceMap } from '../../../base/common/map.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { Mimes, normalizeMimeType } from '../../../base/common/mime.js'; import { nextCharLength } from '../../../base/common/strings.js'; @@ -22,36 +20,37 @@ import { TextEditorSelectionSource } from '../../../platform/editor/common/edito import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from '../../../platform/files/common/files.js'; import { RemoteAuthorityResolverErrorCode } from '../../../platform/remote/common/remoteAuthorityResolver.js'; -import { CellEditType, ICellMetadataEdit, IDocumentMetadataEdit, isTextStreamMime } from '../../contrib/notebook/common/notebookCommon.js'; +import { isTextStreamMime } from '../../contrib/notebook/common/notebookCommon.js'; import { IRelativePatternDto } from './extHost.protocol.js'; - -/** - * @deprecated - * - * This utility ensures that old JS code that uses functions for classes still works. Existing usages cannot be removed - * but new ones must not be added - * */ -function es5ClassCompat(target: Function): any { - const interceptFunctions = { - apply: function (...args: any[]): any { - if (args.length === 0) { - return Reflect.construct(target, []); - } else { - const argsList = args.length === 1 ? [] : args[1]; - return Reflect.construct(target, argsList, args[0].constructor); - } - }, - call: function (...args: any[]): any { - if (args.length === 0) { - return Reflect.construct(target, []); - } else { - const [thisArg, ...restArgs] = args; - return Reflect.construct(target, restArgs, thisArg.constructor); - } - } - }; - return Object.assign(target, interceptFunctions); -} +import { Position } from './extHostTypes/position.js'; +import { es5ClassCompat } from './extHostTypes/es5ClassCompat.js'; +import { Range } from './extHostTypes/range.js'; +import { CodeActionKind } from './extHostTypes/codeActionKind.js'; +import { Location } from './extHostTypes/location.js'; +import { Diagnostic } from './extHostTypes/diagnostic.js'; +import { TextEdit } from './extHostTypes/textEdit.js'; +import { WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; +import { SnippetString } from './extHostTypes/snippetString.js'; +import { MarkdownString } from './extHostTypes/markdownString.js'; + +export { Position } from './extHostTypes/position.js'; +export { Range } from './extHostTypes/range.js'; +export { Selection } from './extHostTypes/selection.js'; +export { CodeActionKind } from './extHostTypes/codeActionKind.js'; +export { Location } from './extHostTypes/location.js'; +export { + Diagnostic, DiagnosticRelatedInformation, + DiagnosticSeverity, DiagnosticTag +} from './extHostTypes/diagnostic.js'; +export { EndOfLine, TextEdit } from './extHostTypes/textEdit.js'; +export { FileEditType, WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; +export { SnippetString } from './extHostTypes/snippetString.js'; +export { SnippetTextEdit } from './extHostTypes/snippetTextEdit.js'; +export { + NotebookCellKind, NotebookRange, NotebookCellData, + NotebookData, NotebookEdit +} from './extHostTypes/notebooks.js'; +export { MarkdownString } from './extHostTypes/markdownString.js'; export enum TerminalOutputAnchor { Top = 0, @@ -65,1155 +64,120 @@ export enum TerminalQuickFixType { } @es5ClassCompat -export class Disposable { - - static from(...inDisposables: { dispose(): any }[]): Disposable { - let disposables: ReadonlyArray<{ dispose(): any }> | undefined = inDisposables; - return new Disposable(function () { - if (disposables) { - for (const disposable of disposables) { - if (disposable && typeof disposable.dispose === 'function') { - disposable.dispose(); - } - } - disposables = undefined; - } - }); - } - - #callOnDispose?: () => any; - - constructor(callOnDispose: () => any) { - this.#callOnDispose = callOnDispose; - } - - dispose(): any { - if (typeof this.#callOnDispose === 'function') { - this.#callOnDispose(); - this.#callOnDispose = undefined; - } - } -} - -@es5ClassCompat -export class Position { - - static Min(...positions: Position[]): Position { - if (positions.length === 0) { - throw new TypeError(); - } - let result = positions[0]; - for (let i = 1; i < positions.length; i++) { - const p = positions[i]; - if (p.isBefore(result)) { - result = p; - } - } - return result; - } - - static Max(...positions: Position[]): Position { - if (positions.length === 0) { - throw new TypeError(); - } - let result = positions[0]; - for (let i = 1; i < positions.length; i++) { - const p = positions[i]; - if (p.isAfter(result)) { - result = p; - } - } - return result; - } - - static isPosition(other: any): other is Position { - if (!other) { - return false; - } - if (other instanceof Position) { - return true; - } - const { line, character } = other; - if (typeof line === 'number' && typeof character === 'number') { - return true; - } - return false; - } - - static of(obj: vscode.Position): Position { - if (obj instanceof Position) { - return obj; - } else if (this.isPosition(obj)) { - return new Position(obj.line, obj.character); - } - throw new Error('Invalid argument, is NOT a position-like object'); - } - - private _line: number; - private _character: number; - - get line(): number { - return this._line; - } - - get character(): number { - return this._character; - } - - constructor(line: number, character: number) { - if (line < 0) { - throw illegalArgument('line must be non-negative'); - } - if (character < 0) { - throw illegalArgument('character must be non-negative'); - } - this._line = line; - this._character = character; - } - - isBefore(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character < other._character; - } - - isBeforeOrEqual(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character <= other._character; - } - - isAfter(other: Position): boolean { - return !this.isBeforeOrEqual(other); - } - - isAfterOrEqual(other: Position): boolean { - return !this.isBefore(other); - } - - isEqual(other: Position): boolean { - return this._line === other._line && this._character === other._character; - } - - compareTo(other: Position): number { - if (this._line < other._line) { - return -1; - } else if (this._line > other.line) { - return 1; - } else { - // equal line - if (this._character < other._character) { - return -1; - } else if (this._character > other._character) { - return 1; - } else { - // equal line and character - return 0; - } - } - } - - translate(change: { lineDelta?: number; characterDelta?: number }): Position; - translate(lineDelta?: number, characterDelta?: number): Position; - translate(lineDeltaOrChange: number | undefined | { lineDelta?: number; characterDelta?: number }, characterDelta: number = 0): Position { - - if (lineDeltaOrChange === null || characterDelta === null) { - throw illegalArgument(); - } - - let lineDelta: number; - if (typeof lineDeltaOrChange === 'undefined') { - lineDelta = 0; - } else if (typeof lineDeltaOrChange === 'number') { - lineDelta = lineDeltaOrChange; - } else { - lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0; - characterDelta = typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0; - } - - if (lineDelta === 0 && characterDelta === 0) { - return this; - } - return new Position(this.line + lineDelta, this.character + characterDelta); - } - - with(change: { line?: number; character?: number }): Position; - with(line?: number, character?: number): Position; - with(lineOrChange: number | undefined | { line?: number; character?: number }, character: number = this.character): Position { - - if (lineOrChange === null || character === null) { - throw illegalArgument(); - } - - let line: number; - if (typeof lineOrChange === 'undefined') { - line = this.line; - - } else if (typeof lineOrChange === 'number') { - line = lineOrChange; - - } else { - line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; - character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; - } - - if (line === this.line && character === this.character) { - return this; - } - return new Position(line, character); - } - - toJSON(): any { - return { line: this.line, character: this.character }; - } - - [Symbol.for('debug.description')]() { - return `(${this.line}:${this.character})`; - } -} - -@es5ClassCompat -export class Range { - - static isRange(thing: any): thing is vscode.Range { - if (thing instanceof Range) { - return true; - } - if (!thing) { - return false; - } - return Position.isPosition((thing).start) - && Position.isPosition((thing.end)); - } - - static of(obj: vscode.Range): Range { - if (obj instanceof Range) { - return obj; - } - if (this.isRange(obj)) { - return new Range(obj.start, obj.end); - } - throw new Error('Invalid argument, is NOT a range-like object'); - } - - protected _start: Position; - protected _end: Position; - - get start(): Position { - return this._start; - } - - get end(): Position { - return this._end; - } - - constructor(start: vscode.Position, end: vscode.Position); - constructor(start: Position, end: Position); - constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); - constructor(startLineOrStart: number | Position | vscode.Position, startColumnOrEnd: number | Position | vscode.Position, endLine?: number, endColumn?: number) { - let start: Position | undefined; - let end: Position | undefined; - - if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { - start = new Position(startLineOrStart, startColumnOrEnd); - end = new Position(endLine, endColumn); - } else if (Position.isPosition(startLineOrStart) && Position.isPosition(startColumnOrEnd)) { - start = Position.of(startLineOrStart); - end = Position.of(startColumnOrEnd); - } - - if (!start || !end) { - throw new Error('Invalid arguments'); - } - - if (start.isBefore(end)) { - this._start = start; - this._end = end; - } else { - this._start = end; - this._end = start; - } - } - - contains(positionOrRange: Position | Range): boolean { - if (Range.isRange(positionOrRange)) { - return this.contains(positionOrRange.start) - && this.contains(positionOrRange.end); - - } else if (Position.isPosition(positionOrRange)) { - if (Position.of(positionOrRange).isBefore(this._start)) { - return false; - } - if (this._end.isBefore(positionOrRange)) { - return false; - } - return true; - } - return false; - } - - isEqual(other: Range): boolean { - return this._start.isEqual(other._start) && this._end.isEqual(other._end); - } - - intersection(other: Range): Range | undefined { - const start = Position.Max(other.start, this._start); - const end = Position.Min(other.end, this._end); - if (start.isAfter(end)) { - // this happens when there is no overlap: - // |-----| - // |----| - return undefined; - } - return new Range(start, end); - } - - union(other: Range): Range { - if (this.contains(other)) { - return this; - } else if (other.contains(this)) { - return other; - } - const start = Position.Min(other.start, this._start); - const end = Position.Max(other.end, this.end); - return new Range(start, end); - } - - get isEmpty(): boolean { - return this._start.isEqual(this._end); - } - - get isSingleLine(): boolean { - return this._start.line === this._end.line; - } - - with(change: { start?: Position; end?: Position }): Range; - with(start?: Position, end?: Position): Range; - with(startOrChange: Position | undefined | { start?: Position; end?: Position }, end: Position = this.end): Range { - - if (startOrChange === null || end === null) { - throw illegalArgument(); - } - - let start: Position; - if (!startOrChange) { - start = this.start; - - } else if (Position.isPosition(startOrChange)) { - start = startOrChange; - - } else { - start = startOrChange.start || this.start; - end = startOrChange.end || this.end; - } - - if (start.isEqual(this._start) && end.isEqual(this.end)) { - return this; - } - return new Range(start, end); - } - - toJSON(): any { - return [this.start, this.end]; - } - - [Symbol.for('debug.description')]() { - return getDebugDescriptionOfRange(this); - } -} - -@es5ClassCompat -export class Selection extends Range { - - static isSelection(thing: any): thing is Selection { - if (thing instanceof Selection) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange(thing) - && Position.isPosition((thing).anchor) - && Position.isPosition((thing).active) - && typeof (thing).isReversed === 'boolean'; - } - - private _anchor: Position; - - public get anchor(): Position { - return this._anchor; - } - - private _active: Position; - - public get active(): Position { - return this._active; - } - - constructor(anchor: Position, active: Position); - constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); - constructor(anchorLineOrAnchor: number | Position, anchorColumnOrActive: number | Position, activeLine?: number, activeColumn?: number) { - let anchor: Position | undefined; - let active: Position | undefined; - - if (typeof anchorLineOrAnchor === 'number' && typeof anchorColumnOrActive === 'number' && typeof activeLine === 'number' && typeof activeColumn === 'number') { - anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive); - active = new Position(activeLine, activeColumn); - } else if (Position.isPosition(anchorLineOrAnchor) && Position.isPosition(anchorColumnOrActive)) { - anchor = Position.of(anchorLineOrAnchor); - active = Position.of(anchorColumnOrActive); - } - - if (!anchor || !active) { - throw new Error('Invalid arguments'); - } - - super(anchor, active); - - this._anchor = anchor; - this._active = active; - } - - get isReversed(): boolean { - return this._anchor === this._end; - } - - override toJSON() { - return { - start: this.start, - end: this.end, - active: this.active, - anchor: this.anchor - }; - } - - - [Symbol.for('debug.description')]() { - return getDebugDescriptionOfSelection(this); - } -} - -export function getDebugDescriptionOfRange(range: vscode.Range): string { - return range.isEmpty - ? `[${range.start.line}:${range.start.character})` - : `[${range.start.line}:${range.start.character} -> ${range.end.line}:${range.end.character})`; -} - -export function getDebugDescriptionOfSelection(selection: vscode.Selection): string { - let rangeStr = getDebugDescriptionOfRange(selection); - if (!selection.isEmpty) { - if (selection.active.isEqual(selection.start)) { - rangeStr = `|${rangeStr}`; - } else { - rangeStr = `${rangeStr}|`; - } - } - return rangeStr; -} - -const validateConnectionToken = (connectionToken: string) => { - if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) { - throw illegalArgument('connectionToken'); - } -}; - - -export class ResolvedAuthority { - public static isResolvedAuthority(resolvedAuthority: any): resolvedAuthority is ResolvedAuthority { - return resolvedAuthority - && typeof resolvedAuthority === 'object' - && typeof resolvedAuthority.host === 'string' - && typeof resolvedAuthority.port === 'number' - && (resolvedAuthority.connectionToken === undefined || typeof resolvedAuthority.connectionToken === 'string'); - } - - readonly host: string; - readonly port: number; - readonly connectionToken: string | undefined; - - constructor(host: string, port: number, connectionToken?: string) { - if (typeof host !== 'string' || host.length === 0) { - throw illegalArgument('host'); - } - if (typeof port !== 'number' || port === 0 || Math.round(port) !== port) { - throw illegalArgument('port'); - } - if (typeof connectionToken !== 'undefined') { - validateConnectionToken(connectionToken); - } - this.host = host; - this.port = Math.round(port); - this.connectionToken = connectionToken; - } -} - - -export class ManagedResolvedAuthority { - - public static isManagedResolvedAuthority(resolvedAuthority: any): resolvedAuthority is ManagedResolvedAuthority { - return resolvedAuthority - && typeof resolvedAuthority === 'object' - && typeof resolvedAuthority.makeConnection === 'function' - && (resolvedAuthority.connectionToken === undefined || typeof resolvedAuthority.connectionToken === 'string'); - } - - constructor(public readonly makeConnection: () => Thenable, public readonly connectionToken?: string) { - if (typeof connectionToken !== 'undefined') { - validateConnectionToken(connectionToken); - } - } -} - -export class RemoteAuthorityResolverError extends Error { - - static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError { - return new RemoteAuthorityResolverError(message, RemoteAuthorityResolverErrorCode.NotAvailable, handled); - } - - static TemporarilyNotAvailable(message?: string): RemoteAuthorityResolverError { - return new RemoteAuthorityResolverError(message, RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable); - } - - public readonly _message: string | undefined; - public readonly _code: RemoteAuthorityResolverErrorCode; - public readonly _detail: unknown; - - constructor(message?: string, code: RemoteAuthorityResolverErrorCode = RemoteAuthorityResolverErrorCode.Unknown, detail?: unknown) { - super(message); - - this._message = message; - this._code = code; - this._detail = detail; - - // workaround when extending builtin objects and when compiling to ES5, see: - // https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, RemoteAuthorityResolverError.prototype); - } -} - -export enum EndOfLine { - LF = 1, - CRLF = 2 -} - -export enum EnvironmentVariableMutatorType { - Replace = 1, - Append = 2, - Prepend = 3 -} - -@es5ClassCompat -export class TextEdit { - - static isTextEdit(thing: any): thing is TextEdit { - if (thing instanceof TextEdit) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((thing)) - && typeof (thing).newText === 'string'; - } - - static replace(range: Range, newText: string): TextEdit { - return new TextEdit(range, newText); - } - - static insert(position: Position, newText: string): TextEdit { - return TextEdit.replace(new Range(position, position), newText); - } - - static delete(range: Range): TextEdit { - return TextEdit.replace(range, ''); - } - - static setEndOfLine(eol: EndOfLine): TextEdit { - const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); - ret.newEol = eol; - return ret; - } - - protected _range: Range; - protected _newText: string | null; - protected _newEol?: EndOfLine; - - get range(): Range { - return this._range; - } - - set range(value: Range) { - if (value && !Range.isRange(value)) { - throw illegalArgument('range'); - } - this._range = value; - } - - get newText(): string { - return this._newText || ''; - } - - set newText(value: string) { - if (value && typeof value !== 'string') { - throw illegalArgument('newText'); - } - this._newText = value; - } - - get newEol(): EndOfLine | undefined { - return this._newEol; - } - - set newEol(value: EndOfLine | undefined) { - if (value && typeof value !== 'number') { - throw illegalArgument('newEol'); - } - this._newEol = value; - } - - constructor(range: Range, newText: string | null) { - this._range = range; - this._newText = newText; - } - - toJSON(): any { - return { - range: this.range, - newText: this.newText, - newEol: this._newEol - }; - } -} - -@es5ClassCompat -export class NotebookEdit implements vscode.NotebookEdit { - - static isNotebookCellEdit(thing: any): thing is NotebookEdit { - if (thing instanceof NotebookEdit) { - return true; - } - if (!thing) { - return false; - } - return NotebookRange.isNotebookRange((thing)) - && Array.isArray((thing).newCells); - } - - static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit { - return new NotebookEdit(range, newCells); - } - - static insertCells(index: number, newCells: vscode.NotebookCellData[]): vscode.NotebookEdit { - return new NotebookEdit(new NotebookRange(index, index), newCells); - } - - static deleteCells(range: NotebookRange): NotebookEdit { - return new NotebookEdit(range, []); - } - - static updateCellMetadata(index: number, newMetadata: { [key: string]: any }): NotebookEdit { - const edit = new NotebookEdit(new NotebookRange(index, index), []); - edit.newCellMetadata = newMetadata; - return edit; - } - - static updateNotebookMetadata(newMetadata: { [key: string]: any }): NotebookEdit { - const edit = new NotebookEdit(new NotebookRange(0, 0), []); - edit.newNotebookMetadata = newMetadata; - return edit; - } - - range: NotebookRange; - newCells: NotebookCellData[]; - newCellMetadata?: { [key: string]: any }; - newNotebookMetadata?: { [key: string]: any }; - - constructor(range: NotebookRange, newCells: NotebookCellData[]) { - this.range = range; - this.newCells = newCells; - } -} - -export class SnippetTextEdit implements vscode.SnippetTextEdit { - - static isSnippetTextEdit(thing: any): thing is SnippetTextEdit { - if (thing instanceof SnippetTextEdit) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((thing).range) - && SnippetString.isSnippetString((thing).snippet); - } - - static replace(range: Range, snippet: SnippetString): SnippetTextEdit { - return new SnippetTextEdit(range, snippet); - } - - static insert(position: Position, snippet: SnippetString): SnippetTextEdit { - return SnippetTextEdit.replace(new Range(position, position), snippet); - } - - range: Range; - - snippet: SnippetString; - - keepWhitespace?: boolean; - - constructor(range: Range, snippet: SnippetString) { - this.range = range; - this.snippet = snippet; - } -} - -export interface IFileOperationOptions { - readonly overwrite?: boolean; - readonly ignoreIfExists?: boolean; - readonly ignoreIfNotExists?: boolean; - readonly recursive?: boolean; - readonly contents?: Uint8Array | vscode.DataTransferFile; -} - -export const enum FileEditType { - File = 1, - Text = 2, - Cell = 3, - CellReplace = 5, - Snippet = 6, -} - -export interface IFileOperation { - readonly _type: FileEditType.File; - readonly from?: URI; - readonly to?: URI; - readonly options?: IFileOperationOptions; - readonly metadata?: vscode.WorkspaceEditEntryMetadata; -} - -export interface IFileTextEdit { - readonly _type: FileEditType.Text; - readonly uri: URI; - readonly edit: TextEdit; - readonly metadata?: vscode.WorkspaceEditEntryMetadata; -} - -export interface IFileSnippetTextEdit { - readonly _type: FileEditType.Snippet; - readonly uri: URI; - readonly range: vscode.Range; - readonly edit: vscode.SnippetString; - readonly metadata?: vscode.WorkspaceEditEntryMetadata; - readonly keepWhitespace?: boolean; -} - -export interface IFileCellEdit { - readonly _type: FileEditType.Cell; - readonly uri: URI; - readonly edit?: ICellMetadataEdit | IDocumentMetadataEdit; - readonly metadata?: vscode.WorkspaceEditEntryMetadata; -} - -export interface ICellEdit { - readonly _type: FileEditType.CellReplace; - readonly metadata?: vscode.WorkspaceEditEntryMetadata; - readonly uri: URI; - readonly index: number; - readonly count: number; - readonly cells: vscode.NotebookCellData[]; -} - - -type WorkspaceEditEntry = IFileOperation | IFileTextEdit | IFileSnippetTextEdit | IFileCellEdit | ICellEdit; - -@es5ClassCompat -export class WorkspaceEdit implements vscode.WorkspaceEdit { - - private readonly _edits: WorkspaceEditEntry[] = []; - - - _allEntries(): ReadonlyArray { - return this._edits; - } - - // --- file - - renameFile(from: vscode.Uri, to: vscode.Uri, options?: { readonly overwrite?: boolean; readonly ignoreIfExists?: boolean }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.File, from, to, options, metadata }); - } - - createFile(uri: vscode.Uri, options?: { readonly overwrite?: boolean; readonly ignoreIfExists?: boolean; readonly contents?: Uint8Array | vscode.DataTransferFile }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }); - } - - deleteFile(uri: vscode.Uri, options?: { readonly recursive?: boolean; readonly ignoreIfNotExists?: boolean }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); - } - - // --- notebook - - private replaceNotebookMetadata(uri: URI, value: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: value } }); - } - - private replaceNotebookCells(uri: URI, startOrRange: vscode.NotebookRange, cellData: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { - const start = startOrRange.start; - const end = startOrRange.end; - - if (start !== end || cellData.length > 0) { - this._edits.push({ _type: FileEditType.CellReplace, uri, index: start, count: end - start, cells: cellData, metadata }); - } - } - - private replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); - } - - // --- text - - replace(uri: URI, range: Range, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }); - } - - insert(resource: URI, position: Position, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this.replace(resource, new Range(position, position), newText, metadata); - } - - delete(resource: URI, range: Range, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this.replace(resource, range, '', metadata); - } - - // --- text (Maplike) - - has(uri: URI): boolean { - return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); - } - - set(uri: URI, edits: ReadonlyArray): void; - set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, vscode.WorkspaceEditEntryMetadata | undefined]>): void; - set(uri: URI, edits: readonly NotebookEdit[]): void; - set(uri: URI, edits: ReadonlyArray<[NotebookEdit, vscode.WorkspaceEditEntryMetadata | undefined]>): void; - - set(uri: URI, edits: null | undefined | ReadonlyArray): void { - if (!edits) { - // remove all text, snippet, or notebook edits for `uri` - for (let i = 0; i < this._edits.length; i++) { - const element = this._edits[i]; - switch (element._type) { - case FileEditType.Text: - case FileEditType.Snippet: - case FileEditType.Cell: - case FileEditType.CellReplace: - if (element.uri.toString() === uri.toString()) { - this._edits[i] = undefined!; // will be coalesced down below - } - break; - } - } - coalesceInPlace(this._edits); - } else { - // append edit to the end - for (const editOrTuple of edits) { - if (!editOrTuple) { - continue; - } - let edit: TextEdit | SnippetTextEdit | NotebookEdit; - let metadata: vscode.WorkspaceEditEntryMetadata | undefined; - if (Array.isArray(editOrTuple)) { - edit = editOrTuple[0]; - metadata = editOrTuple[1]; - } else { - edit = editOrTuple; - } - if (NotebookEdit.isNotebookCellEdit(edit)) { - if (edit.newCellMetadata) { - this.replaceNotebookCellMetadata(uri, edit.range.start, edit.newCellMetadata, metadata); - } else if (edit.newNotebookMetadata) { - this.replaceNotebookMetadata(uri, edit.newNotebookMetadata, metadata); - } else { - this.replaceNotebookCells(uri, edit.range, edit.newCells, metadata); - } - } else if (SnippetTextEdit.isSnippetTextEdit(edit)) { - this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata, keepWhitespace: edit.keepWhitespace }); - - } else { - this._edits.push({ _type: FileEditType.Text, uri, edit, metadata }); - } - } - } - } - - get(uri: URI): TextEdit[] { - const res: TextEdit[] = []; - for (const candidate of this._edits) { - if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { - res.push(candidate.edit); - } - } - return res; - } +export class Disposable { - entries(): [URI, TextEdit[]][] { - const textEdits = new ResourceMap<[URI, TextEdit[]]>(); - for (const candidate of this._edits) { - if (candidate._type === FileEditType.Text) { - let textEdit = textEdits.get(candidate.uri); - if (!textEdit) { - textEdit = [candidate.uri, []]; - textEdits.set(candidate.uri, textEdit); + static from(...inDisposables: { dispose(): any }[]): Disposable { + let disposables: ReadonlyArray<{ dispose(): any }> | undefined = inDisposables; + return new Disposable(function () { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } } - textEdit[1].push(candidate.edit); + disposables = undefined; } - } - return [...textEdits.values()]; + }); } - get size(): number { - return this.entries().length; - } + #callOnDispose?: () => any; - toJSON(): any { - return this.entries(); + constructor(callOnDispose: () => any) { + this.#callOnDispose = callOnDispose; } -} - -@es5ClassCompat -export class SnippetString { - static isSnippetString(thing: any): thing is SnippetString { - if (thing instanceof SnippetString) { - return true; - } - if (!thing) { - return false; + dispose(): any { + if (typeof this.#callOnDispose === 'function') { + this.#callOnDispose(); + this.#callOnDispose = undefined; } - return typeof (thing).value === 'string'; - } - - private static _escape(value: string): string { - return value.replace(/\$|}|\\/g, '\\$&'); } +} - private _tabstop: number = 1; - - value: string; - - constructor(value?: string) { - this.value = value || ''; +const validateConnectionToken = (connectionToken: string) => { + if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) { + throw illegalArgument('connectionToken'); } +}; - appendText(string: string): SnippetString { - this.value += SnippetString._escape(string); - return this; - } - appendTabstop(number: number = this._tabstop++): SnippetString { - this.value += '$'; - this.value += number; - return this; +export class ResolvedAuthority { + public static isResolvedAuthority(resolvedAuthority: any): resolvedAuthority is ResolvedAuthority { + return resolvedAuthority + && typeof resolvedAuthority === 'object' + && typeof resolvedAuthority.host === 'string' + && typeof resolvedAuthority.port === 'number' + && (resolvedAuthority.connectionToken === undefined || typeof resolvedAuthority.connectionToken === 'string'); } - appendPlaceholder(value: string | ((snippet: SnippetString) => any), number: number = this._tabstop++): SnippetString { + readonly host: string; + readonly port: number; + readonly connectionToken: string | undefined; - if (typeof value === 'function') { - const nested = new SnippetString(); - nested._tabstop = this._tabstop; - value(nested); - this._tabstop = nested._tabstop; - value = nested.value; - } else { - value = SnippetString._escape(value); + constructor(host: string, port: number, connectionToken?: string) { + if (typeof host !== 'string' || host.length === 0) { + throw illegalArgument('host'); } - - this.value += '${'; - this.value += number; - this.value += ':'; - this.value += value; - this.value += '}'; - - return this; - } - - appendChoice(values: string[], number: number = this._tabstop++): SnippetString { - const value = values.map(s => s.replaceAll(/[|\\,]/g, '\\$&')).join(','); - - this.value += '${'; - this.value += number; - this.value += '|'; - this.value += value; - this.value += '|}'; - - return this; - } - - appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { - - if (typeof defaultValue === 'function') { - const nested = new SnippetString(); - nested._tabstop = this._tabstop; - defaultValue(nested); - this._tabstop = nested._tabstop; - defaultValue = nested.value; - - } else if (typeof defaultValue === 'string') { - defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] I do not want to escape backslashes here + if (typeof port !== 'number' || port === 0 || Math.round(port) !== port) { + throw illegalArgument('port'); } - - this.value += '${'; - this.value += name; - if (defaultValue) { - this.value += ':'; - this.value += defaultValue; + if (typeof connectionToken !== 'undefined') { + validateConnectionToken(connectionToken); } - this.value += '}'; - - - return this; + this.host = host; + this.port = Math.round(port); + this.connectionToken = connectionToken; } } -export enum DiagnosticTag { - Unnecessary = 1, - Deprecated = 2 -} - -export enum DiagnosticSeverity { - Hint = 3, - Information = 2, - Warning = 1, - Error = 0 -} -@es5ClassCompat -export class Location { +export class ManagedResolvedAuthority { - static isLocation(thing: any): thing is vscode.Location { - if (thing instanceof Location) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((thing).range) - && URI.isUri((thing).uri); + public static isManagedResolvedAuthority(resolvedAuthority: any): resolvedAuthority is ManagedResolvedAuthority { + return resolvedAuthority + && typeof resolvedAuthority === 'object' + && typeof resolvedAuthority.makeConnection === 'function' + && (resolvedAuthority.connectionToken === undefined || typeof resolvedAuthority.connectionToken === 'string'); } - uri: URI; - range!: Range; - - constructor(uri: URI, rangeOrPosition: Range | Position) { - this.uri = uri; - - if (!rangeOrPosition) { - //that's OK - } else if (Range.isRange(rangeOrPosition)) { - this.range = Range.of(rangeOrPosition); - } else if (Position.isPosition(rangeOrPosition)) { - this.range = new Range(rangeOrPosition, rangeOrPosition); - } else { - throw new Error('Illegal argument'); + constructor(public readonly makeConnection: () => Thenable, public readonly connectionToken?: string) { + if (typeof connectionToken !== 'undefined') { + validateConnectionToken(connectionToken); } } - - toJSON(): any { - return { - uri: this.uri, - range: this.range - }; - } } -@es5ClassCompat -export class DiagnosticRelatedInformation { +export class RemoteAuthorityResolverError extends Error { - static is(thing: any): thing is DiagnosticRelatedInformation { - if (!thing) { - return false; - } - return typeof (thing).message === 'string' - && (thing).location - && Range.isRange((thing).location.range) - && URI.isUri((thing).location.uri); + static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError { + return new RemoteAuthorityResolverError(message, RemoteAuthorityResolverErrorCode.NotAvailable, handled); } - location: Location; - message: string; - - constructor(location: Location, message: string) { - this.location = location; - this.message = message; + static TemporarilyNotAvailable(message?: string): RemoteAuthorityResolverError { + return new RemoteAuthorityResolverError(message, RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable); } - static isEqual(a: DiagnosticRelatedInformation, b: DiagnosticRelatedInformation): boolean { - if (a === b) { - return true; - } - if (!a || !b) { - return false; - } - return a.message === b.message - && a.location.range.isEqual(b.location.range) - && a.location.uri.toString() === b.location.uri.toString(); - } -} + public readonly _message: string | undefined; + public readonly _code: RemoteAuthorityResolverErrorCode; + public readonly _detail: unknown; -@es5ClassCompat -export class Diagnostic { + constructor(message?: string, code: RemoteAuthorityResolverErrorCode = RemoteAuthorityResolverErrorCode.Unknown, detail?: unknown) { + super(message); - range: Range; - message: string; - severity: DiagnosticSeverity; - source?: string; - code?: string | number; - relatedInformation?: DiagnosticRelatedInformation[]; - tags?: DiagnosticTag[]; - - constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { - if (!Range.isRange(range)) { - throw new TypeError('range must be set'); - } - if (!message) { - throw new TypeError('message must be set'); - } - this.range = range; - this.message = message; - this.severity = severity; - } + this._message = message; + this._code = code; + this._detail = detail; - toJSON(): any { - return { - severity: DiagnosticSeverity[this.severity], - message: this.message, - range: this.range, - source: this.source, - code: this.code, - }; + // workaround when extending builtin objects and when compiling to ES5, see: + // https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, RemoteAuthorityResolverError.prototype); } +} - static isEqual(a: Diagnostic | undefined, b: Diagnostic | undefined): boolean { - if (a === b) { - return true; - } - if (!a || !b) { - return false; - } - return a.message === b.message - && a.severity === b.severity - && a.code === b.code - && a.severity === b.severity - && a.source === b.source - && a.range.isEqual(b.range) - && equals(a.tags, b.tags) - && equals(a.relatedInformation, b.relatedInformation, DiagnosticRelatedInformation.isEqual); - } +export enum EnvironmentVariableMutatorType { + Replace = 1, + Append = 2, + Prepend = 3 } @es5ClassCompat @@ -1442,51 +406,6 @@ export class CodeAction { } } -@es5ClassCompat -export class CodeActionKind { - private static readonly sep = '.'; - - public static Empty: CodeActionKind; - public static QuickFix: 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; - public static SourceFixAll: CodeActionKind; - public static Notebook: CodeActionKind; - - constructor( - public readonly value: string - ) { } - - public append(parts: string): CodeActionKind { - return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); - } - - public intersects(other: CodeActionKind): boolean { - return this.contains(other) || other.contains(this); - } - - public contains(other: CodeActionKind): boolean { - return this.value === other.value || other.value.startsWith(this.value + CodeActionKind.sep); - } -} - -CodeActionKind.Empty = new CodeActionKind(''); -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'); -CodeActionKind.SourceFixAll = CodeActionKind.Source.append('fixAll'); -CodeActionKind.Notebook = CodeActionKind.Empty.append('notebook'); - @es5ClassCompat export class SelectionRange { @@ -1571,77 +490,6 @@ export class CodeLens { } } -@es5ClassCompat -export class MarkdownString implements vscode.MarkdownString { - - readonly #delegate: BaseMarkdownString; - - static isMarkdownString(thing: any): thing is vscode.MarkdownString { - if (thing instanceof MarkdownString) { - return true; - } - return thing && thing.appendCodeblock && thing.appendMarkdown && thing.appendText && (thing.value !== undefined); - } - - constructor(value?: string, supportThemeIcons: boolean = false) { - this.#delegate = new BaseMarkdownString(value, { supportThemeIcons }); - } - - get value(): string { - return this.#delegate.value; - } - set value(value: string) { - this.#delegate.value = value; - } - - get isTrusted(): boolean | MarkdownStringTrustedOptions | undefined { - return this.#delegate.isTrusted; - } - - set isTrusted(value: boolean | MarkdownStringTrustedOptions | undefined) { - this.#delegate.isTrusted = value; - } - - get supportThemeIcons(): boolean | undefined { - return this.#delegate.supportThemeIcons; - } - - set supportThemeIcons(value: boolean | undefined) { - this.#delegate.supportThemeIcons = value; - } - - get supportHtml(): boolean | undefined { - return this.#delegate.supportHtml; - } - - set supportHtml(value: boolean | undefined) { - this.#delegate.supportHtml = value; - } - - get baseUri(): vscode.Uri | undefined { - return this.#delegate.baseUri; - } - - set baseUri(value: vscode.Uri | undefined) { - this.#delegate.baseUri = value; - } - - appendText(value: string): vscode.MarkdownString { - this.#delegate.appendText(value); - return this; - } - - appendMarkdown(value: string): vscode.MarkdownString { - this.#delegate.appendMarkdown(value); - return this; - } - - appendCodeblock(value: string, language?: string): vscode.MarkdownString { - this.#delegate.appendCodeblock(language ?? '', value); - return this; - } -} - @es5ClassCompat export class ParameterInformation { @@ -3853,124 +2701,8 @@ export enum ColorThemeKind { } //#endregion Theming - //#region Notebook -export class NotebookRange { - static isNotebookRange(thing: any): thing is vscode.NotebookRange { - if (thing instanceof NotebookRange) { - return true; - } - if (!thing) { - return false; - } - return typeof (thing).start === 'number' - && typeof (thing).end === 'number'; - } - - private _start: number; - private _end: number; - - get start() { - return this._start; - } - - get end() { - return this._end; - } - - get isEmpty(): boolean { - return this._start === this._end; - } - - constructor(start: number, end: number) { - if (start < 0) { - throw illegalArgument('start must be positive'); - } - if (end < 0) { - throw illegalArgument('end must be positive'); - } - if (start <= end) { - this._start = start; - this._end = end; - } else { - this._start = end; - this._end = start; - } - } - - with(change: { start?: number; end?: number }): NotebookRange { - let start = this._start; - let end = this._end; - - if (change.start !== undefined) { - start = change.start; - } - if (change.end !== undefined) { - end = change.end; - } - if (start === this._start && end === this._end) { - return this; - } - return new NotebookRange(start, end); - } -} - -export class NotebookCellData { - - static validate(data: NotebookCellData): void { - if (typeof data.kind !== 'number') { - throw new Error('NotebookCellData MUST have \'kind\' property'); - } - if (typeof data.value !== 'string') { - throw new Error('NotebookCellData MUST have \'value\' property'); - } - if (typeof data.languageId !== 'string') { - throw new Error('NotebookCellData MUST have \'languageId\' property'); - } - } - - static isNotebookCellDataArray(value: unknown): value is vscode.NotebookCellData[] { - return Array.isArray(value) && (value).every(elem => NotebookCellData.isNotebookCellData(elem)); - } - - static isNotebookCellData(value: unknown): value is vscode.NotebookCellData { - // return value instanceof NotebookCellData; - return true; - } - - kind: NotebookCellKind; - value: string; - languageId: string; - mime?: string; - outputs?: vscode.NotebookCellOutput[]; - metadata?: Record; - executionSummary?: vscode.NotebookCellExecutionSummary; - - constructor(kind: NotebookCellKind, value: string, languageId: string, mime?: string, outputs?: vscode.NotebookCellOutput[], metadata?: Record, executionSummary?: vscode.NotebookCellExecutionSummary) { - this.kind = kind; - this.value = value; - this.languageId = languageId; - this.mime = mime; - this.outputs = outputs ?? []; - this.metadata = metadata; - this.executionSummary = executionSummary; - - NotebookCellData.validate(this); - } -} - -export class NotebookData { - - cells: NotebookCellData[]; - metadata?: { [key: string]: any }; - - constructor(cells: NotebookCellData[]) { - this.cells = cells; - } -} - - export class NotebookCellOutputItem { static isNotebookCellOutputItem(obj: unknown): obj is vscode.NotebookCellOutputItem { @@ -4097,11 +2829,6 @@ export class CellErrorStackFrame { ) { } } -export enum NotebookCellKind { - Markup = 1, - Code = 2 -} - export enum NotebookCellExecutionState { Idle = 1, Pending = 2, diff --git a/src/vs/workbench/api/common/extHostTypes/codeActionKind.ts b/src/vs/workbench/api/common/extHostTypes/codeActionKind.ts new file mode 100644 index 0000000000000..fdda882c14347 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/codeActionKind.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { es5ClassCompat } from './es5ClassCompat.js'; + +@es5ClassCompat +export class CodeActionKind { + private static readonly sep = '.'; + + public static Empty: CodeActionKind; + public static QuickFix: 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; + public static SourceFixAll: CodeActionKind; + public static Notebook: CodeActionKind; + + constructor( + public readonly value: string + ) { } + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public intersects(other: CodeActionKind): boolean { + return this.contains(other) || other.contains(this); + } + + public contains(other: CodeActionKind): boolean { + return this.value === other.value || other.value.startsWith(this.value + CodeActionKind.sep); + } +} +CodeActionKind.Empty = new CodeActionKind(''); +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'); +CodeActionKind.SourceFixAll = CodeActionKind.Source.append('fixAll'); +CodeActionKind.Notebook = CodeActionKind.Empty.append('notebook'); diff --git a/src/vs/workbench/api/common/extHostTypes/diagnostic.ts b/src/vs/workbench/api/common/extHostTypes/diagnostic.ts new file mode 100644 index 0000000000000..486e383fcffd2 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/diagnostic.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from '../../../../base/common/arrays.js'; +import { URI } from '../../../../base/common/uri.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Location } from './location.js'; +import { Range } from './range.js'; + +export enum DiagnosticTag { + Unnecessary = 1, + Deprecated = 2 +} + +export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0 +} + +@es5ClassCompat +export class DiagnosticRelatedInformation { + + static is(thing: any): thing is DiagnosticRelatedInformation { + if (!thing) { + return false; + } + return typeof (thing).message === 'string' + && (thing).location + && Range.isRange((thing).location.range) + && URI.isUri((thing).location.uri); + } + + location: Location; + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } + + static isEqual(a: DiagnosticRelatedInformation, b: DiagnosticRelatedInformation): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.message === b.message + && a.location.range.isEqual(b.location.range) + && a.location.uri.toString() === b.location.uri.toString(); + } +} + +@es5ClassCompat +export class Diagnostic { + + range: Range; + message: string; + severity: DiagnosticSeverity; + source?: string; + code?: string | number; + relatedInformation?: DiagnosticRelatedInformation[]; + tags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + if (!Range.isRange(range)) { + throw new TypeError('range must be set'); + } + if (!message) { + throw new TypeError('message must be set'); + } + this.range = range; + this.message = message; + this.severity = severity; + } + + toJSON(): any { + return { + severity: DiagnosticSeverity[this.severity], + message: this.message, + range: this.range, + source: this.source, + code: this.code, + }; + } + + static isEqual(a: Diagnostic | undefined, b: Diagnostic | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.message === b.message + && a.severity === b.severity + && a.code === b.code + && a.severity === b.severity + && a.source === b.source + && a.range.isEqual(b.range) + && equals(a.tags, b.tags) + && equals(a.relatedInformation, b.relatedInformation, DiagnosticRelatedInformation.isEqual); + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/es5ClassCompat.ts b/src/vs/workbench/api/common/extHostTypes/es5ClassCompat.ts new file mode 100644 index 0000000000000..e0984a5de629a --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/es5ClassCompat.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * @deprecated + * + * This utility ensures that old JS code that uses functions for classes still works. Existing usages cannot be removed + * but new ones must not be added + */ +export function es5ClassCompat(target: Function): any { + const interceptFunctions = { + apply: function (...args: any[]): any { + if (args.length === 0) { + return Reflect.construct(target, []); + } else { + const argsList = args.length === 1 ? [] : args[1]; + return Reflect.construct(target, argsList, args[0].constructor); + } + }, + call: function (...args: any[]): any { + if (args.length === 0) { + return Reflect.construct(target, []); + } else { + const [thisArg, ...restArgs] = args; + return Reflect.construct(target, restArgs, thisArg.constructor); + } + } + }; + return Object.assign(target, interceptFunctions); +} diff --git a/src/vs/workbench/api/common/extHostTypes/location.ts b/src/vs/workbench/api/common/extHostTypes/location.ts new file mode 100644 index 0000000000000..7fb90d61e010c --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/location.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { URI } from '../../../../base/common/uri.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Position } from './position.js'; +import { Range } from './range.js'; + +@es5ClassCompat +export class Location { + + static isLocation(thing: any): thing is vscode.Location { + if (thing instanceof Location) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing).range) + && URI.isUri((thing).uri); + } + + uri: URI; + range!: Range; + + constructor(uri: URI, rangeOrPosition: Range | Position) { + this.uri = uri; + + if (!rangeOrPosition) { + //that's OK + } else if (Range.isRange(rangeOrPosition)) { + this.range = Range.of(rangeOrPosition); + } else if (Position.isPosition(rangeOrPosition)) { + this.range = new Range(rangeOrPosition, rangeOrPosition); + } else { + throw new Error('Illegal argument'); + } + } + + toJSON(): any { + return { + uri: this.uri, + range: this.range + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/markdownString.ts b/src/vs/workbench/api/common/extHostTypes/markdownString.ts new file mode 100644 index 0000000000000..7a364bf1a3582 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/markdownString.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable local/code-no-native-private */ + +import type * as vscode from 'vscode'; +import { MarkdownString as BaseMarkdownString, MarkdownStringTrustedOptions } from '../../../../base/common/htmlContent.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; + +@es5ClassCompat +export class MarkdownString implements vscode.MarkdownString { + + readonly #delegate: BaseMarkdownString; + + static isMarkdownString(thing: any): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; + } + return thing && thing.appendCodeblock && thing.appendMarkdown && thing.appendText && (thing.value !== undefined); + } + + constructor(value?: string, supportThemeIcons: boolean = false) { + this.#delegate = new BaseMarkdownString(value, { supportThemeIcons }); + } + + get value(): string { + return this.#delegate.value; + } + set value(value: string) { + this.#delegate.value = value; + } + + get isTrusted(): boolean | MarkdownStringTrustedOptions | undefined { + return this.#delegate.isTrusted; + } + + set isTrusted(value: boolean | MarkdownStringTrustedOptions | undefined) { + this.#delegate.isTrusted = value; + } + + get supportThemeIcons(): boolean | undefined { + return this.#delegate.supportThemeIcons; + } + + set supportThemeIcons(value: boolean | undefined) { + this.#delegate.supportThemeIcons = value; + } + + get supportHtml(): boolean | undefined { + return this.#delegate.supportHtml; + } + + set supportHtml(value: boolean | undefined) { + this.#delegate.supportHtml = value; + } + + get baseUri(): vscode.Uri | undefined { + return this.#delegate.baseUri; + } + + set baseUri(value: vscode.Uri | undefined) { + this.#delegate.baseUri = value; + } + + appendText(value: string): vscode.MarkdownString { + this.#delegate.appendText(value); + return this; + } + + appendMarkdown(value: string): vscode.MarkdownString { + this.#delegate.appendMarkdown(value); + return this; + } + + appendCodeblock(value: string, language?: string): vscode.MarkdownString { + this.#delegate.appendCodeblock(language ?? '', value); + return this; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/notebooks.ts b/src/vs/workbench/api/common/extHostTypes/notebooks.ts new file mode 100644 index 0000000000000..5652ce6ad06b7 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/notebooks.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { illegalArgument } from '../../../../base/common/errors.js'; + +export enum NotebookCellKind { + Markup = 1, + Code = 2 +} + +export class NotebookRange { + static isNotebookRange(thing: any): thing is vscode.NotebookRange { + if (thing instanceof NotebookRange) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).start === 'number' + && typeof (thing).end === 'number'; + } + + private _start: number; + private _end: number; + + get start() { + return this._start; + } + + get end() { + return this._end; + } + + get isEmpty(): boolean { + return this._start === this._end; + } + + constructor(start: number, end: number) { + if (start < 0) { + throw illegalArgument('start must be positive'); + } + if (end < 0) { + throw illegalArgument('end must be positive'); + } + if (start <= end) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } + + with(change: { start?: number; end?: number }): NotebookRange { + let start = this._start; + let end = this._end; + + if (change.start !== undefined) { + start = change.start; + } + if (change.end !== undefined) { + end = change.end; + } + if (start === this._start && end === this._end) { + return this; + } + return new NotebookRange(start, end); + } +} + +export class NotebookCellData { + + static validate(data: NotebookCellData): void { + if (typeof data.kind !== 'number') { + throw new Error('NotebookCellData MUST have \'kind\' property'); + } + if (typeof data.value !== 'string') { + throw new Error('NotebookCellData MUST have \'value\' property'); + } + if (typeof data.languageId !== 'string') { + throw new Error('NotebookCellData MUST have \'languageId\' property'); + } + } + + static isNotebookCellDataArray(value: unknown): value is vscode.NotebookCellData[] { + return Array.isArray(value) && (value).every(elem => NotebookCellData.isNotebookCellData(elem)); + } + + static isNotebookCellData(value: unknown): value is vscode.NotebookCellData { + // return value instanceof NotebookCellData; + return true; + } + + kind: NotebookCellKind; + value: string; + languageId: string; + mime?: string; + outputs?: vscode.NotebookCellOutput[]; + metadata?: Record; + executionSummary?: vscode.NotebookCellExecutionSummary; + + constructor(kind: NotebookCellKind, value: string, languageId: string, mime?: string, outputs?: vscode.NotebookCellOutput[], metadata?: Record, executionSummary?: vscode.NotebookCellExecutionSummary) { + this.kind = kind; + this.value = value; + this.languageId = languageId; + this.mime = mime; + this.outputs = outputs ?? []; + this.metadata = metadata; + this.executionSummary = executionSummary; + + NotebookCellData.validate(this); + } +} + +export class NotebookData { + + cells: NotebookCellData[]; + metadata?: { [key: string]: any }; + + constructor(cells: NotebookCellData[]) { + this.cells = cells; + } +} + +@es5ClassCompat +export class NotebookEdit implements vscode.NotebookEdit { + + static isNotebookCellEdit(thing: any): thing is NotebookEdit { + if (thing instanceof NotebookEdit) { + return true; + } + if (!thing) { + return false; + } + return NotebookRange.isNotebookRange((thing)) + && Array.isArray((thing).newCells); + } + + static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit { + return new NotebookEdit(range, newCells); + } + + static insertCells(index: number, newCells: vscode.NotebookCellData[]): vscode.NotebookEdit { + return new NotebookEdit(new NotebookRange(index, index), newCells); + } + + static deleteCells(range: NotebookRange): NotebookEdit { + return new NotebookEdit(range, []); + } + + static updateCellMetadata(index: number, newMetadata: { [key: string]: any }): NotebookEdit { + const edit = new NotebookEdit(new NotebookRange(index, index), []); + edit.newCellMetadata = newMetadata; + return edit; + } + + static updateNotebookMetadata(newMetadata: { [key: string]: any }): NotebookEdit { + const edit = new NotebookEdit(new NotebookRange(0, 0), []); + edit.newNotebookMetadata = newMetadata; + return edit; + } + + range: NotebookRange; + newCells: NotebookCellData[]; + newCellMetadata?: { [key: string]: any }; + newNotebookMetadata?: { [key: string]: any }; + + constructor(range: NotebookRange, newCells: NotebookCellData[]) { + this.range = range; + this.newCells = newCells; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/position.ts b/src/vs/workbench/api/common/extHostTypes/position.ts new file mode 100644 index 0000000000000..63a7b1d31af3a --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/position.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { illegalArgument } from '../../../../base/common/errors.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; + +@es5ClassCompat +export class Position { + + static Min(...positions: Position[]): Position { + if (positions.length === 0) { + throw new TypeError(); + } + let result = positions[0]; + for (let i = 1; i < positions.length; i++) { + const p = positions[i]; + if (p.isBefore(result)) { + result = p; + } + } + return result; + } + + static Max(...positions: Position[]): Position { + if (positions.length === 0) { + throw new TypeError(); + } + let result = positions[0]; + for (let i = 1; i < positions.length; i++) { + const p = positions[i]; + if (p.isAfter(result)) { + result = p; + } + } + return result; + } + + static isPosition(other: any): other is Position { + if (!other) { + return false; + } + if (other instanceof Position) { + return true; + } + const { line, character } = other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } + + static of(obj: vscode.Position): Position { + if (obj instanceof Position) { + return obj; + } else if (this.isPosition(obj)) { + return new Position(obj.line, obj.character); + } + throw new Error('Invalid argument, is NOT a position-like object'); + } + + private _line: number; + private _character: number; + + get line(): number { + return this._line; + } + + get character(): number { + return this._character; + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); + } + if (character < 0) { + throw illegalArgument('character must be non-negative'); + } + this._line = line; + this._character = character; + } + + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character < other._character; + } + + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character <= other._character; + } + + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } + + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } + + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } + + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; + } else if (this._line > other.line) { + return 1; + } else { + // equal line + if (this._character < other._character) { + return -1; + } else if (this._character > other._character) { + return 1; + } else { + // equal line and character + return 0; + } + } + } + + translate(change: { lineDelta?: number; characterDelta?: number }): Position; + translate(lineDelta?: number, characterDelta?: number): Position; + translate(lineDeltaOrChange: number | undefined | { lineDelta?: number; characterDelta?: number }, characterDelta: number = 0): Position { + + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); + } + + let lineDelta: number; + if (typeof lineDeltaOrChange === 'undefined') { + lineDelta = 0; + } else if (typeof lineDeltaOrChange === 'number') { + lineDelta = lineDeltaOrChange; + } else { + lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0; + characterDelta = typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0; + } + + if (lineDelta === 0 && characterDelta === 0) { + return this; + } + return new Position(this.line + lineDelta, this.character + characterDelta); + } + + with(change: { line?: number; character?: number }): Position; + with(line?: number, character?: number): Position; + with(lineOrChange: number | undefined | { line?: number; character?: number }, character: number = this.character): Position { + + if (lineOrChange === null || character === null) { + throw illegalArgument(); + } + + let line: number; + if (typeof lineOrChange === 'undefined') { + line = this.line; + + } else if (typeof lineOrChange === 'number') { + line = lineOrChange; + + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; + } + + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } + + toJSON(): any { + return { line: this.line, character: this.character }; + } + + [Symbol.for('debug.description')]() { + return `(${this.line}:${this.character})`; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/range.ts b/src/vs/workbench/api/common/extHostTypes/range.ts new file mode 100644 index 0000000000000..3bbf1f2db048f --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/range.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { illegalArgument } from '../../../../base/common/errors.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Position } from './position.js'; + +@es5ClassCompat +export class Range { + + static isRange(thing: any): thing is vscode.Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; + } + return Position.isPosition((thing).start) + && Position.isPosition((thing.end)); + } + + static of(obj: vscode.Range): Range { + if (obj instanceof Range) { + return obj; + } + if (this.isRange(obj)) { + return new Range(obj.start, obj.end); + } + throw new Error('Invalid argument, is NOT a range-like object'); + } + + protected _start: Position; + protected _end: Position; + + get start(): Position { + return this._start; + } + + get end(): Position { + return this._end; + } + + constructor(start: vscode.Position, end: vscode.Position); + constructor(start: Position, end: Position); + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); + constructor(startLineOrStart: number | Position | vscode.Position, startColumnOrEnd: number | Position | vscode.Position, endLine?: number, endColumn?: number) { + let start: Position | undefined; + let end: Position | undefined; + + if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { + start = new Position(startLineOrStart, startColumnOrEnd); + end = new Position(endLine, endColumn); + } else if (Position.isPosition(startLineOrStart) && Position.isPosition(startColumnOrEnd)) { + start = Position.of(startLineOrStart); + end = Position.of(startColumnOrEnd); + } + + if (!start || !end) { + throw new Error('Invalid arguments'); + } + + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } + + contains(positionOrRange: Position | Range): boolean { + if (Range.isRange(positionOrRange)) { + return this.contains(positionOrRange.start) + && this.contains(positionOrRange.end); + + } else if (Position.isPosition(positionOrRange)) { + if (Position.of(positionOrRange).isBefore(this._start)) { + return false; + } + if (this._end.isBefore(positionOrRange)) { + return false; + } + return true; + } + return false; + } + + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } + + intersection(other: Range): Range | undefined { + const start = Position.Max(other.start, this._start); + const end = Position.Min(other.end, this._end); + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined; + } + return new Range(start, end); + } + + union(other: Range): Range { + if (this.contains(other)) { + return this; + } else if (other.contains(this)) { + return other; + } + const start = Position.Min(other.start, this._start); + const end = Position.Max(other.end, this.end); + return new Range(start, end); + } + + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } + + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } + + with(change: { start?: Position; end?: Position }): Range; + with(start?: Position, end?: Position): Range; + with(startOrChange: Position | undefined | { start?: Position; end?: Position }, end: Position = this.end): Range { + + if (startOrChange === null || end === null) { + throw illegalArgument(); + } + + let start: Position; + if (!startOrChange) { + start = this.start; + + } else if (Position.isPosition(startOrChange)) { + start = startOrChange; + + } else { + start = startOrChange.start || this.start; + end = startOrChange.end || this.end; + } + + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; + } + return new Range(start, end); + } + + toJSON(): any { + return [this.start, this.end]; + } + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfRange(this); + } +} + +export function getDebugDescriptionOfRange(range: vscode.Range): string { + return range.isEmpty + ? `[${range.start.line}:${range.start.character})` + : `[${range.start.line}:${range.start.character} -> ${range.end.line}:${range.end.character})`; +} diff --git a/src/vs/workbench/api/common/extHostTypes/selection.ts b/src/vs/workbench/api/common/extHostTypes/selection.ts new file mode 100644 index 0000000000000..32de7de754378 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/selection.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Position } from './position.js'; +import { getDebugDescriptionOfRange, Range } from './range.js'; + +@es5ClassCompat +export class Selection extends Range { + + static isSelection(thing: any): thing is Selection { + if (thing instanceof Selection) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange(thing) + && Position.isPosition((thing).anchor) + && Position.isPosition((thing).active) + && typeof (thing).isReversed === 'boolean'; + } + + private _anchor: Position; + + public get anchor(): Position { + return this._anchor; + } + + private _active: Position; + + public get active(): Position { + return this._active; + } + + constructor(anchor: Position, active: Position); + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); + constructor(anchorLineOrAnchor: number | Position, anchorColumnOrActive: number | Position, activeLine?: number, activeColumn?: number) { + let anchor: Position | undefined; + let active: Position | undefined; + + if (typeof anchorLineOrAnchor === 'number' && typeof anchorColumnOrActive === 'number' && typeof activeLine === 'number' && typeof activeColumn === 'number') { + anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive); + active = new Position(activeLine, activeColumn); + } else if (Position.isPosition(anchorLineOrAnchor) && Position.isPosition(anchorColumnOrActive)) { + anchor = Position.of(anchorLineOrAnchor); + active = Position.of(anchorColumnOrActive); + } + + if (!anchor || !active) { + throw new Error('Invalid arguments'); + } + + super(anchor, active); + + this._anchor = anchor; + this._active = active; + } + + get isReversed(): boolean { + return this._anchor === this._end; + } + + override toJSON() { + return { + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor + }; + } + + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfSelection(this); + } +} + +export function getDebugDescriptionOfSelection(selection: vscode.Selection): string { + let rangeStr = getDebugDescriptionOfRange(selection); + if (!selection.isEmpty) { + if (selection.active.isEqual(selection.start)) { + rangeStr = `|${rangeStr}`; + } else { + rangeStr = `${rangeStr}|`; + } + } + return rangeStr; +} diff --git a/src/vs/workbench/api/common/extHostTypes/snippetString.ts b/src/vs/workbench/api/common/extHostTypes/snippetString.ts new file mode 100644 index 0000000000000..f1c083ce3734e --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/snippetString.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { es5ClassCompat } from './es5ClassCompat.js'; + +@es5ClassCompat +export class SnippetString { + + static isSnippetString(thing: any): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).value === 'string'; + } + + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + private _tabstop: number = 1; + + value: string; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; + } + + appendTabstop(number: number = this._tabstop++): SnippetString { + this.value += '$'; + this.value += number; + return this; + } + + appendPlaceholder(value: string | ((snippet: SnippetString) => any), number: number = this._tabstop++): SnippetString { + + if (typeof value === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + value(nested); + this._tabstop = nested._tabstop; + value = nested.value; + } else { + value = SnippetString._escape(value); + } + + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; + } + + appendChoice(values: string[], number: number = this._tabstop++): SnippetString { + const value = values.map(s => s.replaceAll(/[|\\,]/g, '\\$&')).join(','); + + this.value += '${'; + this.value += number; + this.value += '|'; + this.value += value; + this.value += '|}'; + + return this; + } + + appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { + + if (typeof defaultValue === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + defaultValue(nested); + this._tabstop = nested._tabstop; + defaultValue = nested.value; + + } else if (typeof defaultValue === 'string') { + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] I do not want to escape backslashes here + } + + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; + } + this.value += '}'; + + + return this; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/snippetTextEdit.ts b/src/vs/workbench/api/common/extHostTypes/snippetTextEdit.ts new file mode 100644 index 0000000000000..a9dc25b57b441 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/snippetTextEdit.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { SnippetString } from './snippetString.js'; +import { Position } from './position.js'; +import { Range } from './range.js'; + +export class SnippetTextEdit implements vscode.SnippetTextEdit { + + static isSnippetTextEdit(thing: any): thing is SnippetTextEdit { + if (thing instanceof SnippetTextEdit) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing).range) + && SnippetString.isSnippetString((thing).snippet); + } + + static replace(range: Range, snippet: SnippetString): SnippetTextEdit { + return new SnippetTextEdit(range, snippet); + } + + static insert(position: Position, snippet: SnippetString): SnippetTextEdit { + return SnippetTextEdit.replace(new Range(position, position), snippet); + } + + range: Range; + + snippet: SnippetString; + + keepWhitespace?: boolean; + + constructor(range: Range, snippet: SnippetString) { + this.range = range; + this.snippet = snippet; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/textEdit.ts b/src/vs/workbench/api/common/extHostTypes/textEdit.ts new file mode 100644 index 0000000000000..565f09c45e0fb --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/textEdit.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { illegalArgument } from '../../../../base/common/errors.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Position } from './position.js'; +import { Range } from './range.js'; + +export enum EndOfLine { + LF = 1, + CRLF = 2 +} + +@es5ClassCompat +export class TextEdit { + + static isTextEdit(thing: any): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing)) + && typeof (thing).newText === 'string'; + } + + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } + + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } + + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } + + static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + ret.newEol = eol; + return ret; + } + + protected _range: Range; + protected _newText: string | null; + protected _newEol?: EndOfLine; + + get range(): Range { + return this._range; + } + + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); + } + this._range = value; + } + + get newText(): string { + return this._newText || ''; + } + + set newText(value: string) { + if (value && typeof value !== 'string') { + throw illegalArgument('newText'); + } + this._newText = value; + } + + get newEol(): EndOfLine | undefined { + return this._newEol; + } + + set newEol(value: EndOfLine | undefined) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); + } + this._newEol = value; + } + + constructor(range: Range, newText: string | null) { + this._range = range; + this._newText = newText; + } + + toJSON(): any { + return { + range: this.range, + newText: this.newText, + newEol: this._newEol + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes/workspaceEdit.ts b/src/vs/workbench/api/common/extHostTypes/workspaceEdit.ts new file mode 100644 index 0000000000000..7104fcb3eb8a6 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTypes/workspaceEdit.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { coalesceInPlace } from '../../../../base/common/arrays.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { URI } from '../../../../base/common/uri.js'; +import { CellEditType, ICellMetadataEdit, IDocumentMetadataEdit } from '../../../contrib/notebook/common/notebookCommon.js'; +import { NotebookEdit } from './notebooks.js'; +import { SnippetTextEdit } from './snippetTextEdit.js'; +import { es5ClassCompat } from './es5ClassCompat.js'; +import { Position } from './position.js'; +import { Range } from './range.js'; +import { TextEdit } from './textEdit.js'; + +export interface IFileOperationOptions { + readonly overwrite?: boolean; + readonly ignoreIfExists?: boolean; + readonly ignoreIfNotExists?: boolean; + readonly recursive?: boolean; + readonly contents?: Uint8Array | vscode.DataTransferFile; +} + +export const enum FileEditType { + File = 1, + Text = 2, + Cell = 3, + CellReplace = 5, + Snippet = 6, +} + +export interface IFileOperation { + readonly _type: FileEditType.File; + readonly from?: URI; + readonly to?: URI; + readonly options?: IFileOperationOptions; + readonly metadata?: vscode.WorkspaceEditEntryMetadata; +} + +export interface IFileTextEdit { + readonly _type: FileEditType.Text; + readonly uri: URI; + readonly edit: TextEdit; + readonly metadata?: vscode.WorkspaceEditEntryMetadata; +} + +export interface IFileSnippetTextEdit { + readonly _type: FileEditType.Snippet; + readonly uri: URI; + readonly range: vscode.Range; + readonly edit: vscode.SnippetString; + readonly metadata?: vscode.WorkspaceEditEntryMetadata; + readonly keepWhitespace?: boolean; +} + +export interface IFileCellEdit { + readonly _type: FileEditType.Cell; + readonly uri: URI; + readonly edit?: ICellMetadataEdit | IDocumentMetadataEdit; + readonly metadata?: vscode.WorkspaceEditEntryMetadata; +} + +export interface ICellEdit { + readonly _type: FileEditType.CellReplace; + readonly metadata?: vscode.WorkspaceEditEntryMetadata; + readonly uri: URI; + readonly index: number; + readonly count: number; + readonly cells: vscode.NotebookCellData[]; +} + +export type WorkspaceEditEntry = IFileOperation | IFileTextEdit | IFileSnippetTextEdit | IFileCellEdit | ICellEdit; + +@es5ClassCompat +export class WorkspaceEdit implements vscode.WorkspaceEdit { + + private readonly _edits: WorkspaceEditEntry[] = []; + + + _allEntries(): ReadonlyArray { + return this._edits; + } + + // --- file + renameFile(from: vscode.Uri, to: vscode.Uri, options?: { readonly overwrite?: boolean; readonly ignoreIfExists?: boolean }, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from, to, options, metadata }); + } + + createFile(uri: vscode.Uri, options?: { readonly overwrite?: boolean; readonly ignoreIfExists?: boolean; readonly contents?: Uint8Array | vscode.DataTransferFile }, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }); + } + + deleteFile(uri: vscode.Uri, options?: { readonly recursive?: boolean; readonly ignoreIfNotExists?: boolean }, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); + } + + // --- notebook + private replaceNotebookMetadata(uri: URI, value: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: value } }); + } + + private replaceNotebookCells(uri: URI, startOrRange: vscode.NotebookRange, cellData: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + const start = startOrRange.start; + const end = startOrRange.end; + + if (start !== end || cellData.length > 0) { + this._edits.push({ _type: FileEditType.CellReplace, uri, index: start, count: end - start, cells: cellData, metadata }); + } + } + + private replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: Record, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); + } + + // --- text + replace(uri: URI, range: Range, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }); + } + + insert(resource: URI, position: Position, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this.replace(resource, new Range(position, position), newText, metadata); + } + + delete(resource: URI, range: Range, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this.replace(resource, range, '', metadata); + } + + // --- text (Maplike) + has(uri: URI): boolean { + return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); + } + + set(uri: URI, edits: ReadonlyArray): void; + set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, vscode.WorkspaceEditEntryMetadata | undefined]>): void; + set(uri: URI, edits: readonly NotebookEdit[]): void; + set(uri: URI, edits: ReadonlyArray<[NotebookEdit, vscode.WorkspaceEditEntryMetadata | undefined]>): void; + + set(uri: URI, edits: null | undefined | ReadonlyArray): void { + if (!edits) { + // remove all text, snippet, or notebook edits for `uri` + for (let i = 0; i < this._edits.length; i++) { + const element = this._edits[i]; + switch (element._type) { + case FileEditType.Text: + case FileEditType.Snippet: + case FileEditType.Cell: + case FileEditType.CellReplace: + if (element.uri.toString() === uri.toString()) { + this._edits[i] = undefined!; // will be coalesced down below + } + break; + } + } + coalesceInPlace(this._edits); + } else { + // append edit to the end + for (const editOrTuple of edits) { + if (!editOrTuple) { + continue; + } + let edit: TextEdit | SnippetTextEdit | NotebookEdit; + let metadata: vscode.WorkspaceEditEntryMetadata | undefined; + if (Array.isArray(editOrTuple)) { + edit = editOrTuple[0]; + metadata = editOrTuple[1]; + } else { + edit = editOrTuple; + } + if (NotebookEdit.isNotebookCellEdit(edit)) { + if (edit.newCellMetadata) { + this.replaceNotebookCellMetadata(uri, edit.range.start, edit.newCellMetadata, metadata); + } else if (edit.newNotebookMetadata) { + this.replaceNotebookMetadata(uri, edit.newNotebookMetadata, metadata); + } else { + this.replaceNotebookCells(uri, edit.range, edit.newCells, metadata); + } + } else if (SnippetTextEdit.isSnippetTextEdit(edit)) { + this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata, keepWhitespace: edit.keepWhitespace }); + + } else { + this._edits.push({ _type: FileEditType.Text, uri, edit, metadata }); + } + } + } + } + + get(uri: URI): TextEdit[] { + const res: TextEdit[] = []; + for (const candidate of this._edits) { + if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { + res.push(candidate.edit); + } + } + return res; + } + + entries(): [URI, TextEdit[]][] { + const textEdits = new ResourceMap<[URI, TextEdit[]]>(); + for (const candidate of this._edits) { + if (candidate._type === FileEditType.Text) { + let textEdit = textEdits.get(candidate.uri); + if (!textEdit) { + textEdit = [candidate.uri, []]; + textEdits.set(candidate.uri, textEdit); + } + textEdit[1].push(candidate.edit); + } + } + return [...textEdits.values()]; + } + + get size(): number { + return this.entries().length; + } + + toJSON(): any { + return this.entries(); + } +}