diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 7ea2b6f5af60c..577a4fd1686cf 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -45,10 +45,20 @@ "onCommand:typescript.openTsServerLog", "onCommand:workbench.action.tasks.runTask", "onCommand:_typescript.configurePlugin", + "onCommand:_typescript.learnMoreAboutRefactorings", "onLanguage:jsonc" ], "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 1bd543cb55c40..adad367759b49 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -97,5 +97,6 @@ "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" + "codeActions.source.organizeImports.title": "Organize imports", + "documentation.refactoring.title": "Learn more about JS/TS refactorings" } diff --git a/extensions/typescript-language-features/src/commands/index.ts b/extensions/typescript-language-features/src/commands/index.ts index f22b3e448de50..8e4cfddcccd90 100644 --- a/extensions/typescript-language-features/src/commands/index.ts +++ b/extensions/typescript-language-features/src/commands/index.ts @@ -13,6 +13,7 @@ import { OpenTsServerLogCommand } from './openTsServerLog'; import { ReloadJavaScriptProjectsCommand, ReloadTypeScriptProjectsCommand } from './reloadProject'; import { RestartTsServerCommand } from './restartTsServer'; import { SelectTypeScriptVersionCommand } from './selectTypeScriptVersion'; +import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings'; export function registerCommands( commandManager: CommandManager, @@ -27,4 +28,5 @@ export function registerCommands( commandManager.register(new TypeScriptGoToProjectConfigCommand(lazyClientHost)); commandManager.register(new JavaScriptGoToProjectConfigCommand(lazyClientHost)); commandManager.register(new ConfigurePluginCommand(pluginManager)); -} \ No newline at end of file + commandManager.register(new LearnMoreAboutRefactoringsCommand()); +} diff --git a/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts new file mode 100644 index 0000000000000..3c1673bb63ce3 --- /dev/null +++ b/extensions/typescript-language-features/src/commands/learnMoreAboutRefactorings.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Command } from '../utils/commandManager'; + +export class LearnMoreAboutRefactoringsCommand implements Command { + public readonly id = '_typescript.learnMoreAboutRefactorings'; + + public execute() { + vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=2114477')); + } +} diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index c2cdc9c4f0fec..7be72d3d2e3da 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; -import { Action } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { canceled } from 'vs/base/common/errors'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; @@ -14,7 +15,7 @@ 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 } from 'vs/editor/common/modes'; -import { CodeActionSet, refactorCommandId, sourceActionCommandId, codeActionCommandId, organizeImportsCommandId, fixAllCommandId } from 'vs/editor/contrib/codeAction/codeAction'; +import { codeActionCommandId, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/types'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -83,8 +84,7 @@ export class CodeActionMenu extends Disposable { this._visible = true; this._showingActions.value = codeActions; - const menuActions = actionsToShow.map(action => - new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action))); + const menuActions = this.getMenuActions(actionsToShow); const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 }; const resolver = this._keybindingResolver.getResolver(); @@ -101,6 +101,24 @@ export class CodeActionMenu extends Disposable { }); } + private getMenuActions(actionsToShow: readonly CodeAction[]): IAction[] { + const allActions = actionsToShow + .map(action => new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action))); + + // Treat documentation actions as special + const result: IAction[] = allActions + .filter(action => !action.action.kind || !CodeActionKind.RefactorDocumentation.contains(new CodeActionKind(action.action.kind))); + + const documentationActions = allActions + .filter(action => action.action.kind && CodeActionKind.RefactorDocumentation.contains(new CodeActionKind(action.action.kind))); + + if (documentationActions.length) { + result.push(new Separator(), ...documentationActions); + } + + return result; + } + private _toCoords(position: IPosition): { x: number, y: number } { if (!this._editor.hasModel()) { return { x: 0, y: 0 }; diff --git a/src/vs/editor/contrib/codeAction/types.ts b/src/vs/editor/contrib/codeAction/types.ts index 650be89e98c72..bce9f61232901 100644 --- a/src/vs/editor/contrib/codeAction/types.ts +++ b/src/vs/editor/contrib/codeAction/types.ts @@ -14,6 +14,7 @@ export class CodeActionKind { public static readonly Empty = new CodeActionKind(''); public static readonly QuickFix = new CodeActionKind('quickfix'); public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly RefactorDocumentation = new CodeActionKind('refactor.documentation'); public static readonly Source = new CodeActionKind('source'); public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll'); diff --git a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts index c23b8d27d4173..c7969c9d1c0b8 100644 --- a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts +++ b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts @@ -4,24 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { CodeActionWorkbenchConfigurationContribution, editorConfiguration } from 'vs/workbench/contrib/codeActions/common/configuration'; -import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/extensionPoint'; +import { CodeActionsContribution, editorConfiguration } from 'vs/workbench/contrib/codeActions/common/codeActionsContribution'; +import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint'; +import { CodeActionDocumentationContribution } from 'vs/workbench/contrib/codeActions/common/documentationContribution'; +import { DocumentationExtensionPoint, documentationExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/documentationExtensionPoint'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; const codeActionsExtensionPoint = ExtensionsRegistry.registerExtensionPoint(codeActionsExtensionPointDescriptor); +const documentationExtensionPoint = ExtensionsRegistry.registerExtensionPoint(documentationExtensionPointDescriptor); Registry.as(Extensions.Configuration) .registerConfiguration(editorConfiguration); class WorkbenchConfigurationContribution { constructor( - @IKeybindingService keybindingsService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService, ) { - new CodeActionWorkbenchConfigurationContribution(codeActionsExtensionPoint, keybindingsService); + instantiationService.createInstance(CodeActionsContribution, codeActionsExtensionPoint); + instantiationService.createInstance(CodeActionDocumentationContribution, documentationExtensionPoint); } } diff --git a/src/vs/workbench/contrib/codeActions/common/configuration.ts b/src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts similarity index 96% rename from src/vs/workbench/contrib/codeActions/common/configuration.ts rename to src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts index 19b9ac85ddf29..53153e88dca2c 100644 --- a/src/vs/workbench/contrib/codeActions/common/configuration.ts +++ b/src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts @@ -15,7 +15,7 @@ import { Extensions, IConfigurationNode, IConfigurationRegistry, ConfigurationSc import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CodeActionsExtensionPoint, ContributedCodeAction } from 'vs/workbench/contrib/codeActions/common/extensionPoint'; +import { CodeActionsExtensionPoint, ContributedCodeAction } from 'vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; @@ -50,7 +50,7 @@ export const editorConfiguration = Object.freeze({ } }); -export class CodeActionWorkbenchConfigurationContribution extends Disposable implements IWorkbenchContribution { +export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; @@ -58,7 +58,7 @@ export class CodeActionWorkbenchConfigurationContribution extends Disposable imp constructor( codeActionsExtensionPoint: IExtensionPoint, - keybindingService: IKeybindingService, + @IKeybindingService keybindingService: IKeybindingService, ) { super(); diff --git a/src/vs/workbench/contrib/codeActions/common/extensionPoint.ts b/src/vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint.ts similarity index 100% rename from src/vs/workbench/contrib/codeActions/common/extensionPoint.ts rename to src/vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint.ts diff --git a/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts new file mode 100644 index 0000000000000..cc26586e2b4df --- /dev/null +++ b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeAction, CodeActionContext, CodeActionList, CodeActionProvider, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { DocumentationExtensionPoint } from './documentationExtensionPoint'; + + +export class CodeActionDocumentationContribution extends Disposable implements IWorkbenchContribution, CodeActionProvider { + + private contributions: { + title: string; + when: ContextKeyExpr; + command: string; + }[] = []; + + constructor( + extensionPoint: IExtensionPoint, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + CodeActionProviderRegistry.register('*', this); + + extensionPoint.setHandler(points => { + this.contributions = []; + for (const documentation of points) { + if (!documentation.value.refactoring) { + continue; + } + + for (const contribution of documentation.value.refactoring) { + const precondition = ContextKeyExpr.deserialize(contribution.when); + if (!precondition) { + continue; + } + + this.contributions.push({ + title: contribution.title, + when: precondition, + command: contribution.command + }); + + } + } + }); + } + + async provideCodeActions(_model: ITextModel, _range: Range | Selection, context: CodeActionContext, _token: CancellationToken): Promise { + if (!context.only || !CodeActionKind.Refactor.contains(new CodeActionKind(context.only))) { + return { + actions: [], + dispose: () => { } + }; + } + + const actions: CodeAction[] = []; + + for (const contribution of this.contributions) { + if (!this.contextKeyService.contextMatchesRules(contribution.when)) { + continue; + } + + actions.push({ + title: contribution.title, + kind: CodeActionKind.RefactorDocumentation.value, + command: { + id: contribution.command, + title: contribution.title + } + }); + } + + return { + actions, + dispose: () => { } + }; + } + + public readonly providedCodeActionKinds = [CodeActionKind.RefactorDocumentation.value] as const; +} diff --git a/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts b/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts new file mode 100644 index 0000000000000..bb848f8d64d17 --- /dev/null +++ b/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; + +export enum DocumentationExtensionPointFields { + when = 'when', + title = 'title', + command = 'command', +} + +export interface RefactoringDocumentationExtensionPoint { + readonly [DocumentationExtensionPointFields.title]: string; + readonly [DocumentationExtensionPointFields.when]: string; + readonly [DocumentationExtensionPointFields.command]: string; +} + +export interface DocumentationExtensionPoint { + readonly refactoring?: readonly RefactoringDocumentationExtensionPoint[]; +} + +const documentationExtensionPointSchema = Object.freeze({ + type: 'object', + description: nls.localize('contributes.documentation', "Contributed documentation."), + properties: { + 'refactoring': { + type: 'array', + description: nls.localize('contributes.documentation.refactorings', "Contributed documentation for refactorings."), + items: { + type: 'object', + description: nls.localize('contributes.documentation.refactoring', "Contributed documentation for refactoring."), + required: [ + DocumentationExtensionPointFields.title, + DocumentationExtensionPointFields.when, + DocumentationExtensionPointFields.command + ], + properties: { + [DocumentationExtensionPointFields.title]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.title', "Label for the documentation used in the UI."), + }, + [DocumentationExtensionPointFields.when]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.when', "When clause."), + }, + [DocumentationExtensionPointFields.command]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.command', "Command executed."), + }, + }, + } + } + } +}); + +export const documentationExtensionPointDescriptor = { + extensionPoint: 'documentation', + deps: [languagesExtPoint], + jsonSchema: documentationExtensionPointSchema +};