editorExtensions.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. import * as nls from '../../nls.js';
  6. import { URI } from '../../base/common/uri.js';
  7. import { ICodeEditorService } from './services/codeEditorService.js';
  8. import { Position } from '../common/core/position.js';
  9. import { IModelService } from '../common/services/modelService.js';
  10. import { ITextModelService } from '../common/services/resolverService.js';
  11. import { MenuId, MenuRegistry } from '../../platform/actions/common/actions.js';
  12. import { CommandsRegistry } from '../../platform/commands/common/commands.js';
  13. import { ContextKeyExpr, IContextKeyService } from '../../platform/contextkey/common/contextkey.js';
  14. import { KeybindingsRegistry } from '../../platform/keybinding/common/keybindingsRegistry.js';
  15. import { Registry } from '../../platform/registry/common/platform.js';
  16. import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js';
  17. import { withNullAsUndefined, assertType } from '../../base/common/types.js';
  18. import { ILogService } from '../../platform/log/common/log.js';
  19. export class Command {
  20. constructor(opts) {
  21. this.id = opts.id;
  22. this.precondition = opts.precondition;
  23. this._kbOpts = opts.kbOpts;
  24. this._menuOpts = opts.menuOpts;
  25. this._description = opts.description;
  26. }
  27. register() {
  28. if (Array.isArray(this._menuOpts)) {
  29. this._menuOpts.forEach(this._registerMenuItem, this);
  30. }
  31. else if (this._menuOpts) {
  32. this._registerMenuItem(this._menuOpts);
  33. }
  34. if (this._kbOpts) {
  35. const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts];
  36. for (const kbOpts of kbOptsArr) {
  37. let kbWhen = kbOpts.kbExpr;
  38. if (this.precondition) {
  39. if (kbWhen) {
  40. kbWhen = ContextKeyExpr.and(kbWhen, this.precondition);
  41. }
  42. else {
  43. kbWhen = this.precondition;
  44. }
  45. }
  46. const desc = {
  47. id: this.id,
  48. weight: kbOpts.weight,
  49. args: kbOpts.args,
  50. when: kbWhen,
  51. primary: kbOpts.primary,
  52. secondary: kbOpts.secondary,
  53. win: kbOpts.win,
  54. linux: kbOpts.linux,
  55. mac: kbOpts.mac,
  56. };
  57. KeybindingsRegistry.registerKeybindingRule(desc);
  58. }
  59. }
  60. CommandsRegistry.registerCommand({
  61. id: this.id,
  62. handler: (accessor, args) => this.runCommand(accessor, args),
  63. description: this._description
  64. });
  65. }
  66. _registerMenuItem(item) {
  67. MenuRegistry.appendMenuItem(item.menuId, {
  68. group: item.group,
  69. command: {
  70. id: this.id,
  71. title: item.title,
  72. icon: item.icon,
  73. precondition: this.precondition
  74. },
  75. when: item.when,
  76. order: item.order
  77. });
  78. }
  79. }
  80. export class MultiCommand extends Command {
  81. constructor() {
  82. super(...arguments);
  83. this._implementations = [];
  84. }
  85. /**
  86. * A higher priority gets to be looked at first
  87. */
  88. addImplementation(priority, name, implementation) {
  89. this._implementations.push({ priority, name, implementation });
  90. this._implementations.sort((a, b) => b.priority - a.priority);
  91. return {
  92. dispose: () => {
  93. for (let i = 0; i < this._implementations.length; i++) {
  94. if (this._implementations[i].implementation === implementation) {
  95. this._implementations.splice(i, 1);
  96. return;
  97. }
  98. }
  99. }
  100. };
  101. }
  102. runCommand(accessor, args) {
  103. const logService = accessor.get(ILogService);
  104. logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`);
  105. for (const impl of this._implementations) {
  106. const result = impl.implementation(accessor, args);
  107. if (result) {
  108. logService.trace(`Command '${this.id}' was handled by '${impl.name}'.`);
  109. if (typeof result === 'boolean') {
  110. return;
  111. }
  112. return result;
  113. }
  114. }
  115. logService.trace(`The Command '${this.id}' was not handled by any implementation.`);
  116. }
  117. }
  118. //#endregion
  119. /**
  120. * A command that delegates to another command's implementation.
  121. *
  122. * This lets different commands be registered but share the same implementation
  123. */
  124. export class ProxyCommand extends Command {
  125. constructor(command, opts) {
  126. super(opts);
  127. this.command = command;
  128. }
  129. runCommand(accessor, args) {
  130. return this.command.runCommand(accessor, args);
  131. }
  132. }
  133. export class EditorCommand extends Command {
  134. /**
  135. * Create a command class that is bound to a certain editor contribution.
  136. */
  137. static bindToContribution(controllerGetter) {
  138. return class EditorControllerCommandImpl extends EditorCommand {
  139. constructor(opts) {
  140. super(opts);
  141. this._callback = opts.handler;
  142. }
  143. runEditorCommand(accessor, editor, args) {
  144. const controller = controllerGetter(editor);
  145. if (controller) {
  146. this._callback(controllerGetter(editor), args);
  147. }
  148. }
  149. };
  150. }
  151. runCommand(accessor, args) {
  152. const codeEditorService = accessor.get(ICodeEditorService);
  153. // Find the editor with text focus or active
  154. const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
  155. if (!editor) {
  156. // well, at least we tried...
  157. return;
  158. }
  159. return editor.invokeWithinContext((editorAccessor) => {
  160. const kbService = editorAccessor.get(IContextKeyService);
  161. if (!kbService.contextMatchesRules(withNullAsUndefined(this.precondition))) {
  162. // precondition does not hold
  163. return;
  164. }
  165. return this.runEditorCommand(editorAccessor, editor, args);
  166. });
  167. }
  168. }
  169. export class EditorAction extends EditorCommand {
  170. constructor(opts) {
  171. super(EditorAction.convertOptions(opts));
  172. this.label = opts.label;
  173. this.alias = opts.alias;
  174. }
  175. static convertOptions(opts) {
  176. let menuOpts;
  177. if (Array.isArray(opts.menuOpts)) {
  178. menuOpts = opts.menuOpts;
  179. }
  180. else if (opts.menuOpts) {
  181. menuOpts = [opts.menuOpts];
  182. }
  183. else {
  184. menuOpts = [];
  185. }
  186. function withDefaults(item) {
  187. if (!item.menuId) {
  188. item.menuId = MenuId.EditorContext;
  189. }
  190. if (!item.title) {
  191. item.title = opts.label;
  192. }
  193. item.when = ContextKeyExpr.and(opts.precondition, item.when);
  194. return item;
  195. }
  196. if (Array.isArray(opts.contextMenuOpts)) {
  197. menuOpts.push(...opts.contextMenuOpts.map(withDefaults));
  198. }
  199. else if (opts.contextMenuOpts) {
  200. menuOpts.push(withDefaults(opts.contextMenuOpts));
  201. }
  202. opts.menuOpts = menuOpts;
  203. return opts;
  204. }
  205. runEditorCommand(accessor, editor, args) {
  206. this.reportTelemetry(accessor, editor);
  207. return this.run(accessor, editor, args || {});
  208. }
  209. reportTelemetry(accessor, editor) {
  210. accessor.get(ITelemetryService).publicLog2('editorActionInvoked', { name: this.label, id: this.id });
  211. }
  212. }
  213. export class MultiEditorAction extends EditorAction {
  214. constructor() {
  215. super(...arguments);
  216. this._implementations = [];
  217. }
  218. /**
  219. * A higher priority gets to be looked at first
  220. */
  221. addImplementation(priority, implementation) {
  222. this._implementations.push([priority, implementation]);
  223. this._implementations.sort((a, b) => b[0] - a[0]);
  224. return {
  225. dispose: () => {
  226. for (let i = 0; i < this._implementations.length; i++) {
  227. if (this._implementations[i][1] === implementation) {
  228. this._implementations.splice(i, 1);
  229. return;
  230. }
  231. }
  232. }
  233. };
  234. }
  235. run(accessor, editor, args) {
  236. for (const impl of this._implementations) {
  237. const result = impl[1](accessor, editor, args);
  238. if (result) {
  239. if (typeof result === 'boolean') {
  240. return;
  241. }
  242. return result;
  243. }
  244. }
  245. }
  246. }
  247. //#endregion
  248. // --- Registration of commands and actions
  249. export function registerModelAndPositionCommand(id, handler) {
  250. CommandsRegistry.registerCommand(id, function (accessor, ...args) {
  251. const [resource, position] = args;
  252. assertType(URI.isUri(resource));
  253. assertType(Position.isIPosition(position));
  254. const model = accessor.get(IModelService).getModel(resource);
  255. if (model) {
  256. const editorPosition = Position.lift(position);
  257. return handler(model, editorPosition, ...args.slice(2));
  258. }
  259. return accessor.get(ITextModelService).createModelReference(resource).then(reference => {
  260. return new Promise((resolve, reject) => {
  261. try {
  262. const result = handler(reference.object.textEditorModel, Position.lift(position), args.slice(2));
  263. resolve(result);
  264. }
  265. catch (err) {
  266. reject(err);
  267. }
  268. }).finally(() => {
  269. reference.dispose();
  270. });
  271. });
  272. });
  273. }
  274. export function registerModelCommand(id, handler) {
  275. CommandsRegistry.registerCommand(id, function (accessor, ...args) {
  276. const [resource] = args;
  277. assertType(URI.isUri(resource));
  278. const model = accessor.get(IModelService).getModel(resource);
  279. if (model) {
  280. return handler(model, ...args.slice(1));
  281. }
  282. return accessor.get(ITextModelService).createModelReference(resource).then(reference => {
  283. return new Promise((resolve, reject) => {
  284. try {
  285. const result = handler(reference.object.textEditorModel, args.slice(1));
  286. resolve(result);
  287. }
  288. catch (err) {
  289. reject(err);
  290. }
  291. }).finally(() => {
  292. reference.dispose();
  293. });
  294. });
  295. });
  296. }
  297. export function registerEditorCommand(editorCommand) {
  298. EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand);
  299. return editorCommand;
  300. }
  301. export function registerEditorAction(ctor) {
  302. const action = new ctor();
  303. EditorContributionRegistry.INSTANCE.registerEditorAction(action);
  304. return action;
  305. }
  306. export function registerMultiEditorAction(action) {
  307. EditorContributionRegistry.INSTANCE.registerEditorAction(action);
  308. return action;
  309. }
  310. export function registerInstantiatedEditorAction(editorAction) {
  311. EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction);
  312. }
  313. export function registerEditorContribution(id, ctor) {
  314. EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor);
  315. }
  316. export var EditorExtensionsRegistry;
  317. (function (EditorExtensionsRegistry) {
  318. function getEditorCommand(commandId) {
  319. return EditorContributionRegistry.INSTANCE.getEditorCommand(commandId);
  320. }
  321. EditorExtensionsRegistry.getEditorCommand = getEditorCommand;
  322. function getEditorActions() {
  323. return EditorContributionRegistry.INSTANCE.getEditorActions();
  324. }
  325. EditorExtensionsRegistry.getEditorActions = getEditorActions;
  326. function getEditorContributions() {
  327. return EditorContributionRegistry.INSTANCE.getEditorContributions();
  328. }
  329. EditorExtensionsRegistry.getEditorContributions = getEditorContributions;
  330. function getSomeEditorContributions(ids) {
  331. return EditorContributionRegistry.INSTANCE.getEditorContributions().filter(c => ids.indexOf(c.id) >= 0);
  332. }
  333. EditorExtensionsRegistry.getSomeEditorContributions = getSomeEditorContributions;
  334. function getDiffEditorContributions() {
  335. return EditorContributionRegistry.INSTANCE.getDiffEditorContributions();
  336. }
  337. EditorExtensionsRegistry.getDiffEditorContributions = getDiffEditorContributions;
  338. })(EditorExtensionsRegistry || (EditorExtensionsRegistry = {}));
  339. // Editor extension points
  340. const Extensions = {
  341. EditorCommonContributions: 'editor.contributions'
  342. };
  343. class EditorContributionRegistry {
  344. constructor() {
  345. this.editorContributions = [];
  346. this.diffEditorContributions = [];
  347. this.editorActions = [];
  348. this.editorCommands = Object.create(null);
  349. }
  350. registerEditorContribution(id, ctor) {
  351. this.editorContributions.push({ id, ctor: ctor });
  352. }
  353. getEditorContributions() {
  354. return this.editorContributions.slice(0);
  355. }
  356. getDiffEditorContributions() {
  357. return this.diffEditorContributions.slice(0);
  358. }
  359. registerEditorAction(action) {
  360. action.register();
  361. this.editorActions.push(action);
  362. }
  363. getEditorActions() {
  364. return this.editorActions.slice(0);
  365. }
  366. registerEditorCommand(editorCommand) {
  367. editorCommand.register();
  368. this.editorCommands[editorCommand.id] = editorCommand;
  369. }
  370. getEditorCommand(commandId) {
  371. return (this.editorCommands[commandId] || null);
  372. }
  373. }
  374. EditorContributionRegistry.INSTANCE = new EditorContributionRegistry();
  375. Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE);
  376. function registerCommand(command) {
  377. command.register();
  378. return command;
  379. }
  380. export const UndoCommand = registerCommand(new MultiCommand({
  381. id: 'undo',
  382. precondition: undefined,
  383. kbOpts: {
  384. weight: 0 /* EditorCore */,
  385. primary: 2048 /* CtrlCmd */ | 56 /* KeyZ */
  386. },
  387. menuOpts: [{
  388. menuId: MenuId.MenubarEditMenu,
  389. group: '1_do',
  390. title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"),
  391. order: 1
  392. }, {
  393. menuId: MenuId.CommandPalette,
  394. group: '',
  395. title: nls.localize('undo', "Undo"),
  396. order: 1
  397. }]
  398. }));
  399. registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined }));
  400. export const RedoCommand = registerCommand(new MultiCommand({
  401. id: 'redo',
  402. precondition: undefined,
  403. kbOpts: {
  404. weight: 0 /* EditorCore */,
  405. primary: 2048 /* CtrlCmd */ | 55 /* KeyY */,
  406. secondary: [2048 /* CtrlCmd */ | 1024 /* Shift */ | 56 /* KeyZ */],
  407. mac: { primary: 2048 /* CtrlCmd */ | 1024 /* Shift */ | 56 /* KeyZ */ }
  408. },
  409. menuOpts: [{
  410. menuId: MenuId.MenubarEditMenu,
  411. group: '1_do',
  412. title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"),
  413. order: 2
  414. }, {
  415. menuId: MenuId.CommandPalette,
  416. group: '',
  417. title: nls.localize('redo', "Redo"),
  418. order: 1
  419. }]
  420. }));
  421. registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined }));
  422. export const SelectAllCommand = registerCommand(new MultiCommand({
  423. id: 'editor.action.selectAll',
  424. precondition: undefined,
  425. kbOpts: {
  426. weight: 0 /* EditorCore */,
  427. kbExpr: null,
  428. primary: 2048 /* CtrlCmd */ | 31 /* KeyA */
  429. },
  430. menuOpts: [{
  431. menuId: MenuId.MenubarSelectionMenu,
  432. group: '1_basic',
  433. title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"),
  434. order: 1
  435. }, {
  436. menuId: MenuId.CommandPalette,
  437. group: '',
  438. title: nls.localize('selectAll', "Select All"),
  439. order: 1
  440. }]
  441. }));