codeAction.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  6. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  7. return new (P || (P = Promise))(function (resolve, reject) {
  8. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  9. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  10. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  11. step((generator = generator.apply(thisArg, _arguments || [])).next());
  12. });
  13. };
  14. import { coalesce, equals, flatten, isNonEmptyArray } from '../../../base/common/arrays.js';
  15. import { CancellationToken } from '../../../base/common/cancellation.js';
  16. import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from '../../../base/common/errors.js';
  17. import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
  18. import { URI } from '../../../base/common/uri.js';
  19. import { TextModelCancellationTokenSource } from '../../browser/core/editorState.js';
  20. import { Range } from '../../common/core/range.js';
  21. import { Selection } from '../../common/core/selection.js';
  22. import * as modes from '../../common/modes.js';
  23. import { IModelService } from '../../common/services/modelService.js';
  24. import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
  25. import { Progress } from '../../../platform/progress/common/progress.js';
  26. import { CodeActionKind, filtersAction, mayIncludeActionsOfKind } from './types.js';
  27. export const codeActionCommandId = 'editor.action.codeAction';
  28. export const refactorCommandId = 'editor.action.refactor';
  29. export const sourceActionCommandId = 'editor.action.sourceAction';
  30. export const organizeImportsCommandId = 'editor.action.organizeImports';
  31. export const fixAllCommandId = 'editor.action.fixAll';
  32. export class CodeActionItem {
  33. constructor(action, provider) {
  34. this.action = action;
  35. this.provider = provider;
  36. }
  37. resolve(token) {
  38. var _a;
  39. return __awaiter(this, void 0, void 0, function* () {
  40. if (((_a = this.provider) === null || _a === void 0 ? void 0 : _a.resolveCodeAction) && !this.action.edit) {
  41. let action;
  42. try {
  43. action = yield this.provider.resolveCodeAction(this.action, token);
  44. }
  45. catch (err) {
  46. onUnexpectedExternalError(err);
  47. }
  48. if (action) {
  49. this.action.edit = action.edit;
  50. }
  51. }
  52. return this;
  53. });
  54. }
  55. }
  56. class ManagedCodeActionSet extends Disposable {
  57. constructor(actions, documentation, disposables) {
  58. super();
  59. this.documentation = documentation;
  60. this._register(disposables);
  61. this.allActions = [...actions].sort(ManagedCodeActionSet.codeActionsComparator);
  62. this.validActions = this.allActions.filter(({ action }) => !action.disabled);
  63. }
  64. static codeActionsComparator({ action: a }, { action: b }) {
  65. if (a.isPreferred && !b.isPreferred) {
  66. return -1;
  67. }
  68. else if (!a.isPreferred && b.isPreferred) {
  69. return 1;
  70. }
  71. if (isNonEmptyArray(a.diagnostics)) {
  72. if (isNonEmptyArray(b.diagnostics)) {
  73. return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message);
  74. }
  75. else {
  76. return -1;
  77. }
  78. }
  79. else if (isNonEmptyArray(b.diagnostics)) {
  80. return 1;
  81. }
  82. else {
  83. return 0; // both have no diagnostics
  84. }
  85. }
  86. get hasAutoFix() {
  87. return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new CodeActionKind(fix.kind)) && !!fix.isPreferred);
  88. }
  89. }
  90. const emptyCodeActionsResponse = { actions: [], documentation: undefined };
  91. export function getCodeActions(model, rangeOrSelection, trigger, progress, token) {
  92. var _a;
  93. const filter = trigger.filter || {};
  94. const codeActionContext = {
  95. only: (_a = filter.include) === null || _a === void 0 ? void 0 : _a.value,
  96. trigger: trigger.type,
  97. };
  98. const cts = new TextModelCancellationTokenSource(model, token);
  99. const providers = getCodeActionProviders(model, filter);
  100. const disposables = new DisposableStore();
  101. const promises = providers.map((provider) => __awaiter(this, void 0, void 0, function* () {
  102. try {
  103. progress.report(provider);
  104. const providedCodeActions = yield provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token);
  105. if (providedCodeActions) {
  106. disposables.add(providedCodeActions);
  107. }
  108. if (cts.token.isCancellationRequested) {
  109. return emptyCodeActionsResponse;
  110. }
  111. const filteredActions = ((providedCodeActions === null || providedCodeActions === void 0 ? void 0 : providedCodeActions.actions) || []).filter(action => action && filtersAction(filter, action));
  112. const documentation = getDocumentation(provider, filteredActions, filter.include);
  113. return {
  114. actions: filteredActions.map(action => new CodeActionItem(action, provider)),
  115. documentation
  116. };
  117. }
  118. catch (err) {
  119. if (isPromiseCanceledError(err)) {
  120. throw err;
  121. }
  122. onUnexpectedExternalError(err);
  123. return emptyCodeActionsResponse;
  124. }
  125. }));
  126. const listener = modes.CodeActionProviderRegistry.onDidChange(() => {
  127. const newProviders = modes.CodeActionProviderRegistry.all(model);
  128. if (!equals(newProviders, providers)) {
  129. cts.cancel();
  130. }
  131. });
  132. return Promise.all(promises).then(actions => {
  133. const allActions = flatten(actions.map(x => x.actions));
  134. const allDocumentation = coalesce(actions.map(x => x.documentation));
  135. return new ManagedCodeActionSet(allActions, allDocumentation, disposables);
  136. })
  137. .finally(() => {
  138. listener.dispose();
  139. cts.dispose();
  140. });
  141. }
  142. function getCodeActionProviders(model, filter) {
  143. return modes.CodeActionProviderRegistry.all(model)
  144. // Don't include providers that we know will not return code actions of interest
  145. .filter(provider => {
  146. if (!provider.providedCodeActionKinds) {
  147. // We don't know what type of actions this provider will return.
  148. return true;
  149. }
  150. return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind)));
  151. });
  152. }
  153. function getDocumentation(provider, providedCodeActions, only) {
  154. if (!provider.documentation) {
  155. return undefined;
  156. }
  157. const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command }));
  158. if (only) {
  159. let currentBest;
  160. for (const entry of documentation) {
  161. if (entry.kind.contains(only)) {
  162. if (!currentBest) {
  163. currentBest = entry;
  164. }
  165. else {
  166. // Take best match
  167. if (currentBest.kind.contains(entry.kind)) {
  168. currentBest = entry;
  169. }
  170. }
  171. }
  172. }
  173. if (currentBest) {
  174. return currentBest === null || currentBest === void 0 ? void 0 : currentBest.command;
  175. }
  176. }
  177. // Otherwise, check to see if any of the provided actions match.
  178. for (const action of providedCodeActions) {
  179. if (!action.kind) {
  180. continue;
  181. }
  182. for (const entry of documentation) {
  183. if (entry.kind.contains(new CodeActionKind(action.kind))) {
  184. return entry.command;
  185. }
  186. }
  187. }
  188. return undefined;
  189. }
  190. CommandsRegistry.registerCommand('_executeCodeActionProvider', function (accessor, resource, rangeOrSelection, kind, itemResolveCount) {
  191. return __awaiter(this, void 0, void 0, function* () {
  192. if (!(resource instanceof URI)) {
  193. throw illegalArgument();
  194. }
  195. const model = accessor.get(IModelService).getModel(resource);
  196. if (!model) {
  197. throw illegalArgument();
  198. }
  199. const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection)
  200. ? Selection.liftSelection(rangeOrSelection)
  201. : Range.isIRange(rangeOrSelection)
  202. ? model.validateRange(rangeOrSelection)
  203. : undefined;
  204. if (!validatedRangeOrSelection) {
  205. throw illegalArgument();
  206. }
  207. const include = typeof kind === 'string' ? new CodeActionKind(kind) : undefined;
  208. const codeActionSet = yield getCodeActions(model, validatedRangeOrSelection, { type: 1 /* Invoke */, filter: { includeSourceActions: true, include } }, Progress.None, CancellationToken.None);
  209. const resolving = [];
  210. const resolveCount = Math.min(codeActionSet.validActions.length, typeof itemResolveCount === 'number' ? itemResolveCount : 0);
  211. for (let i = 0; i < resolveCount; i++) {
  212. resolving.push(codeActionSet.validActions[i].resolve(CancellationToken.None));
  213. }
  214. try {
  215. yield Promise.all(resolving);
  216. return codeActionSet.validActions.map(item => item.action);
  217. }
  218. finally {
  219. setTimeout(() => codeActionSet.dispose(), 100);
  220. }
  221. });
  222. });