linesOperations.js 40 KB

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