shiftCommand.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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 { CursorColumns } from '../controller/cursorCommon.js';
  7. import { Range } from '../core/range.js';
  8. import { Selection } from '../core/selection.js';
  9. import { LanguageConfigurationRegistry } from '../modes/languageConfigurationRegistry.js';
  10. const repeatCache = Object.create(null);
  11. export function cachedStringRepeat(str, count) {
  12. if (count <= 0) {
  13. return '';
  14. }
  15. if (!repeatCache[str]) {
  16. repeatCache[str] = ['', str];
  17. }
  18. const cache = repeatCache[str];
  19. for (let i = cache.length; i <= count; i++) {
  20. cache[i] = cache[i - 1] + str;
  21. }
  22. return cache[count];
  23. }
  24. export class ShiftCommand {
  25. constructor(range, opts) {
  26. this._opts = opts;
  27. this._selection = range;
  28. this._selectionId = null;
  29. this._useLastEditRangeForCursorEndPosition = false;
  30. this._selectionStartColumnStaysPut = false;
  31. }
  32. static unshiftIndent(line, column, tabSize, indentSize, insertSpaces) {
  33. // Determine the visible column where the content starts
  34. const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);
  35. if (insertSpaces) {
  36. const indent = cachedStringRepeat(' ', indentSize);
  37. const desiredTabStop = CursorColumns.prevIndentTabStop(contentStartVisibleColumn, indentSize);
  38. const indentCount = desiredTabStop / indentSize; // will be an integer
  39. return cachedStringRepeat(indent, indentCount);
  40. }
  41. else {
  42. const indent = '\t';
  43. const desiredTabStop = CursorColumns.prevRenderTabStop(contentStartVisibleColumn, tabSize);
  44. const indentCount = desiredTabStop / tabSize; // will be an integer
  45. return cachedStringRepeat(indent, indentCount);
  46. }
  47. }
  48. static shiftIndent(line, column, tabSize, indentSize, insertSpaces) {
  49. // Determine the visible column where the content starts
  50. const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);
  51. if (insertSpaces) {
  52. const indent = cachedStringRepeat(' ', indentSize);
  53. const desiredTabStop = CursorColumns.nextIndentTabStop(contentStartVisibleColumn, indentSize);
  54. const indentCount = desiredTabStop / indentSize; // will be an integer
  55. return cachedStringRepeat(indent, indentCount);
  56. }
  57. else {
  58. const indent = '\t';
  59. const desiredTabStop = CursorColumns.nextRenderTabStop(contentStartVisibleColumn, tabSize);
  60. const indentCount = desiredTabStop / tabSize; // will be an integer
  61. return cachedStringRepeat(indent, indentCount);
  62. }
  63. }
  64. _addEditOperation(builder, range, text) {
  65. if (this._useLastEditRangeForCursorEndPosition) {
  66. builder.addTrackedEditOperation(range, text);
  67. }
  68. else {
  69. builder.addEditOperation(range, text);
  70. }
  71. }
  72. getEditOperations(model, builder) {
  73. const startLine = this._selection.startLineNumber;
  74. let endLine = this._selection.endLineNumber;
  75. if (this._selection.endColumn === 1 && startLine !== endLine) {
  76. endLine = endLine - 1;
  77. }
  78. const { tabSize, indentSize, insertSpaces } = this._opts;
  79. const shouldIndentEmptyLines = (startLine === endLine);
  80. if (this._opts.useTabStops) {
  81. // if indenting or outdenting on a whitespace only line
  82. if (this._selection.isEmpty()) {
  83. if (/^\s*$/.test(model.getLineContent(startLine))) {
  84. this._useLastEditRangeForCursorEndPosition = true;
  85. }
  86. }
  87. // keep track of previous line's "miss-alignment"
  88. let previousLineExtraSpaces = 0, extraSpaces = 0;
  89. for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++, previousLineExtraSpaces = extraSpaces) {
  90. extraSpaces = 0;
  91. let lineText = model.getLineContent(lineNumber);
  92. let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);
  93. if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {
  94. // empty line or line with no leading whitespace => nothing to do
  95. continue;
  96. }
  97. if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {
  98. // do not indent empty lines => nothing to do
  99. continue;
  100. }
  101. if (indentationEndIndex === -1) {
  102. // the entire line is whitespace
  103. indentationEndIndex = lineText.length;
  104. }
  105. if (lineNumber > 1) {
  106. let contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(lineText, indentationEndIndex + 1, tabSize);
  107. if (contentStartVisibleColumn % indentSize !== 0) {
  108. // The current line is "miss-aligned", so let's see if this is expected...
  109. // This can only happen when it has trailing commas in the indent
  110. if (model.isCheapToTokenize(lineNumber - 1)) {
  111. let enterAction = LanguageConfigurationRegistry.getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)));
  112. if (enterAction) {
  113. extraSpaces = previousLineExtraSpaces;
  114. if (enterAction.appendText) {
  115. for (let j = 0, lenJ = enterAction.appendText.length; j < lenJ && extraSpaces < indentSize; j++) {
  116. if (enterAction.appendText.charCodeAt(j) === 32 /* Space */) {
  117. extraSpaces++;
  118. }
  119. else {
  120. break;
  121. }
  122. }
  123. }
  124. if (enterAction.removeText) {
  125. extraSpaces = Math.max(0, extraSpaces - enterAction.removeText);
  126. }
  127. // Act as if `prefixSpaces` is not part of the indentation
  128. for (let j = 0; j < extraSpaces; j++) {
  129. if (indentationEndIndex === 0 || lineText.charCodeAt(indentationEndIndex - 1) !== 32 /* Space */) {
  130. break;
  131. }
  132. indentationEndIndex--;
  133. }
  134. }
  135. }
  136. }
  137. }
  138. if (this._opts.isUnshift && indentationEndIndex === 0) {
  139. // line with no leading whitespace => nothing to do
  140. continue;
  141. }
  142. let desiredIndent;
  143. if (this._opts.isUnshift) {
  144. desiredIndent = ShiftCommand.unshiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);
  145. }
  146. else {
  147. desiredIndent = ShiftCommand.shiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);
  148. }
  149. this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), desiredIndent);
  150. if (lineNumber === startLine && !this._selection.isEmpty()) {
  151. // Force the startColumn to stay put because we're inserting after it
  152. this._selectionStartColumnStaysPut = (this._selection.startColumn <= indentationEndIndex + 1);
  153. }
  154. }
  155. }
  156. else {
  157. // if indenting or outdenting on a whitespace only line
  158. if (!this._opts.isUnshift && this._selection.isEmpty() && model.getLineLength(startLine) === 0) {
  159. this._useLastEditRangeForCursorEndPosition = true;
  160. }
  161. const oneIndent = (insertSpaces ? cachedStringRepeat(' ', indentSize) : '\t');
  162. for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
  163. const lineText = model.getLineContent(lineNumber);
  164. let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);
  165. if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {
  166. // empty line or line with no leading whitespace => nothing to do
  167. continue;
  168. }
  169. if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {
  170. // do not indent empty lines => nothing to do
  171. continue;
  172. }
  173. if (indentationEndIndex === -1) {
  174. // the entire line is whitespace
  175. indentationEndIndex = lineText.length;
  176. }
  177. if (this._opts.isUnshift && indentationEndIndex === 0) {
  178. // line with no leading whitespace => nothing to do
  179. continue;
  180. }
  181. if (this._opts.isUnshift) {
  182. indentationEndIndex = Math.min(indentationEndIndex, indentSize);
  183. for (let i = 0; i < indentationEndIndex; i++) {
  184. const chr = lineText.charCodeAt(i);
  185. if (chr === 9 /* Tab */) {
  186. indentationEndIndex = i + 1;
  187. break;
  188. }
  189. }
  190. this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), '');
  191. }
  192. else {
  193. this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, 1), oneIndent);
  194. if (lineNumber === startLine && !this._selection.isEmpty()) {
  195. // Force the startColumn to stay put because we're inserting after it
  196. this._selectionStartColumnStaysPut = (this._selection.startColumn === 1);
  197. }
  198. }
  199. }
  200. }
  201. this._selectionId = builder.trackSelection(this._selection);
  202. }
  203. computeCursorState(model, helper) {
  204. if (this._useLastEditRangeForCursorEndPosition) {
  205. let lastOp = helper.getInverseEditOperations()[0];
  206. return new Selection(lastOp.range.endLineNumber, lastOp.range.endColumn, lastOp.range.endLineNumber, lastOp.range.endColumn);
  207. }
  208. const result = helper.getTrackedSelection(this._selectionId);
  209. if (this._selectionStartColumnStaysPut) {
  210. // The selection start should not move
  211. let initialStartColumn = this._selection.startColumn;
  212. let resultStartColumn = result.startColumn;
  213. if (resultStartColumn <= initialStartColumn) {
  214. return result;
  215. }
  216. if (result.getDirection() === 0 /* LTR */) {
  217. return new Selection(result.startLineNumber, initialStartColumn, result.endLineNumber, result.endColumn);
  218. }
  219. return new Selection(result.endLineNumber, result.endColumn, result.startLineNumber, initialStartColumn);
  220. }
  221. return result;
  222. }
  223. }