diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index dc773ba24ac..c52278cc655 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -51,15 +51,6 @@ ], "main": "./out/extension", "contributes": { - "documentation": { - "refactoring": [ - { - "title": "%documentation.refactoring.title%", - "when": "typescript.isManagedFile", - "command": "_typescript.learnMoreAboutRefactorings" - } - ] - }, "jsonValidation": [ { "fileMatch": "package.json", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index afd6e4c4949..a421936acb9 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -114,6 +114,5 @@ "codeActions.refactor.rewrite.parameters.toDestructured.title": "Convert parameters to destructured object", "codeActions.refactor.rewrite.property.generateAccessors.title": "Generate accessors", "codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors", - "codeActions.source.organizeImports.title": "Organize imports", - "documentation.refactoring.title": "Learn more about JS/TS refactorings" + "codeActions.source.organizeImports.title": "Organize imports" } diff --git a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts index 91d9e70ae80..21366d6c607 100644 --- a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts +++ b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts @@ -8,7 +8,8 @@ import { Command } from '../utils/commandManager'; import { isTypeScriptDocument } from '../utils/languageModeIds'; export class LearnMoreAboutRefactoringsCommand implements Command { - public readonly id = '_typescript.learnMoreAboutRefactorings'; + public static readonly id = '_typescript.learnMoreAboutRefactorings'; + public readonly id = LearnMoreAboutRefactoringsCommand.id; public execute() { const docUrl = vscode.window.activeTextEditor && isTypeScriptDocument(vscode.window.activeTextEditor.document) diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/features/refactor.ts index 11ff4002fbe..e863cf03bd9 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/features/refactor.ts @@ -5,16 +5,17 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { Command, CommandManager } from '../utils/commandManager'; import { VersionDependentRegistration } from '../utils/dependentRegistration'; +import * as fileSchemes from '../utils/fileSchemes'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; import FormattingOptionsManager from './fileConfigurationManager'; -import * as fileSchemes from '../utils/fileSchemes'; const localize = nls.loadMessageBundle(); @@ -209,6 +210,15 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { vscode.CodeActionKind.Refactor, ...allKnownCodeActionKinds.map(x => x.kind), ], + documentation: [ + { + kind: vscode.CodeActionKind.Refactor, + command: { + command: LearnMoreAboutRefactoringsCommand.id, + title: localize('refactor.documentation.title', "Learn more about JS/TS refactorings") + } + } + ] }; public async provideCodeActions( diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6e6fbc7bcaa..7d925b4bc2c 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -615,7 +615,9 @@ export interface CodeActionProvider { /** * Optional list of CodeActionKinds that this provider returns. */ - providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: ReadonlyArray; + + readonly documentation?: ReadonlyArray<{ readonly kind: string, readonly command: Command }>; /** * @internal diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index b0a9aa8051f..82ab87cacec 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals, flatten, isNonEmptyArray, mergeSort } from 'vs/base/common/arrays'; +import { equals, flatten, isNonEmptyArray, mergeSort, coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -27,6 +27,8 @@ export interface CodeActionSet extends IDisposable { readonly validActions: readonly modes.CodeAction[]; readonly allActions: readonly modes.CodeAction[]; readonly hasAutoFix: boolean; + + readonly documentation: readonly modes.Command[]; } class ManagedCodeActionSet extends Disposable implements CodeActionSet { @@ -48,7 +50,11 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { public readonly validActions: readonly modes.CodeAction[]; public readonly allActions: readonly modes.CodeAction[]; - public constructor(actions: readonly modes.CodeAction[], disposables: DisposableStore) { + public constructor( + actions: readonly modes.CodeAction[], + public readonly documentation: readonly modes.Command[], + disposables: DisposableStore, + ) { super(); this._register(disposables); this.allActions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator); @@ -80,17 +86,23 @@ export function getCodeActions( const promises = providers.map(async provider => { try { const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token); - if (cts.token.isCancellationRequested || !providedCodeActions) { - return []; + if (providedCodeActions) { + disposables.add(providedCodeActions); } - disposables.add(providedCodeActions); - return providedCodeActions.actions.filter(action => action && filtersAction(filter, action)); + + if (cts.token.isCancellationRequested) { + return { actions: [] as modes.CodeAction[], documentation: undefined }; + } + + const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action)); + const documentation = getDocumentation(provider, filteredActions, filter.include); + return { actions: filteredActions, documentation }; } catch (err) { if (isPromiseCanceledError(err)) { throw err; } onUnexpectedExternalError(err); - return []; + return { actions: [] as modes.CodeAction[], documentation: undefined }; } }); @@ -101,9 +113,11 @@ export function getCodeActions( } }); - return Promise.all(promises) - .then(flatten) - .then(actions => new ManagedCodeActionSet(actions, disposables)) + return Promise.all(promises).then(actions => { + const allActions = flatten(actions.map(x => x.actions)); + const allDocumentation = coalesce(actions.map(x => x.documentation)); + return new ManagedCodeActionSet(allActions, allDocumentation, disposables); + }) .finally(() => { listener.dispose(); cts.dispose(); @@ -125,6 +139,52 @@ function getCodeActionProviders( }); } +function getDocumentation( + provider: modes.CodeActionProvider, + providedCodeActions: readonly modes.CodeAction[], + only?: CodeActionKind +): modes.Command | undefined { + if (!provider.documentation) { + return undefined; + } + + const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command })); + + if (only) { + let currentBest: { readonly kind: CodeActionKind, readonly command: modes.Command } | undefined; + for (const entry of documentation) { + if (entry.kind.contains(only)) { + if (!currentBest) { + currentBest = entry; + } else { + // Take best match + if (currentBest.kind.contains(entry.kind)) { + currentBest = entry; + } + } + } + } + if (currentBest) { + return currentBest?.command; + } + } + + // Otherwise, check to see if any of the provided actions match. + for (const action of providedCodeActions) { + if (!action.kind) { + continue; + } + + for (const entry of documentation) { + if (entry.kind.contains(new CodeActionKind(action.kind))) { + return entry.command; + } + } + } + + return undefined; +} + registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise> { const { resource, rangeOrSelection, kind } = args; if (!(resource instanceof URI)) { diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index e732dec94c3..b736d9c2884 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -14,9 +14,9 @@ import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionProviderRegistry, Command } from 'vs/editor/common/modes'; import { codeActionCommandId, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction'; -import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; +import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionTrigger, CodeActionKind } from 'vs/editor/contrib/codeAction/types'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -84,7 +84,7 @@ export class CodeActionMenu extends Disposable { this._visible = true; this._showingActions.value = codeActions; - const menuActions = this.getMenuActions(trigger, actionsToShow); + const menuActions = this.getMenuActions(trigger, actionsToShow, codeActions.documentation); const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 }; const resolver = this._keybindingResolver.getResolver(); @@ -101,28 +101,34 @@ export class CodeActionMenu extends Disposable { }); } - private getMenuActions(trigger: CodeActionTrigger, actionsToShow: readonly CodeAction[]): IAction[] { + private getMenuActions( + trigger: CodeActionTrigger, + actionsToShow: readonly CodeAction[], + documentation: readonly Command[] + ): IAction[] { const toCodeActionAction = (action: CodeAction): CodeActionAction => new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action)); const result: IAction[] = actionsToShow .map(toCodeActionAction); + const allDocumentation: Command[] = [...documentation]; const model = this._editor.getModel(); if (model && result.length) { for (const provider of CodeActionProviderRegistry.all(model)) { if (provider._getAdditionalMenuItems) { - const items = provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow); - if (items.length) { - result.push(new Separator(), ...items.map(command => toCodeActionAction({ - title: command.title, - command: command, - }))); - } + allDocumentation.push(...provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow)); } } } + if (allDocumentation.length) { + result.push(new Separator(), ...allDocumentation.map(command => toCodeActionAction({ + title: command.title, + command: command, + }))); + } + return result; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7dd8193e7d5..574145b9cdf 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1622,4 +1622,17 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/86788 + + export interface CodeActionProviderMetadata { + /** + * Static documentation for a class of code actions. + * + * The documentation is shown at the + */ + readonly documentation?: ReadonlyArray<{ readonly kind: CodeActionKind, readonly command: Command }>; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 43ada931f82..678a44ca3f3 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -235,7 +235,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- quick fix - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], providedCodeActionKinds?: string[]): void { + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void { this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); @@ -251,7 +251,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } }; }, - providedCodeActionKinds + providedCodeActionKinds: metadata.providedKinds, + documentation: metadata.documentation })); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5d73a2e16d5..c181215c41d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -355,7 +355,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], supportedKinds?: string[]): void; + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -1146,6 +1146,11 @@ export interface ICodeActionListDto { actions: ReadonlyArray; } +export interface ICodeActionProviderMetadataDto { + readonly providedKinds?: readonly string[]; + readonly documentation?: ReadonlyArray<{ readonly kind: string, readonly command: ICommandDto }>; +} + export type CacheId = number; export type ChainedCacheId = [CacheId, CacheId]; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 73ac4858560..090d0892cb1 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -230,6 +230,8 @@ export class CommandsConverter { this._commands.registerCommand(true, this._delegatingCommandId, this._executeConvertedCommand, this); } + toInternal(command: vscode.Command, disposables: DisposableStore): ICommandDto; + toInternal(command: vscode.Command | undefined, disposables: DisposableStore): ICommandDto | undefined; toInternal(command: vscode.Command | undefined, disposables: DisposableStore): ICommandDto | undefined { if (!command) { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 8f0929fc1b2..b90e3ddeb24 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1576,9 +1576,17 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- quick fix registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { + const store = new DisposableStore(); const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), (metadata && metadata.providedCodeActionKinds) ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined); - return this._createDisposable(handle); + this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), { + providedKinds: metadata?.providedCodeActionKinds?.map(kind => kind.value), + documentation: metadata?.documentation?.map(x => ({ + kind: x.kind.value, + command: this._commands.converter.toInternal(x.command, store), + })) + }); + store.add(this._createDisposable(handle)); + return store; } diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index 6852f286947..78786ce99dd 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -10,7 +10,7 @@ import { Language } from 'vs/base/common/platform'; import { Action, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { Mode, IEntryRunContext, IAutoFocus, IModel, IQuickNavigateConfiguration } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenEntryGroup, IHighlight, QuickOpenModel, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { QuickOpenHandler, IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen'; import { IEditorAction } from 'vs/editor/common/editorCommon'; @@ -471,7 +471,9 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { // Other Actions const menu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.CommandPalette, accessor.get(IContextKeyService))); - const menuActions = menu.getActions().reduce((r, [, actions]) => [...r, ...actions], []).filter(action => action instanceof MenuItemAction) as MenuItemAction[]; + const menuActions = menu.getActions() + .reduce((r, [, actions]) => [...r, ...actions], >[]) + .filter(action => action instanceof MenuItemAction) as MenuItemAction[]; const commandEntries = this.menuItemActionsToEntries(menuActions, searchValue); menu.dispose(); this.disposeOnClose.add(toDisposable(() => dispose(menuActions)));