41f550677487d5ff4f43cdec0dc2b2c66ae7384714a0ff3f778c10ccd3d4035b3a7099309f69182829ee960063a457c6600103a3b2f91d7b13ec109f2d546e 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  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 { KeyChord } from '../../../../base/common/keyCodes.js';
  6. import { CoreEditingCommands } from '../../../browser/coreCommands.js';
  7. import { EditorAction, registerEditorAction } from '../../../browser/editorExtensions.js';
  8. import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from '../../../common/commands/replaceCommand.js';
  9. import { TrimTrailingWhitespaceCommand } from '../../../common/commands/trimTrailingWhitespaceCommand.js';
  10. import { TypeOperations } from '../../../common/cursor/cursorTypeOperations.js';
  11. import { EditOperation } from '../../../common/core/editOperation.js';
  12. import { Position } from '../../../common/core/position.js';
  13. import { Range } from '../../../common/core/range.js';
  14. import { Selection } from '../../../common/core/selection.js';
  15. import { EditorContextKeys } from '../../../common/editorContextKeys.js';
  16. import { CopyLinesCommand } from './copyLinesCommand.js';
  17. import { MoveLinesCommand } from './moveLinesCommand.js';
  18. import { SortLinesCommand } from './sortLinesCommand.js';
  19. import * as nls from '../../../../nls.js';
  20. import { MenuId } from '../../../../platform/actions/common/actions.js';
  21. import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
  22. // copy lines
  23. class AbstractCopyLinesAction extends EditorAction {
  24. constructor(down, opts) {
  25. super(opts);
  26. this.down = down;
  27. }
  28. run(_accessor, editor) {
  29. if (!editor.hasModel()) {
  30. return;
  31. }
  32. const selections = editor.getSelections().map((selection, index) => ({ selection, index, ignore: false }));
  33. selections.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
  34. // Remove selections that would result in copying the same line
  35. let prev = selections[0];
  36. for (let i = 1; i < selections.length; i++) {
  37. const curr = selections[i];
  38. if (prev.selection.endLineNumber === curr.selection.startLineNumber) {
  39. // these two selections would copy the same line
  40. if (prev.index < curr.index) {
  41. // prev wins
  42. curr.ignore = true;
  43. }
  44. else {
  45. // curr wins
  46. prev.ignore = true;
  47. prev = curr;
  48. }
  49. }
  50. }
  51. const commands = [];
  52. for (const selection of selections) {
  53. commands.push(new CopyLinesCommand(selection.selection, this.down, selection.ignore));
  54. }
  55. editor.pushUndoStop();
  56. editor.executeCommands(this.id, commands);
  57. editor.pushUndoStop();
  58. }
  59. }
  60. class CopyLinesUpAction extends AbstractCopyLinesAction {
  61. constructor() {
  62. super(false, {
  63. id: 'editor.action.copyLinesUpAction',
  64. label: nls.localize('lines.copyUp', "Copy Line Up"),
  65. alias: 'Copy Line Up',
  66. precondition: EditorContextKeys.writable,
  67. kbOpts: {
  68. kbExpr: EditorContextKeys.editorTextFocus,
  69. primary: 512 /* KeyMod.Alt */ | 1024 /* KeyMod.Shift */ | 16 /* KeyCode.UpArrow */,
  70. linux: { primary: 2048 /* KeyMod.CtrlCmd */ | 512 /* KeyMod.Alt */ | 1024 /* KeyMod.Shift */ | 16 /* KeyCode.UpArrow */ },
  71. weight: 100 /* KeybindingWeight.EditorContrib */
  72. },
  73. menuOpts: {
  74. menuId: MenuId.MenubarSelectionMenu,
  75. group: '2_line',
  76. title: nls.localize({ key: 'miCopyLinesUp', comment: ['&& denotes a mnemonic'] }, "&&Copy Line Up"),
  77. order: 1
  78. }
  79. });
  80. }
  81. }
  82. class CopyLinesDownAction extends AbstractCopyLinesAction {
  83. constructor() {
  84. super(true, {
  85. id: 'editor.action.copyLinesDownAction',
  86. label: nls.localize('lines.copyDown', "Copy Line Down"),
  87. alias: 'Copy Line Down',
  88. precondition: EditorContextKeys.writable,
  89. kbOpts: {
  90. kbExpr: EditorContextKeys.editorTextFocus,
  91. primary: 512 /* KeyMod.Alt */ | 1024 /* KeyMod.Shift */ | 18 /* KeyCode.DownArrow */,
  92. linux: { primary: 2048 /* KeyMod.CtrlCmd */ | 512 /* KeyMod.Alt */ | 1024 /* KeyMod.Shift */ | 18 /* KeyCode.DownArrow */ },
  93. weight: 100 /* KeybindingWeight.EditorContrib */
  94. },
  95. menuOpts: {
  96. menuId: MenuId.MenubarSelectionMenu,
  97. group: '2_line',
  98. title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down"),
  99. order: 2
  100. }
  101. });
  102. }
  103. }
  104. export class DuplicateSelectionAction extends EditorAction {
  105. constructor() {
  106. super({
  107. id: 'editor.action.duplicateSelection',
  108. label: nls.localize('duplicateSelection', "Duplicate Selection"),
  109. alias: 'Duplicate Selection',
  110. precondition: EditorContextKeys.writable,
  111. menuOpts: {
  112. menuId: MenuId.MenubarSelectionMenu,
  113. group: '2_line',
  114. title: nls.localize({ key: 'miDuplicateSelection', comment: ['&& denotes a mnemonic'] }, "&&Duplicate Selection"),
  115. order: 5
  116. }
  117. });
  118. }
  119. run(accessor, editor, args) {
  120. if (!editor.hasModel()) {
  121. return;
  122. }
  123. const commands = [];
  124. const selections = editor.getSelections();
  125. const model = editor.getModel();
  126. for (const selection of selections) {
  127. if (selection.isEmpty()) {
  128. commands.push(new CopyLinesCommand(selection, true));
  129. }
  130. else {
  131. const insertSelection = new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn);
  132. commands.push(new ReplaceCommandThatSelectsText(insertSelection, model.getValueInRange(selection)));
  133. }
  134. }
  135. editor.pushUndoStop();
  136. editor.executeCommands(this.id, commands);
  137. editor.pushUndoStop();
  138. }
  139. }
  140. // move lines
  141. class AbstractMoveLinesAction extends EditorAction {
  142. constructor(down, opts) {
  143. super(opts);
  144. this.down = down;
  145. }
  146. run(accessor, editor) {
  147. const languageConfigurationService = accessor.get(ILanguageConfigurationService);
  148. const commands = [];
  149. const selections = editor.getSelections() || [];
  150. const autoIndent = editor.getOption(9 /* EditorOption.autoIndent */);
  151. for (const selection of selections) {
  152. commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));
  153. }
  154. editor.pushUndoStop();
  155. editor.executeCommands(this.id, commands);
  156. editor.pushUndoStop();
  157. }
  158. }
  159. class MoveLinesUpAction extends AbstractMoveLinesAction {
  160. constructor() {
  161. super(false, {
  162. id: 'editor.action.moveLinesUpAction',
  163. label: nls.localize('lines.moveUp', "Move Line Up"),
  164. alias: 'Move Line Up',
  165. precondition: EditorContextKeys.writable,
  166. kbOpts: {
  167. kbExpr: EditorContextKeys.editorTextFocus,
  168. primary: 512 /* KeyMod.Alt */ | 16 /* KeyCode.UpArrow */,
  169. linux: { primary: 512 /* KeyMod.Alt */ | 16 /* KeyCode.UpArrow */ },
  170. weight: 100 /* KeybindingWeight.EditorContrib */
  171. },
  172. menuOpts: {
  173. menuId: MenuId.MenubarSelectionMenu,
  174. group: '2_line',
  175. title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up"),
  176. order: 3
  177. }
  178. });
  179. }
  180. }
  181. class MoveLinesDownAction extends AbstractMoveLinesAction {
  182. constructor() {
  183. super(true, {
  184. id: 'editor.action.moveLinesDownAction',
  185. label: nls.localize('lines.moveDown', "Move Line Down"),
  186. alias: 'Move Line Down',
  187. precondition: EditorContextKeys.writable,
  188. kbOpts: {
  189. kbExpr: EditorContextKeys.editorTextFocus,
  190. primary: 512 /* KeyMod.Alt */ | 18 /* KeyCode.DownArrow */,
  191. linux: { primary: 512 /* KeyMod.Alt */ | 18 /* KeyCode.DownArrow */ },
  192. weight: 100 /* KeybindingWeight.EditorContrib */
  193. },
  194. menuOpts: {
  195. menuId: MenuId.MenubarSelectionMenu,
  196. group: '2_line',
  197. title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down"),
  198. order: 4
  199. }
  200. });
  201. }
  202. }
  203. export class AbstractSortLinesAction extends EditorAction {
  204. constructor(descending, opts) {
  205. super(opts);
  206. this.descending = descending;
  207. }
  208. run(_accessor, editor) {
  209. const selections = editor.getSelections() || [];
  210. for (const selection of selections) {
  211. if (!SortLinesCommand.canRun(editor.getModel(), selection, this.descending)) {
  212. return;
  213. }
  214. }
  215. const commands = [];
  216. for (let i = 0, len = selections.length; i < len; i++) {
  217. commands[i] = new SortLinesCommand(selections[i], this.descending);
  218. }
  219. editor.pushUndoStop();
  220. editor.executeCommands(this.id, commands);
  221. editor.pushUndoStop();
  222. }
  223. }
  224. export class SortLinesAscendingAction extends AbstractSortLinesAction {
  225. constructor() {
  226. super(false, {
  227. id: 'editor.action.sortLinesAscending',
  228. label: nls.localize('lines.sortAscending', "Sort Lines Ascending"),
  229. alias: 'Sort Lines Ascending',
  230. precondition: EditorContextKeys.writable
  231. });
  232. }
  233. }
  234. export class SortLinesDescendingAction extends AbstractSortLinesAction {
  235. constructor() {
  236. super(true, {
  237. id: 'editor.action.sortLinesDescending',
  238. label: nls.localize('lines.sortDescending', "Sort Lines Descending"),
  239. alias: 'Sort Lines Descending',
  240. precondition: EditorContextKeys.writable
  241. });
  242. }
  243. }
  244. export class DeleteDuplicateLinesAction extends EditorAction {
  245. constructor() {
  246. super({
  247. id: 'editor.action.removeDuplicateLines',
  248. label: nls.localize('lines.deleteDuplicates', "Delete Duplicate Lines"),
  249. alias: 'Delete Duplicate Lines',
  250. precondition: EditorContextKeys.writable
  251. });
  252. }
  253. run(_accessor, editor) {
  254. if (!editor.hasModel()) {
  255. return;
  256. }
  257. const model = editor.getModel();
  258. if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
  259. return;
  260. }
  261. const edits = [];
  262. const endCursorState = [];
  263. let linesDeleted = 0;
  264. for (const selection of editor.getSelections()) {
  265. const uniqueLines = new Set();
  266. const lines = [];
  267. for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
  268. const line = model.getLineContent(i);
  269. if (uniqueLines.has(line)) {
  270. continue;
  271. }
  272. lines.push(line);
  273. uniqueLines.add(line);
  274. }
  275. const selectionToReplace = new Selection(selection.startLineNumber, 1, selection.endLineNumber, model.getLineMaxColumn(selection.endLineNumber));
  276. const adjustedSelectionStart = selection.startLineNumber - linesDeleted;
  277. const finalSelection = new Selection(adjustedSelectionStart, 1, adjustedSelectionStart + lines.length - 1, lines[lines.length - 1].length);
  278. edits.push(EditOperation.replace(selectionToReplace, lines.join('\n')));
  279. endCursorState.push(finalSelection);
  280. linesDeleted += (selection.endLineNumber - selection.startLineNumber + 1) - lines.length;
  281. }
  282. editor.pushUndoStop();
  283. editor.executeEdits(this.id, edits, endCursorState);
  284. editor.pushUndoStop();
  285. }
  286. }
  287. export class TrimTrailingWhitespaceAction extends EditorAction {
  288. constructor() {
  289. super({
  290. id: TrimTrailingWhitespaceAction.ID,
  291. label: nls.localize('lines.trimTrailingWhitespace', "Trim Trailing Whitespace"),
  292. alias: 'Trim Trailing Whitespace',
  293. precondition: EditorContextKeys.writable,
  294. kbOpts: {
  295. kbExpr: EditorContextKeys.editorTextFocus,
  296. primary: KeyChord(2048 /* KeyMod.CtrlCmd */ | 41 /* KeyCode.KeyK */, 2048 /* KeyMod.CtrlCmd */ | 54 /* KeyCode.KeyX */),
  297. weight: 100 /* KeybindingWeight.EditorContrib */
  298. }
  299. });
  300. }
  301. run(_accessor, editor, args) {
  302. let cursors = [];
  303. if (args.reason === 'auto-save') {
  304. // See https://github.com/editorconfig/editorconfig-vscode/issues/47
  305. // It is very convenient for the editor config extension to invoke this action.
  306. // So, if we get a reason:'auto-save' passed in, let's preserve cursor positions.
  307. cursors = (editor.getSelections() || []).map(s => new Position(s.positionLineNumber, s.positionColumn));
  308. }
  309. const selection = editor.getSelection();
  310. if (selection === null) {
  311. return;
  312. }
  313. const command = new TrimTrailingWhitespaceCommand(selection, cursors);
  314. editor.pushUndoStop();
  315. editor.executeCommands(this.id, [command]);
  316. editor.pushUndoStop();
  317. }
  318. }
  319. TrimTrailingWhitespaceAction.ID = 'editor.action.trimTrailingWhitespace';
  320. export class DeleteLinesAction extends EditorAction {
  321. constructor() {
  322. super({
  323. id: 'editor.action.deleteLines',
  324. label: nls.localize('lines.delete', "Delete Line"),
  325. alias: 'Delete Line',
  326. precondition: EditorContextKeys.writable,
  327. kbOpts: {
  328. kbExpr: EditorContextKeys.textInputFocus,
  329. primary: 2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 41 /* KeyCode.KeyK */,
  330. weight: 100 /* KeybindingWeight.EditorContrib */
  331. }
  332. });
  333. }
  334. run(_accessor, editor) {
  335. if (!editor.hasModel()) {
  336. return;
  337. }
  338. const ops = this._getLinesToRemove(editor);
  339. const model = editor.getModel();
  340. if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
  341. // Model is empty
  342. return;
  343. }
  344. let linesDeleted = 0;
  345. const edits = [];
  346. const cursorState = [];
  347. for (let i = 0, len = ops.length; i < len; i++) {
  348. const op = ops[i];
  349. let startLineNumber = op.startLineNumber;
  350. let endLineNumber = op.endLineNumber;
  351. let startColumn = 1;
  352. let endColumn = model.getLineMaxColumn(endLineNumber);
  353. if (endLineNumber < model.getLineCount()) {
  354. endLineNumber += 1;
  355. endColumn = 1;
  356. }
  357. else if (startLineNumber > 1) {
  358. startLineNumber -= 1;
  359. startColumn = model.getLineMaxColumn(startLineNumber);
  360. }
  361. edits.push(EditOperation.replace(new Selection(startLineNumber, startColumn, endLineNumber, endColumn), ''));
  362. cursorState.push(new Selection(startLineNumber - linesDeleted, op.positionColumn, startLineNumber - linesDeleted, op.positionColumn));
  363. linesDeleted += (op.endLineNumber - op.startLineNumber + 1);
  364. }
  365. editor.pushUndoStop();
  366. editor.executeEdits(this.id, edits, cursorState);
  367. editor.pushUndoStop();
  368. }
  369. _getLinesToRemove(editor) {
  370. // Construct delete operations
  371. const operations = editor.getSelections().map((s) => {
  372. let endLineNumber = s.endLineNumber;
  373. if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
  374. endLineNumber -= 1;
  375. }
  376. return {
  377. startLineNumber: s.startLineNumber,
  378. selectionStartColumn: s.selectionStartColumn,
  379. endLineNumber: endLineNumber,
  380. positionColumn: s.positionColumn
  381. };
  382. });
  383. // Sort delete operations
  384. operations.sort((a, b) => {
  385. if (a.startLineNumber === b.startLineNumber) {
  386. return a.endLineNumber - b.endLineNumber;
  387. }
  388. return a.startLineNumber - b.startLineNumber;
  389. });
  390. // Merge delete operations which are adjacent or overlapping
  391. const mergedOperations = [];
  392. let previousOperation = operations[0];
  393. for (let i = 1; i < operations.length; i++) {
  394. if (previousOperation.endLineNumber + 1 >= operations[i].startLineNumber) {
  395. // Merge current operations into the previous one
  396. previousOperation.endLineNumber = operations[i].endLineNumber;
  397. }
  398. else {
  399. // Push previous operation
  400. mergedOperations.push(previousOperation);
  401. previousOperation = operations[i];
  402. }
  403. }
  404. // Push the last operation
  405. mergedOperations.push(previousOperation);
  406. return mergedOperations;
  407. }
  408. }
  409. export class IndentLinesAction extends EditorAction {
  410. constructor() {
  411. super({
  412. id: 'editor.action.indentLines',
  413. label: nls.localize('lines.indent', "Indent Line"),
  414. alias: 'Indent Line',
  415. precondition: EditorContextKeys.writable,
  416. kbOpts: {
  417. kbExpr: EditorContextKeys.editorTextFocus,
  418. primary: 2048 /* KeyMod.CtrlCmd */ | 89 /* KeyCode.BracketRight */,
  419. weight: 100 /* KeybindingWeight.EditorContrib */
  420. }
  421. });
  422. }
  423. run(_accessor, editor) {
  424. const viewModel = editor._getViewModel();
  425. if (!viewModel) {
  426. return;
  427. }
  428. editor.pushUndoStop();
  429. editor.executeCommands(this.id, TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
  430. editor.pushUndoStop();
  431. }
  432. }
  433. class OutdentLinesAction extends EditorAction {
  434. constructor() {
  435. super({
  436. id: 'editor.action.outdentLines',
  437. label: nls.localize('lines.outdent', "Outdent Line"),
  438. alias: 'Outdent Line',
  439. precondition: EditorContextKeys.writable,
  440. kbOpts: {
  441. kbExpr: EditorContextKeys.editorTextFocus,
  442. primary: 2048 /* KeyMod.CtrlCmd */ | 87 /* KeyCode.BracketLeft */,
  443. weight: 100 /* KeybindingWeight.EditorContrib */
  444. }
  445. });
  446. }
  447. run(_accessor, editor) {
  448. CoreEditingCommands.Outdent.runEditorCommand(_accessor, editor, null);
  449. }
  450. }
  451. export class InsertLineBeforeAction extends EditorAction {
  452. constructor() {
  453. super({
  454. id: 'editor.action.insertLineBefore',
  455. label: nls.localize('lines.insertBefore', "Insert Line Above"),
  456. alias: 'Insert Line Above',
  457. precondition: EditorContextKeys.writable,
  458. kbOpts: {
  459. kbExpr: EditorContextKeys.editorTextFocus,
  460. primary: 2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 3 /* KeyCode.Enter */,
  461. weight: 100 /* KeybindingWeight.EditorContrib */
  462. }
  463. });
  464. }
  465. run(_accessor, editor) {
  466. const viewModel = editor._getViewModel();
  467. if (!viewModel) {
  468. return;
  469. }
  470. editor.pushUndoStop();
  471. editor.executeCommands(this.id, TypeOperations.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
  472. }
  473. }
  474. export class InsertLineAfterAction extends EditorAction {
  475. constructor() {
  476. super({
  477. id: 'editor.action.insertLineAfter',
  478. label: nls.localize('lines.insertAfter', "Insert Line Below"),
  479. alias: 'Insert Line Below',
  480. precondition: EditorContextKeys.writable,
  481. kbOpts: {
  482. kbExpr: EditorContextKeys.editorTextFocus,
  483. primary: 2048 /* KeyMod.CtrlCmd */ | 3 /* KeyCode.Enter */,
  484. weight: 100 /* KeybindingWeight.EditorContrib */
  485. }
  486. });
  487. }
  488. run(_accessor, editor) {
  489. const viewModel = editor._getViewModel();
  490. if (!viewModel) {
  491. return;
  492. }
  493. editor.pushUndoStop();
  494. editor.executeCommands(this.id, TypeOperations.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
  495. }
  496. }
  497. export class AbstractDeleteAllToBoundaryAction extends EditorAction {
  498. run(_accessor, editor) {
  499. if (!editor.hasModel()) {
  500. return;
  501. }
  502. const primaryCursor = editor.getSelection();
  503. const rangesToDelete = this._getRangesToDelete(editor);
  504. // merge overlapping selections
  505. const effectiveRanges = [];
  506. for (let i = 0, count = rangesToDelete.length - 1; i < count; i++) {
  507. const range = rangesToDelete[i];
  508. const nextRange = rangesToDelete[i + 1];
  509. if (Range.intersectRanges(range, nextRange) === null) {
  510. effectiveRanges.push(range);
  511. }
  512. else {
  513. rangesToDelete[i + 1] = Range.plusRange(range, nextRange);
  514. }
  515. }
  516. effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]);
  517. const endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges);
  518. const edits = effectiveRanges.map(range => {
  519. return EditOperation.replace(range, '');
  520. });
  521. editor.pushUndoStop();
  522. editor.executeEdits(this.id, edits, endCursorState);
  523. editor.pushUndoStop();
  524. }
  525. }
  526. export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction {
  527. constructor() {
  528. super({
  529. id: 'deleteAllLeft',
  530. label: nls.localize('lines.deleteAllLeft', "Delete All Left"),
  531. alias: 'Delete All Left',
  532. precondition: EditorContextKeys.writable,
  533. kbOpts: {
  534. kbExpr: EditorContextKeys.textInputFocus,
  535. primary: 0,
  536. mac: { primary: 2048 /* KeyMod.CtrlCmd */ | 1 /* KeyCode.Backspace */ },
  537. weight: 100 /* KeybindingWeight.EditorContrib */
  538. }
  539. });
  540. }
  541. _getEndCursorState(primaryCursor, rangesToDelete) {
  542. let endPrimaryCursor = null;
  543. const endCursorState = [];
  544. let deletedLines = 0;
  545. rangesToDelete.forEach(range => {
  546. let endCursor;
  547. if (range.endColumn === 1 && deletedLines > 0) {
  548. const newStartLine = range.startLineNumber - deletedLines;
  549. endCursor = new Selection(newStartLine, range.startColumn, newStartLine, range.startColumn);
  550. }
  551. else {
  552. endCursor = new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn);
  553. }
  554. deletedLines += range.endLineNumber - range.startLineNumber;
  555. if (range.intersectRanges(primaryCursor)) {
  556. endPrimaryCursor = endCursor;
  557. }
  558. else {
  559. endCursorState.push(endCursor);
  560. }
  561. });
  562. if (endPrimaryCursor) {
  563. endCursorState.unshift(endPrimaryCursor);
  564. }
  565. return endCursorState;
  566. }
  567. _getRangesToDelete(editor) {
  568. const selections = editor.getSelections();
  569. if (selections === null) {
  570. return [];
  571. }
  572. let rangesToDelete = selections;
  573. const model = editor.getModel();
  574. if (model === null) {
  575. return [];
  576. }
  577. rangesToDelete.sort(Range.compareRangesUsingStarts);
  578. rangesToDelete = rangesToDelete.map(selection => {
  579. if (selection.isEmpty()) {
  580. if (selection.startColumn === 1) {
  581. const deleteFromLine = Math.max(1, selection.startLineNumber - 1);
  582. const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineContent(deleteFromLine).length + 1;
  583. return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1);
  584. }
  585. else {
  586. return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);
  587. }
  588. }
  589. else {
  590. return new Range(selection.startLineNumber, 1, selection.endLineNumber, selection.endColumn);
  591. }
  592. });
  593. return rangesToDelete;
  594. }
  595. }
  596. export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction {
  597. constructor() {
  598. super({
  599. id: 'deleteAllRight',
  600. label: nls.localize('lines.deleteAllRight', "Delete All Right"),
  601. alias: 'Delete All Right',
  602. precondition: EditorContextKeys.writable,
  603. kbOpts: {
  604. kbExpr: EditorContextKeys.textInputFocus,
  605. primary: 0,
  606. mac: { primary: 256 /* KeyMod.WinCtrl */ | 41 /* KeyCode.KeyK */, secondary: [2048 /* KeyMod.CtrlCmd */ | 20 /* KeyCode.Delete */] },
  607. weight: 100 /* KeybindingWeight.EditorContrib */
  608. }
  609. });
  610. }
  611. _getEndCursorState(primaryCursor, rangesToDelete) {
  612. let endPrimaryCursor = null;
  613. const endCursorState = [];
  614. for (let i = 0, len = rangesToDelete.length, offset = 0; i < len; i++) {
  615. const range = rangesToDelete[i];
  616. const endCursor = new Selection(range.startLineNumber - offset, range.startColumn, range.startLineNumber - offset, range.startColumn);
  617. if (range.intersectRanges(primaryCursor)) {
  618. endPrimaryCursor = endCursor;
  619. }
  620. else {
  621. endCursorState.push(endCursor);
  622. }
  623. }
  624. if (endPrimaryCursor) {
  625. endCursorState.unshift(endPrimaryCursor);
  626. }
  627. return endCursorState;
  628. }
  629. _getRangesToDelete(editor) {
  630. const model = editor.getModel();
  631. if (model === null) {
  632. return [];
  633. }
  634. const selections = editor.getSelections();
  635. if (selections === null) {
  636. return [];
  637. }
  638. const rangesToDelete = selections.map((sel) => {
  639. if (sel.isEmpty()) {
  640. const maxColumn = model.getLineMaxColumn(sel.startLineNumber);
  641. if (sel.startColumn === maxColumn) {
  642. return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);
  643. }
  644. else {
  645. return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);
  646. }
  647. }
  648. return sel;
  649. });
  650. rangesToDelete.sort(Range.compareRangesUsingStarts);
  651. return rangesToDelete;
  652. }
  653. }
  654. export class JoinLinesAction extends EditorAction {
  655. constructor() {
  656. super({
  657. id: 'editor.action.joinLines',
  658. label: nls.localize('lines.joinLines', "Join Lines"),
  659. alias: 'Join Lines',
  660. precondition: EditorContextKeys.writable,
  661. kbOpts: {
  662. kbExpr: EditorContextKeys.editorTextFocus,
  663. primary: 0,
  664. mac: { primary: 256 /* KeyMod.WinCtrl */ | 40 /* KeyCode.KeyJ */ },
  665. weight: 100 /* KeybindingWeight.EditorContrib */
  666. }
  667. });
  668. }
  669. run(_accessor, editor) {
  670. const selections = editor.getSelections();
  671. if (selections === null) {
  672. return;
  673. }
  674. let primaryCursor = editor.getSelection();
  675. if (primaryCursor === null) {
  676. return;
  677. }
  678. selections.sort(Range.compareRangesUsingStarts);
  679. const reducedSelections = [];
  680. const lastSelection = selections.reduce((previousValue, currentValue) => {
  681. if (previousValue.isEmpty()) {
  682. if (previousValue.endLineNumber === currentValue.startLineNumber) {
  683. if (primaryCursor.equalsSelection(previousValue)) {
  684. primaryCursor = currentValue;
  685. }
  686. return currentValue;
  687. }
  688. if (currentValue.startLineNumber > previousValue.endLineNumber + 1) {
  689. reducedSelections.push(previousValue);
  690. return currentValue;
  691. }
  692. else {
  693. return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);
  694. }
  695. }
  696. else {
  697. if (currentValue.startLineNumber > previousValue.endLineNumber) {
  698. reducedSelections.push(previousValue);
  699. return currentValue;
  700. }
  701. else {
  702. return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);
  703. }
  704. }
  705. });
  706. reducedSelections.push(lastSelection);
  707. const model = editor.getModel();
  708. if (model === null) {
  709. return;
  710. }
  711. const edits = [];
  712. const endCursorState = [];
  713. let endPrimaryCursor = primaryCursor;
  714. let lineOffset = 0;
  715. for (let i = 0, len = reducedSelections.length; i < len; i++) {
  716. const selection = reducedSelections[i];
  717. const startLineNumber = selection.startLineNumber;
  718. const startColumn = 1;
  719. let columnDeltaOffset = 0;
  720. let endLineNumber, endColumn;
  721. const selectionEndPositionOffset = model.getLineContent(selection.endLineNumber).length - selection.endColumn;
  722. if (selection.isEmpty() || selection.startLineNumber === selection.endLineNumber) {
  723. const position = selection.getStartPosition();
  724. if (position.lineNumber < model.getLineCount()) {
  725. endLineNumber = startLineNumber + 1;
  726. endColumn = model.getLineMaxColumn(endLineNumber);
  727. }
  728. else {
  729. endLineNumber = position.lineNumber;
  730. endColumn = model.getLineMaxColumn(position.lineNumber);
  731. }
  732. }
  733. else {
  734. endLineNumber = selection.endLineNumber;
  735. endColumn = model.getLineMaxColumn(endLineNumber);
  736. }
  737. let trimmedLinesContent = model.getLineContent(startLineNumber);
  738. for (let i = startLineNumber + 1; i <= endLineNumber; i++) {
  739. const lineText = model.getLineContent(i);
  740. const firstNonWhitespaceIdx = model.getLineFirstNonWhitespaceColumn(i);
  741. if (firstNonWhitespaceIdx >= 1) {
  742. let insertSpace = true;
  743. if (trimmedLinesContent === '') {
  744. insertSpace = false;
  745. }
  746. if (insertSpace && (trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' ||
  747. trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t')) {
  748. insertSpace = false;
  749. trimmedLinesContent = trimmedLinesContent.replace(/[\s\uFEFF\xA0]+$/g, ' ');
  750. }
  751. const lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx - 1);
  752. trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent;
  753. if (insertSpace) {
  754. columnDeltaOffset = lineTextWithoutIndent.length + 1;
  755. }
  756. else {
  757. columnDeltaOffset = lineTextWithoutIndent.length;
  758. }
  759. }
  760. else {
  761. columnDeltaOffset = 0;
  762. }
  763. }
  764. const deleteSelection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
  765. if (!deleteSelection.isEmpty()) {
  766. let resultSelection;
  767. if (selection.isEmpty()) {
  768. edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
  769. resultSelection = new Selection(deleteSelection.startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1, startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1);
  770. }
  771. else {
  772. if (selection.startLineNumber === selection.endLineNumber) {
  773. edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
  774. resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn, selection.endLineNumber - lineOffset, selection.endColumn);
  775. }
  776. else {
  777. edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
  778. resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn, selection.startLineNumber - lineOffset, trimmedLinesContent.length - selectionEndPositionOffset);
  779. }
  780. }
  781. if (Range.intersectRanges(deleteSelection, primaryCursor) !== null) {
  782. endPrimaryCursor = resultSelection;
  783. }
  784. else {
  785. endCursorState.push(resultSelection);
  786. }
  787. }
  788. lineOffset += deleteSelection.endLineNumber - deleteSelection.startLineNumber;
  789. }
  790. endCursorState.unshift(endPrimaryCursor);
  791. editor.pushUndoStop();
  792. editor.executeEdits(this.id, edits, endCursorState);
  793. editor.pushUndoStop();
  794. }
  795. }
  796. export class TransposeAction extends EditorAction {
  797. constructor() {
  798. super({
  799. id: 'editor.action.transpose',
  800. label: nls.localize('editor.transpose', "Transpose characters around the cursor"),
  801. alias: 'Transpose characters around the cursor',
  802. precondition: EditorContextKeys.writable
  803. });
  804. }
  805. run(_accessor, editor) {
  806. const selections = editor.getSelections();
  807. if (selections === null) {
  808. return;
  809. }
  810. const model = editor.getModel();
  811. if (model === null) {
  812. return;
  813. }
  814. const commands = [];
  815. for (let i = 0, len = selections.length; i < len; i++) {
  816. const selection = selections[i];
  817. if (!selection.isEmpty()) {
  818. continue;
  819. }
  820. const cursor = selection.getStartPosition();
  821. const maxColumn = model.getLineMaxColumn(cursor.lineNumber);
  822. if (cursor.column >= maxColumn) {
  823. if (cursor.lineNumber === model.getLineCount()) {
  824. continue;
  825. }
  826. // The cursor is at the end of current line and current line is not empty
  827. // then we transpose the character before the cursor and the line break if there is any following line.
  828. const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1);
  829. const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');
  830. commands.push(new ReplaceCommand(new Selection(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1), chars));
  831. }
  832. else {
  833. const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber, cursor.column + 1);
  834. const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');
  835. commands.push(new ReplaceCommandThatPreservesSelection(deleteSelection, chars, new Selection(cursor.lineNumber, cursor.column + 1, cursor.lineNumber, cursor.column + 1)));
  836. }
  837. }
  838. editor.pushUndoStop();
  839. editor.executeCommands(this.id, commands);
  840. editor.pushUndoStop();
  841. }
  842. }
  843. export class AbstractCaseAction extends EditorAction {
  844. run(_accessor, editor) {
  845. const selections = editor.getSelections();
  846. if (selections === null) {
  847. return;
  848. }
  849. const model = editor.getModel();
  850. if (model === null) {
  851. return;
  852. }
  853. const wordSeparators = editor.getOption(119 /* EditorOption.wordSeparators */);
  854. const textEdits = [];
  855. for (const selection of selections) {
  856. if (selection.isEmpty()) {
  857. const cursor = selection.getStartPosition();
  858. const word = editor.getConfiguredWordAtPosition(cursor);
  859. if (!word) {
  860. continue;
  861. }
  862. const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn);
  863. const text = model.getValueInRange(wordRange);
  864. textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators)));
  865. }
  866. else {
  867. const text = model.getValueInRange(selection);
  868. textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators)));
  869. }
  870. }
  871. editor.pushUndoStop();
  872. editor.executeEdits(this.id, textEdits);
  873. editor.pushUndoStop();
  874. }
  875. }
  876. export class UpperCaseAction extends AbstractCaseAction {
  877. constructor() {
  878. super({
  879. id: 'editor.action.transformToUppercase',
  880. label: nls.localize('editor.transformToUppercase', "Transform to Uppercase"),
  881. alias: 'Transform to Uppercase',
  882. precondition: EditorContextKeys.writable
  883. });
  884. }
  885. _modifyText(text, wordSeparators) {
  886. return text.toLocaleUpperCase();
  887. }
  888. }
  889. export class LowerCaseAction extends AbstractCaseAction {
  890. constructor() {
  891. super({
  892. id: 'editor.action.transformToLowercase',
  893. label: nls.localize('editor.transformToLowercase', "Transform to Lowercase"),
  894. alias: 'Transform to Lowercase',
  895. precondition: EditorContextKeys.writable
  896. });
  897. }
  898. _modifyText(text, wordSeparators) {
  899. return text.toLocaleLowerCase();
  900. }
  901. }
  902. class BackwardsCompatibleRegExp {
  903. constructor(_pattern, _flags) {
  904. this._pattern = _pattern;
  905. this._flags = _flags;
  906. this._actual = null;
  907. this._evaluated = false;
  908. }
  909. get() {
  910. if (!this._evaluated) {
  911. this._evaluated = true;
  912. try {
  913. this._actual = new RegExp(this._pattern, this._flags);
  914. }
  915. catch (err) {
  916. // this browser does not support this regular expression
  917. }
  918. }
  919. return this._actual;
  920. }
  921. isSupported() {
  922. return (this.get() !== null);
  923. }
  924. }
  925. export class TitleCaseAction extends AbstractCaseAction {
  926. constructor() {
  927. super({
  928. id: 'editor.action.transformToTitlecase',
  929. label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"),
  930. alias: 'Transform to Title Case',
  931. precondition: EditorContextKeys.writable
  932. });
  933. }
  934. _modifyText(text, wordSeparators) {
  935. const titleBoundary = TitleCaseAction.titleBoundary.get();
  936. if (!titleBoundary) {
  937. // cannot support this
  938. return text;
  939. }
  940. return text
  941. .toLocaleLowerCase()
  942. .replace(titleBoundary, (b) => b.toLocaleUpperCase());
  943. }
  944. }
  945. TitleCaseAction.titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu');
  946. export class SnakeCaseAction extends AbstractCaseAction {
  947. constructor() {
  948. super({
  949. id: 'editor.action.transformToSnakecase',
  950. label: nls.localize('editor.transformToSnakecase', "Transform to Snake Case"),
  951. alias: 'Transform to Snake Case',
  952. precondition: EditorContextKeys.writable
  953. });
  954. }
  955. _modifyText(text, wordSeparators) {
  956. const caseBoundary = SnakeCaseAction.caseBoundary.get();
  957. const singleLetters = SnakeCaseAction.singleLetters.get();
  958. if (!caseBoundary || !singleLetters) {
  959. // cannot support this
  960. return text;
  961. }
  962. return (text
  963. .replace(caseBoundary, '$1_$2')
  964. .replace(singleLetters, '$1_$2$3')
  965. .toLocaleLowerCase());
  966. }
  967. }
  968. SnakeCaseAction.caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
  969. SnakeCaseAction.singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');
  970. export class KebabCaseAction extends AbstractCaseAction {
  971. constructor() {
  972. super({
  973. id: 'editor.action.transformToKebabcase',
  974. label: nls.localize('editor.transformToKebabcase', 'Transform to Kebab Case'),
  975. alias: 'Transform to Kebab Case',
  976. precondition: EditorContextKeys.writable
  977. });
  978. }
  979. static isSupported() {
  980. const areAllRegexpsSupported = [
  981. this.caseBoundary,
  982. this.singleLetters,
  983. this.underscoreBoundary,
  984. ].every((regexp) => regexp.isSupported());
  985. return areAllRegexpsSupported;
  986. }
  987. _modifyText(text, _) {
  988. const caseBoundary = KebabCaseAction.caseBoundary.get();
  989. const singleLetters = KebabCaseAction.singleLetters.get();
  990. const underscoreBoundary = KebabCaseAction.underscoreBoundary.get();
  991. if (!caseBoundary || !singleLetters || !underscoreBoundary) {
  992. // one or more regexps aren't supported
  993. return text;
  994. }
  995. return text
  996. .replace(underscoreBoundary, '$1-$3')
  997. .replace(caseBoundary, '$1-$2')
  998. .replace(singleLetters, '$1-$2')
  999. .toLocaleLowerCase();
  1000. }
  1001. }
  1002. KebabCaseAction.caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
  1003. KebabCaseAction.singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu}\\p{Ll})', 'gmu');
  1004. KebabCaseAction.underscoreBoundary = new BackwardsCompatibleRegExp('(\\S)(_)(\\S)', 'gm');
  1005. registerEditorAction(CopyLinesUpAction);
  1006. registerEditorAction(CopyLinesDownAction);
  1007. registerEditorAction(DuplicateSelectionAction);
  1008. registerEditorAction(MoveLinesUpAction);
  1009. registerEditorAction(MoveLinesDownAction);
  1010. registerEditorAction(SortLinesAscendingAction);
  1011. registerEditorAction(SortLinesDescendingAction);
  1012. registerEditorAction(DeleteDuplicateLinesAction);
  1013. registerEditorAction(TrimTrailingWhitespaceAction);
  1014. registerEditorAction(DeleteLinesAction);
  1015. registerEditorAction(IndentLinesAction);
  1016. registerEditorAction(OutdentLinesAction);
  1017. registerEditorAction(InsertLineBeforeAction);
  1018. registerEditorAction(InsertLineAfterAction);
  1019. registerEditorAction(DeleteAllLeftAction);
  1020. registerEditorAction(DeleteAllRightAction);
  1021. registerEditorAction(JoinLinesAction);
  1022. registerEditorAction(TransposeAction);
  1023. registerEditorAction(UpperCaseAction);
  1024. registerEditorAction(LowerCaseAction);
  1025. if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) {
  1026. registerEditorAction(SnakeCaseAction);
  1027. }
  1028. if (TitleCaseAction.titleBoundary.isSupported()) {
  1029. registerEditorAction(TitleCaseAction);
  1030. }
  1031. if (KebabCaseAction.isSupported()) {
  1032. registerEditorAction(KebabCaseAction);
  1033. }