/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import { DisposableStore } from 'vs/base/common/lifecycle'; import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, IWorkspaceEditDto, ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto } from 'vs/workbench/api/common/extHost.protocol'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand, OpenIssueReporter, OpenIssueReporterArgs } from './apiCommands'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { VSBuffer } from 'vs/base/common/buffer'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; //#region --- NEW world const newCommands: ApiCommand[] = [ // -- document highlights new ApiCommand( 'vscode.executeDocumentHighlights', '_executeDocumentHighlights', 'Execute document highlight provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of SymbolInformation and DocumentSymbol instances.', tryMapWith(typeConverters.DocumentHighlight.to)) ), // -- document symbols new ApiCommand( 'vscode.executeDocumentSymbolProvider', '_executeDocumentSymbolProvider', 'Execute document symbol provider.', [ApiCommandArgument.Uri], new ApiCommandResult('A promise that resolves to an array of DocumentHighlight-instances.', (value, apiArgs) => { if (isFalsyOrEmpty(value)) { return undefined; } class MergedInfo extends types.SymbolInformation implements vscode.DocumentSymbol { static to(symbol: modes.DocumentSymbol): MergedInfo { const res = new MergedInfo( symbol.name, typeConverters.SymbolKind.to(symbol.kind), symbol.containerName || '', new types.Location(apiArgs[0], typeConverters.Range.to(symbol.range)) ); res.detail = symbol.detail; res.range = res.location.range; res.selectionRange = typeConverters.Range.to(symbol.selectionRange); res.children = symbol.children ? symbol.children.map(MergedInfo.to) : []; return res; } detail!: string; range!: vscode.Range; selectionRange!: vscode.Range; children!: vscode.DocumentSymbol[]; containerName!: string; } return value.map(MergedInfo.to); }) ), // -- formatting new ApiCommand( 'vscode.executeFormatDocumentProvider', '_executeFormatDocumentProvider', 'Execute document format provider.', [ApiCommandArgument.Uri, new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), new ApiCommand( 'vscode.executeFormatRangeProvider', '_executeFormatRangeProvider', 'Execute range format provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Range, new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), new ApiCommand( 'vscode.executeFormatOnTypeProvider', '_executeFormatOnTypeProvider', 'Execute format on type provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position, new ApiCommandArgument('ch', 'Trigger character', v => typeof v === 'string', v => v), new ApiCommandArgument('options', 'Formatting options', _ => true, v => v)], new ApiCommandResult('A promise that resolves to an array of TextEdits.', tryMapWith(typeConverters.TextEdit.to)) ), // -- go to symbol (definition, type definition, declaration, impl, references) new ApiCommand( 'vscode.executeDefinitionProvider', '_executeDefinitionProvider', 'Execute all definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeTypeDefinitionProvider', '_executeTypeDefinitionProvider', 'Execute all type definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeDeclarationProvider', '_executeDeclarationProvider', 'Execute all declaration providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeImplementationProvider', '_executeImplementationProvider', 'Execute all implementation providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeReferenceProvider', '_executeReferenceProvider', 'Execute all reference providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Location-instances.', tryMapWith(typeConverters.location.to)) ), // -- hover new ApiCommand( 'vscode.executeHoverProvider', '_executeHoverProvider', 'Execute all hover providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Hover-instances.', tryMapWith(typeConverters.Hover.to)) ), // -- selection range new ApiCommand( 'vscode.executeSelectionRangeProvider', '_executeSelectionRangeProvider', 'Execute selection range provider.', [ApiCommandArgument.Uri, new ApiCommandArgument('position', 'A positions in a text document', v => Array.isArray(v) && v.every(v => types.Position.isPosition(v)), v => v.map(typeConverters.Position.from))], new ApiCommandResult('A promise that resolves to an array of ranges.', result => { return result.map(ranges => { let node: types.SelectionRange | undefined; for (const range of ranges.reverse()) { node = new types.SelectionRange(typeConverters.Range.to(range), node); } return node!; }); }) ), // -- symbol search new ApiCommand( 'vscode.executeWorkspaceSymbolProvider', '_executeWorkspaceSymbolProvider', 'Execute all workspace symbol providers.', [ApiCommandArgument.String.with('query', 'Search string')], new ApiCommandResult<[search.IWorkspaceSymbolProvider, search.IWorkspaceSymbol[]][], types.SymbolInformation[]>('A promise that resolves to an array of SymbolInformation-instances.', value => { const result: types.SymbolInformation[] = []; if (Array.isArray(value)) { for (let tuple of value) { result.push(...tuple[1].map(typeConverters.WorkspaceSymbol.to)); } } return result; }) ), // --- call hierarchy new ApiCommand( 'vscode.prepareCallHierarchy', '_executePrepareCallHierarchy', 'Prepare call hierarchy at a position inside a document', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A CallHierarchyItem or undefined', v => v.map(typeConverters.CallHierarchyItem.to)) ), new ApiCommand( 'vscode.provideIncomingCalls', '_executeProvideIncomingCalls', 'Compute incoming calls for an item', [ApiCommandArgument.CallHierarchyItem], new ApiCommandResult('A CallHierarchyItem or undefined', v => v.map(typeConverters.CallHierarchyIncomingCall.to)) ), new ApiCommand( 'vscode.provideOutgoingCalls', '_executeProvideOutgoingCalls', 'Compute outgoing calls for an item', [ApiCommandArgument.CallHierarchyItem], new ApiCommandResult('A CallHierarchyItem or undefined', v => v.map(typeConverters.CallHierarchyOutgoingCall.to)) ), // --- rename new ApiCommand( 'vscode.executeDocumentRenameProvider', '_executeDocumentRenameProvider', 'Execute rename provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position, ApiCommandArgument.String.with('newName', 'The new symbol name')], new ApiCommandResult('A promise that resolves to a WorkspaceEdit.', value => { if (!value) { return undefined; } if (value.rejectReason) { throw new Error(value.rejectReason); } return typeConverters.WorkspaceEdit.to(value); }) ), // --- links new ApiCommand( 'vscode.executeLinkProvider', '_executeLinkProvider', 'Execute document link provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('linkResolveCount', 'Number of links that should be resolved, only when links are unresolved.').optional()], new ApiCommandResult('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to)) ), // --- semantic tokens new ApiCommand( 'vscode.provideDocumentSemanticTokensLegend', '_provideDocumentSemanticTokensLegend', 'Provide semantic tokens legend for a document', [ApiCommandArgument.Uri], new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { if (!value) { return undefined; } return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); }) ), new ApiCommand( 'vscode.provideDocumentSemanticTokens', '_provideDocumentSemanticTokens', 'Provide semantic tokens for a document', [ApiCommandArgument.Uri], new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { if (!value) { return undefined; } const semanticTokensDto = decodeSemanticTokensDto(value); if (semanticTokensDto.type !== 'full') { // only accepting full semantic tokens from provideDocumentSemanticTokens return undefined; } return new types.SemanticTokens(semanticTokensDto.data, undefined); }) ), new ApiCommand( 'vscode.provideDocumentRangeSemanticTokensLegend', '_provideDocumentRangeSemanticTokensLegend', 'Provide semantic tokens legend for a document range', [ApiCommandArgument.Uri], new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { if (!value) { return undefined; } return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); }) ), new ApiCommand( 'vscode.provideDocumentRangeSemanticTokens', '_provideDocumentRangeSemanticTokens', 'Provide semantic tokens for a document range', [ApiCommandArgument.Uri, ApiCommandArgument.Range], new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { if (!value) { return undefined; } const semanticTokensDto = decodeSemanticTokensDto(value); if (semanticTokensDto.type !== 'full') { // only accepting full semantic tokens from provideDocumentRangeSemanticTokens return undefined; } return new types.SemanticTokens(semanticTokensDto.data, undefined); }) ), // --- completions new ApiCommand( 'vscode.executeCompletionItemProvider', '_executeCompletionItemProvider', 'Execute completion item provider.', [ ApiCommandArgument.Uri, ApiCommandArgument.Position, ApiCommandArgument.String.with('triggerCharacter', 'Trigger completion when the user types the character, like `,` or `(`').optional(), ApiCommandArgument.Number.with('itemResolveCount', 'Number of completions to resolve (too large numbers slow down completions)').optional() ], new ApiCommandResult('A promise that resolves to a CompletionList-instance.', (value, _args, converter) => { if (!value) { return new types.CompletionList([]); } const items = value.suggestions.map(suggestion => typeConverters.CompletionItem.to(suggestion, converter)); return new types.CompletionList(items, value.incomplete); }) ), // --- signature help new ApiCommand( 'vscode.executeSignatureHelpProvider', '_executeSignatureHelpProvider', 'Execute signature help provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position, ApiCommandArgument.String.with('triggerCharacter', 'Trigger signature help when the user types the character, like `,` or `(`').optional()], new ApiCommandResult('A promise that resolves to SignatureHelp.', value => { if (value) { return typeConverters.SignatureHelp.to(value); } return undefined; }) ), // --- code lens new ApiCommand( 'vscode.executeCodeLensProvider', '_executeCodeLensProvider', 'Execute code lens provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('itemResolveCount', 'Number of lenses that should be resolved and returned. Will only return resolved lenses, will impact performance)').optional()], new ApiCommandResult('A promise that resolves to an array of CodeLens-instances.', (value, _args, converter) => { return tryMapWith(item => { return new types.CodeLens(typeConverters.Range.to(item.range), item.command && converter.fromInternal(item.command)); })(value); }) ), // --- code actions new ApiCommand( 'vscode.executeCodeActionProvider', '_executeCodeActionProvider', 'Execute code action provider.', [ ApiCommandArgument.Uri, new ApiCommandArgument('rangeOrSelection', 'Range in a text document. Some refactoring provider requires Selection object.', v => types.Range.isRange(v), v => types.Selection.isSelection(v) ? typeConverters.Selection.from(v) : typeConverters.Range.from(v)), ApiCommandArgument.String.with('kind', 'Code action kind to return code actions for').optional(), ApiCommandArgument.Number.with('itemResolveCount', 'Number of code actions to resolve (too large numbers slow down code actions)').optional() ], new ApiCommandResult('A promise that resolves to an array of Command-instances.', (value, _args, converter) => { return tryMapWith((codeAction) => { if (codeAction._isSynthetic) { if (!codeAction.command) { throw new Error('Synthetic code actions must have a command'); } return converter.fromInternal(codeAction.command); } else { const ret = new types.CodeAction( codeAction.title, codeAction.kind ? new types.CodeActionKind(codeAction.kind) : undefined ); if (codeAction.edit) { ret.edit = typeConverters.WorkspaceEdit.to(codeAction.edit); } if (codeAction.command) { ret.command = converter.fromInternal(codeAction.command); } ret.isPreferred = codeAction.isPreferred; return ret; } })(value); }) ), // --- colors new ApiCommand( 'vscode.executeDocumentColorProvider', '_executeDocumentColorProvider', 'Execute document color provider.', [ApiCommandArgument.Uri], new ApiCommandResult('A promise that resolves to an array of ColorInformation objects.', result => { if (result) { return result.map(ci => new types.ColorInformation(typeConverters.Range.to(ci.range), typeConverters.Color.to(ci.color))); } return []; }) ), new ApiCommand( 'vscode.executeColorPresentationProvider', '_executeColorPresentationProvider', 'Execute color presentation provider.', [ new ApiCommandArgument('color', 'The color to show and insert', v => v instanceof types.Color, typeConverters.Color.from), new ApiCommandArgument<{ uri: URI, range: types.Range; }, { uri: URI, range: IRange; }>('context', 'Context object with uri and range', _v => true, v => ({ uri: v.uri, range: typeConverters.Range.from(v.range) })), ], new ApiCommandResult('A promise that resolves to an array of ColorPresentation objects.', result => { if (result) { return result.map(typeConverters.ColorPresentation.to); } return []; }) ), // --- inline hints new ApiCommand( 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', [ApiCommandArgument.Uri, ApiCommandArgument.Range], new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { return result.map(typeConverters.InlineHint.to); }) ), // --- notebooks new ApiCommand( 'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers', [ // new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), // new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), // new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), ], new ApiCommandResult<{ viewType: string; displayName: string; options: { transientOutputs: boolean; transientMetadata: TransientMetadata }; filenamePattern: (string | types.RelativePattern | { include: string | types.RelativePattern, exclude: string | types.RelativePattern })[] }[], { viewType: string; displayName: string; filenamePattern: vscode.NotebookFilenamePattern[]; options: vscode.NotebookDocumentContentOptions; }[] | undefined>('A promise that resolves to an array of NotebookContentProvider static info objects.', tryMapWith(item => { return { viewType: item.viewType, displayName: item.displayName, options: { transientOutputs: item.options.transientOutputs, transientMetadata: item.options.transientMetadata }, filenamePattern: item.filenamePattern.map(pattern => typeConverters.NotebookExclusiveDocumentPattern.to(pattern)) }; })) ), // --- debug support new ApiCommand( 'vscode.executeInlineValueProvider', '_executeInlineValueProvider', 'Execute inline value provider', [ApiCommandArgument.Uri, ApiCommandArgument.Range], new ApiCommandResult('A promise that resolves to an array of InlineValue objects', result => { return result.map(typeConverters.InlineValue.to); }) ), // --- open'ish commands new ApiCommand( 'vscode.open', '_workbench.open', 'Opens the provided resource in the editor. Can be a text or binary file, or a http(s) url. If you need more control over the options for opening a text file, use vscode.window.showTextDocument instead.', [ ApiCommandArgument.Uri, new ApiCommandArgument('columnOrOptions', 'Either the column in which to open or editor options, see vscode.TextDocumentShowOptions', v => v === undefined || typeof v === 'number' || typeof v === 'object', v => !v ? v : typeof v === 'number' ? [v, undefined] : [typeConverters.ViewColumn.from(v.viewColumn), typeConverters.TextEditorOpenOptions.from(v)] ).optional(), ApiCommandArgument.String.with('label', '').optional() ], ApiCommandResult.Void ), new ApiCommand( 'vscode.openWith', '_workbench.openWith', 'Opens the provided resource with a specific editor.', [ ApiCommandArgument.Uri.with('resource', 'Resource to open'), ApiCommandArgument.String.with('viewId', 'Custom editor view id or \'default\' to use VS Code\'s default editor'), new ApiCommandArgument('columnOrOptions', 'Either the column in which to open or editor options, see vscode.TextDocumentShowOptions', v => v === undefined || typeof v === 'number' || typeof v === 'object', v => !v ? v : typeof v === 'number' ? [v, undefined] : [typeConverters.ViewColumn.from(v.viewColumn), typeConverters.TextEditorOpenOptions.from(v)], ).optional() ], ApiCommandResult.Void ), new ApiCommand( 'vscode.diff', '_workbench.diff', 'Opens the provided resources in the diff editor to compare their contents.', [ ApiCommandArgument.Uri.with('left', 'Left-hand side resource of the diff editor'), ApiCommandArgument.Uri.with('right', 'Rigth-hand side resource of the diff editor'), ApiCommandArgument.String.with('title', 'Human readable title for the diff editor').optional(), new ApiCommandArgument('columnOrOptions', 'Either the column in which to open or editor options, see vscode.TextDocumentShowOptions', v => v === undefined || typeof v === 'object', v => v && [typeConverters.ViewColumn.from(v.viewColumn), typeConverters.TextEditorOpenOptions.from(v)] ).optional(), ], ApiCommandResult.Void ), ]; //#endregion //#region OLD world export class ExtHostApiCommands { static register(commands: ExtHostCommands) { newCommands.forEach(commands.registerApiCommand, commands); return new ExtHostApiCommands(commands).registerCommands(); } private _commands: ExtHostCommands; private readonly _disposables = new DisposableStore(); private constructor(commands: ExtHostCommands) { this._commands = commands; } registerCommands() { // ----------------------------------------------------------------- // The following commands are registered on both sides separately. // // We are trying to maintain backwards compatibility for cases where // API commands are encoded as markdown links, for example. // ----------------------------------------------------------------- type ICommandHandler = (...args: any[]) => any; const adjustHandler = (handler: (executor: ICommandsExecutor, ...args: any[]) => any): ICommandHandler => { return (...args: any[]) => { return handler(this._commands, ...args); }; }; this._register(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute), { description: 'Removes an entry with the given path from the recently opened list.', args: [ { name: 'path', description: 'Path to remove from recently opened.', constraint: (value: any) => typeof value === 'string' } ] }); this._register(OpenIssueReporter.ID, adjustHandler(OpenIssueReporter.execute), { description: 'Opens the issue reporter with the provided extension id as the selected source', args: [ { name: 'extensionId', description: 'extensionId to report an issue on', constraint: (value: unknown) => typeof value === 'string' || (typeof value === 'object' && typeof (value as OpenIssueReporterArgs).extensionId === 'string') } ] }); } // --- command impl /** * @deprecated use the ApiCommand instead */ private _register(id: string, handler: (...args: any[]) => any, description?: ICommandHandlerDescription): void { const disposable = this._commands.registerCommand(false, id, handler, this, description); this._disposables.add(disposable); } } function tryMapWith(f: (x: T) => R) { return (value: T[]) => { if (Array.isArray(value)) { return value.map(f); } return undefined; }; } function mapLocationOrLocationLink(values: (modes.Location | modes.LocationLink)[]): (types.Location | vscode.LocationLink)[] | undefined { if (!Array.isArray(values)) { return undefined; } const result: (types.Location | vscode.LocationLink)[] = []; for (const item of values) { if (modes.isLocationLink(item)) { result.push(typeConverters.DefinitionLink.to(item)); } else { result.push(typeConverters.location.to(item)); } } return result; }