moveLinesCommand.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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 strings from '../../../base/common/strings.js';
  6. import { ShiftCommand } from '../../common/commands/shiftCommand.js';
  7. import { Range } from '../../common/core/range.js';
  8. import { Selection } from '../../common/core/selection.js';
  9. import { IndentAction } from '../../common/modes/languageConfiguration.js';
  10. import { LanguageConfigurationRegistry } from '../../common/modes/languageConfigurationRegistry.js';
  11. import * as indentUtils from '../indentation/indentUtils.js';
  12. export class MoveLinesCommand {
  13. constructor(selection, isMovingDown, autoIndent) {
  14. this._selection = selection;
  15. this._isMovingDown = isMovingDown;
  16. this._autoIndent = autoIndent;
  17. this._selectionId = null;
  18. this._moveEndLineSelectionShrink = false;
  19. }
  20. getEditOperations(model, builder) {
  21. let modelLineCount = model.getLineCount();
  22. if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) {
  23. this._selectionId = builder.trackSelection(this._selection);
  24. return;
  25. }
  26. if (!this._isMovingDown && this._selection.startLineNumber === 1) {
  27. this._selectionId = builder.trackSelection(this._selection);
  28. return;
  29. }
  30. this._moveEndPositionDown = false;
  31. let s = this._selection;
  32. if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
  33. this._moveEndPositionDown = true;
  34. s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
  35. }
  36. const { tabSize, indentSize, insertSpaces } = model.getOptions();
  37. let indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces);
  38. let virtualModel = {
  39. getLineTokens: (lineNumber) => {
  40. return model.getLineTokens(lineNumber);
  41. },
  42. getLanguageId: () => {
  43. return model.getLanguageId();
  44. },
  45. getLanguageIdAtPosition: (lineNumber, column) => {
  46. return model.getLanguageIdAtPosition(lineNumber, column);
  47. },
  48. getLineContent: null,
  49. };
  50. if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) {
  51. // Current line is empty
  52. let lineNumber = s.startLineNumber;
  53. let otherLineNumber = (this._isMovingDown ? lineNumber + 1 : lineNumber - 1);
  54. if (model.getLineMaxColumn(otherLineNumber) === 1) {
  55. // Other line number is empty too, so no editing is needed
  56. // Add a no-op to force running by the model
  57. builder.addEditOperation(new Range(1, 1, 1, 1), null);
  58. }
  59. else {
  60. // Type content from other line number on line number
  61. builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), model.getLineContent(otherLineNumber));
  62. // Remove content from other line number
  63. builder.addEditOperation(new Range(otherLineNumber, 1, otherLineNumber, model.getLineMaxColumn(otherLineNumber)), null);
  64. }
  65. // Track selection at the other line number
  66. s = new Selection(otherLineNumber, 1, otherLineNumber, 1);
  67. }
  68. else {
  69. let movingLineNumber;
  70. let movingLineText;
  71. if (this._isMovingDown) {
  72. movingLineNumber = s.endLineNumber + 1;
  73. movingLineText = model.getLineContent(movingLineNumber);
  74. // Delete line that needs to be moved
  75. builder.addEditOperation(new Range(movingLineNumber - 1, model.getLineMaxColumn(movingLineNumber - 1), movingLineNumber, model.getLineMaxColumn(movingLineNumber)), null);
  76. let insertingText = movingLineText;
  77. if (this.shouldAutoIndent(model, s)) {
  78. let movingLineMatchResult = this.matchEnterRule(model, indentConverter, tabSize, movingLineNumber, s.startLineNumber - 1);
  79. // if s.startLineNumber - 1 matches onEnter rule, we still honor that.
  80. if (movingLineMatchResult !== null) {
  81. let oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));
  82. let newSpaceCnt = movingLineMatchResult + indentUtils.getSpaceCnt(oldIndentation, tabSize);
  83. let newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);
  84. insertingText = newIndentation + this.trimLeft(movingLineText);
  85. }
  86. else {
  87. // no enter rule matches, let's check indentatin rules then.
  88. virtualModel.getLineContent = (lineNumber) => {
  89. if (lineNumber === s.startLineNumber) {
  90. return model.getLineContent(movingLineNumber);
  91. }
  92. else {
  93. return model.getLineContent(lineNumber);
  94. }
  95. };
  96. let indentOfMovingLine = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition(movingLineNumber, 1), s.startLineNumber, indentConverter);
  97. if (indentOfMovingLine !== null) {
  98. let oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));
  99. let newSpaceCnt = indentUtils.getSpaceCnt(indentOfMovingLine, tabSize);
  100. let oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
  101. if (newSpaceCnt !== oldSpaceCnt) {
  102. let newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);
  103. insertingText = newIndentation + this.trimLeft(movingLineText);
  104. }
  105. }
  106. }
  107. // add edit operations for moving line first to make sure it's executed after we make indentation change
  108. // to s.startLineNumber
  109. builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');
  110. let ret = this.matchEnterRuleMovingDown(model, indentConverter, tabSize, s.startLineNumber, movingLineNumber, insertingText);
  111. // check if the line being moved before matches onEnter rules, if so let's adjust the indentation by onEnter rules.
  112. if (ret !== null) {
  113. if (ret !== 0) {
  114. this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);
  115. }
  116. }
  117. else {
  118. // it doesn't match onEnter rules, let's check indentation rules then.
  119. virtualModel.getLineContent = (lineNumber) => {
  120. if (lineNumber === s.startLineNumber) {
  121. return insertingText;
  122. }
  123. else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {
  124. return model.getLineContent(lineNumber - 1);
  125. }
  126. else {
  127. return model.getLineContent(lineNumber);
  128. }
  129. };
  130. let newIndentatOfMovingBlock = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition(movingLineNumber, 1), s.startLineNumber + 1, indentConverter);
  131. if (newIndentatOfMovingBlock !== null) {
  132. const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));
  133. const newSpaceCnt = indentUtils.getSpaceCnt(newIndentatOfMovingBlock, tabSize);
  134. const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
  135. if (newSpaceCnt !== oldSpaceCnt) {
  136. const spaceCntOffset = newSpaceCnt - oldSpaceCnt;
  137. this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);
  138. }
  139. }
  140. }
  141. }
  142. else {
  143. // Insert line that needs to be moved before
  144. builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');
  145. }
  146. }
  147. else {
  148. movingLineNumber = s.startLineNumber - 1;
  149. movingLineText = model.getLineContent(movingLineNumber);
  150. // Delete line that needs to be moved
  151. builder.addEditOperation(new Range(movingLineNumber, 1, movingLineNumber + 1, 1), null);
  152. // Insert line that needs to be moved after
  153. builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText);
  154. if (this.shouldAutoIndent(model, s)) {
  155. virtualModel.getLineContent = (lineNumber) => {
  156. if (lineNumber === movingLineNumber) {
  157. return model.getLineContent(s.startLineNumber);
  158. }
  159. else {
  160. return model.getLineContent(lineNumber);
  161. }
  162. };
  163. let ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2);
  164. // check if s.startLineNumber - 2 matches onEnter rules, if so adjust the moving block by onEnter rules.
  165. if (ret !== null) {
  166. if (ret !== 0) {
  167. this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);
  168. }
  169. }
  170. else {
  171. // it doesn't match any onEnter rule, let's check indentation rules then.
  172. let indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition(s.startLineNumber, 1), movingLineNumber, indentConverter);
  173. if (indentOfFirstLine !== null) {
  174. // adjust the indentation of the moving block
  175. let oldIndent = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));
  176. let newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize);
  177. let oldSpaceCnt = indentUtils.getSpaceCnt(oldIndent, tabSize);
  178. if (newSpaceCnt !== oldSpaceCnt) {
  179. let spaceCntOffset = newSpaceCnt - oldSpaceCnt;
  180. this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);
  181. }
  182. }
  183. }
  184. }
  185. }
  186. }
  187. this._selectionId = builder.trackSelection(s);
  188. }
  189. buildIndentConverter(tabSize, indentSize, insertSpaces) {
  190. return {
  191. shiftIndent: (indentation) => {
  192. return ShiftCommand.shiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);
  193. },
  194. unshiftIndent: (indentation) => {
  195. return ShiftCommand.unshiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);
  196. }
  197. };
  198. }
  199. parseEnterResult(model, indentConverter, tabSize, line, enter) {
  200. if (enter) {
  201. let enterPrefix = enter.indentation;
  202. if (enter.indentAction === IndentAction.None) {
  203. enterPrefix = enter.indentation + enter.appendText;
  204. }
  205. else if (enter.indentAction === IndentAction.Indent) {
  206. enterPrefix = enter.indentation + enter.appendText;
  207. }
  208. else if (enter.indentAction === IndentAction.IndentOutdent) {
  209. enterPrefix = enter.indentation;
  210. }
  211. else if (enter.indentAction === IndentAction.Outdent) {
  212. enterPrefix = indentConverter.unshiftIndent(enter.indentation) + enter.appendText;
  213. }
  214. let movingLineText = model.getLineContent(line);
  215. if (this.trimLeft(movingLineText).indexOf(this.trimLeft(enterPrefix)) >= 0) {
  216. let oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line));
  217. let newIndentation = strings.getLeadingWhitespace(enterPrefix);
  218. let indentMetadataOfMovelingLine = LanguageConfigurationRegistry.getIndentMetadata(model, line);
  219. if (indentMetadataOfMovelingLine !== null && indentMetadataOfMovelingLine & 2 /* DECREASE_MASK */) {
  220. newIndentation = indentConverter.unshiftIndent(newIndentation);
  221. }
  222. let newSpaceCnt = indentUtils.getSpaceCnt(newIndentation, tabSize);
  223. let oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
  224. return newSpaceCnt - oldSpaceCnt;
  225. }
  226. }
  227. return null;
  228. }
  229. /**
  230. *
  231. * @param model
  232. * @param indentConverter
  233. * @param tabSize
  234. * @param line the line moving down
  235. * @param futureAboveLineNumber the line which will be at the `line` position
  236. * @param futureAboveLineText
  237. */
  238. matchEnterRuleMovingDown(model, indentConverter, tabSize, line, futureAboveLineNumber, futureAboveLineText) {
  239. if (strings.lastNonWhitespaceIndex(futureAboveLineText) >= 0) {
  240. // break
  241. let maxColumn = model.getLineMaxColumn(futureAboveLineNumber);
  242. let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn));
  243. return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
  244. }
  245. else {
  246. // go upwards, starting from `line - 1`
  247. let validPrecedingLine = line - 1;
  248. while (validPrecedingLine >= 1) {
  249. let lineContent = model.getLineContent(validPrecedingLine);
  250. let nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);
  251. if (nonWhitespaceIdx >= 0) {
  252. break;
  253. }
  254. validPrecedingLine--;
  255. }
  256. if (validPrecedingLine < 1 || line > model.getLineCount()) {
  257. return null;
  258. }
  259. let maxColumn = model.getLineMaxColumn(validPrecedingLine);
  260. let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn));
  261. return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
  262. }
  263. }
  264. matchEnterRule(model, indentConverter, tabSize, line, oneLineAbove, previousLineText) {
  265. let validPrecedingLine = oneLineAbove;
  266. while (validPrecedingLine >= 1) {
  267. // ship empty lines as empty lines just inherit indentation
  268. let lineContent;
  269. if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) {
  270. lineContent = previousLineText;
  271. }
  272. else {
  273. lineContent = model.getLineContent(validPrecedingLine);
  274. }
  275. let nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);
  276. if (nonWhitespaceIdx >= 0) {
  277. break;
  278. }
  279. validPrecedingLine--;
  280. }
  281. if (validPrecedingLine < 1 || line > model.getLineCount()) {
  282. return null;
  283. }
  284. let maxColumn = model.getLineMaxColumn(validPrecedingLine);
  285. let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn));
  286. return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
  287. }
  288. trimLeft(str) {
  289. return str.replace(/^\s+/, '');
  290. }
  291. shouldAutoIndent(model, selection) {
  292. if (this._autoIndent < 4 /* Full */) {
  293. return false;
  294. }
  295. // if it's not easy to tokenize, we stop auto indent.
  296. if (!model.isCheapToTokenize(selection.startLineNumber)) {
  297. return false;
  298. }
  299. let languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1);
  300. let languageAtSelectionEnd = model.getLanguageIdAtPosition(selection.endLineNumber, 1);
  301. if (languageAtSelectionStart !== languageAtSelectionEnd) {
  302. return false;
  303. }
  304. if (LanguageConfigurationRegistry.getIndentRulesSupport(languageAtSelectionStart) === null) {
  305. return false;
  306. }
  307. return true;
  308. }
  309. getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, offset) {
  310. for (let i = s.startLineNumber; i <= s.endLineNumber; i++) {
  311. let lineContent = model.getLineContent(i);
  312. let originalIndent = strings.getLeadingWhitespace(lineContent);
  313. let originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize);
  314. let newSpacesCnt = originalSpacesCnt + offset;
  315. let newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces);
  316. if (newIndent !== originalIndent) {
  317. builder.addEditOperation(new Range(i, 1, i, originalIndent.length + 1), newIndent);
  318. if (i === s.endLineNumber && s.endColumn <= originalIndent.length + 1 && newIndent === '') {
  319. // as users select part of the original indent white spaces
  320. // when we adjust the indentation of endLine, we should adjust the cursor position as well.
  321. this._moveEndLineSelectionShrink = true;
  322. }
  323. }
  324. }
  325. }
  326. computeCursorState(model, helper) {
  327. let result = helper.getTrackedSelection(this._selectionId);
  328. if (this._moveEndPositionDown) {
  329. result = result.setEndPosition(result.endLineNumber + 1, 1);
  330. }
  331. if (this._moveEndLineSelectionShrink && result.startLineNumber < result.endLineNumber) {
  332. result = result.setEndPosition(result.endLineNumber, 2);
  333. }
  334. return result;
  335. }
  336. }