lineCommentCommand.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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 { EditOperation } from '../../common/core/editOperation.js';
  7. import { Position } from '../../common/core/position.js';
  8. import { Range } from '../../common/core/range.js';
  9. import { Selection } from '../../common/core/selection.js';
  10. import { LanguageConfigurationRegistry } from '../../common/modes/languageConfigurationRegistry.js';
  11. import { BlockCommentCommand } from './blockCommentCommand.js';
  12. export class LineCommentCommand {
  13. constructor(selection, tabSize, type, insertSpace, ignoreEmptyLines, ignoreFirstLine) {
  14. this._selection = selection;
  15. this._tabSize = tabSize;
  16. this._type = type;
  17. this._insertSpace = insertSpace;
  18. this._selectionId = null;
  19. this._deltaColumn = 0;
  20. this._moveEndPositionDown = false;
  21. this._ignoreEmptyLines = ignoreEmptyLines;
  22. this._ignoreFirstLine = ignoreFirstLine || false;
  23. }
  24. /**
  25. * Do an initial pass over the lines and gather info about the line comment string.
  26. * Returns null if any of the lines doesn't support a line comment string.
  27. */
  28. static _gatherPreflightCommentStrings(model, startLineNumber, endLineNumber) {
  29. model.tokenizeIfCheap(startLineNumber);
  30. const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);
  31. const config = LanguageConfigurationRegistry.getComments(languageId);
  32. const commentStr = (config ? config.lineCommentToken : null);
  33. if (!commentStr) {
  34. // Mode does not support line comments
  35. return null;
  36. }
  37. let lines = [];
  38. for (let i = 0, lineCount = endLineNumber - startLineNumber + 1; i < lineCount; i++) {
  39. lines[i] = {
  40. ignore: false,
  41. commentStr: commentStr,
  42. commentStrOffset: 0,
  43. commentStrLength: commentStr.length
  44. };
  45. }
  46. return lines;
  47. }
  48. /**
  49. * Analyze lines and decide which lines are relevant and what the toggle should do.
  50. * Also, build up several offsets and lengths useful in the generation of editor operations.
  51. */
  52. static _analyzeLines(type, insertSpace, model, lines, startLineNumber, ignoreEmptyLines, ignoreFirstLine) {
  53. let onlyWhitespaceLines = true;
  54. let shouldRemoveComments;
  55. if (type === 0 /* Toggle */) {
  56. shouldRemoveComments = true;
  57. }
  58. else if (type === 1 /* ForceAdd */) {
  59. shouldRemoveComments = false;
  60. }
  61. else {
  62. shouldRemoveComments = true;
  63. }
  64. for (let i = 0, lineCount = lines.length; i < lineCount; i++) {
  65. const lineData = lines[i];
  66. const lineNumber = startLineNumber + i;
  67. if (lineNumber === startLineNumber && ignoreFirstLine) {
  68. // first line ignored
  69. lineData.ignore = true;
  70. continue;
  71. }
  72. const lineContent = model.getLineContent(lineNumber);
  73. const lineContentStartOffset = strings.firstNonWhitespaceIndex(lineContent);
  74. if (lineContentStartOffset === -1) {
  75. // Empty or whitespace only line
  76. lineData.ignore = ignoreEmptyLines;
  77. lineData.commentStrOffset = lineContent.length;
  78. continue;
  79. }
  80. onlyWhitespaceLines = false;
  81. lineData.ignore = false;
  82. lineData.commentStrOffset = lineContentStartOffset;
  83. if (shouldRemoveComments && !BlockCommentCommand._haystackHasNeedleAtOffset(lineContent, lineData.commentStr, lineContentStartOffset)) {
  84. if (type === 0 /* Toggle */) {
  85. // Every line so far has been a line comment, but this one is not
  86. shouldRemoveComments = false;
  87. }
  88. else if (type === 1 /* ForceAdd */) {
  89. // Will not happen
  90. }
  91. else {
  92. lineData.ignore = true;
  93. }
  94. }
  95. if (shouldRemoveComments && insertSpace) {
  96. // Remove a following space if present
  97. const commentStrEndOffset = lineContentStartOffset + lineData.commentStrLength;
  98. if (commentStrEndOffset < lineContent.length && lineContent.charCodeAt(commentStrEndOffset) === 32 /* Space */) {
  99. lineData.commentStrLength += 1;
  100. }
  101. }
  102. }
  103. if (type === 0 /* Toggle */ && onlyWhitespaceLines) {
  104. // For only whitespace lines, we insert comments
  105. shouldRemoveComments = false;
  106. // Also, no longer ignore them
  107. for (let i = 0, lineCount = lines.length; i < lineCount; i++) {
  108. lines[i].ignore = false;
  109. }
  110. }
  111. return {
  112. supported: true,
  113. shouldRemoveComments: shouldRemoveComments,
  114. lines: lines
  115. };
  116. }
  117. /**
  118. * Analyze all lines and decide exactly what to do => not supported | insert line comments | remove line comments
  119. */
  120. static _gatherPreflightData(type, insertSpace, model, startLineNumber, endLineNumber, ignoreEmptyLines, ignoreFirstLine) {
  121. const lines = LineCommentCommand._gatherPreflightCommentStrings(model, startLineNumber, endLineNumber);
  122. if (lines === null) {
  123. return {
  124. supported: false
  125. };
  126. }
  127. return LineCommentCommand._analyzeLines(type, insertSpace, model, lines, startLineNumber, ignoreEmptyLines, ignoreFirstLine);
  128. }
  129. /**
  130. * Given a successful analysis, execute either insert line comments, either remove line comments
  131. */
  132. _executeLineComments(model, builder, data, s) {
  133. let ops;
  134. if (data.shouldRemoveComments) {
  135. ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber);
  136. }
  137. else {
  138. LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._tabSize);
  139. ops = this._createAddLineCommentsOperations(data.lines, s.startLineNumber);
  140. }
  141. const cursorPosition = new Position(s.positionLineNumber, s.positionColumn);
  142. for (let i = 0, len = ops.length; i < len; i++) {
  143. builder.addEditOperation(ops[i].range, ops[i].text);
  144. if (Range.isEmpty(ops[i].range) && Range.getStartPosition(ops[i].range).equals(cursorPosition)) {
  145. const lineContent = model.getLineContent(cursorPosition.lineNumber);
  146. if (lineContent.length + 1 === cursorPosition.column) {
  147. this._deltaColumn = (ops[i].text || '').length;
  148. }
  149. }
  150. }
  151. this._selectionId = builder.trackSelection(s);
  152. }
  153. _attemptRemoveBlockComment(model, s, startToken, endToken) {
  154. let startLineNumber = s.startLineNumber;
  155. let endLineNumber = s.endLineNumber;
  156. let startTokenAllowedBeforeColumn = endToken.length + Math.max(model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.startColumn);
  157. let startTokenIndex = model.getLineContent(startLineNumber).lastIndexOf(startToken, startTokenAllowedBeforeColumn - 1);
  158. let endTokenIndex = model.getLineContent(endLineNumber).indexOf(endToken, s.endColumn - 1 - startToken.length);
  159. if (startTokenIndex !== -1 && endTokenIndex === -1) {
  160. endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
  161. endLineNumber = startLineNumber;
  162. }
  163. if (startTokenIndex === -1 && endTokenIndex !== -1) {
  164. startTokenIndex = model.getLineContent(endLineNumber).lastIndexOf(startToken, endTokenIndex);
  165. startLineNumber = endLineNumber;
  166. }
  167. if (s.isEmpty() && (startTokenIndex === -1 || endTokenIndex === -1)) {
  168. startTokenIndex = model.getLineContent(startLineNumber).indexOf(startToken);
  169. if (startTokenIndex !== -1) {
  170. endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
  171. }
  172. }
  173. // We have to adjust to possible inner white space.
  174. // For Space after startToken, add Space to startToken - range math will work out.
  175. if (startTokenIndex !== -1 && model.getLineContent(startLineNumber).charCodeAt(startTokenIndex + startToken.length) === 32 /* Space */) {
  176. startToken += ' ';
  177. }
  178. // For Space before endToken, add Space before endToken and shift index one left.
  179. if (endTokenIndex !== -1 && model.getLineContent(endLineNumber).charCodeAt(endTokenIndex - 1) === 32 /* Space */) {
  180. endToken = ' ' + endToken;
  181. endTokenIndex -= 1;
  182. }
  183. if (startTokenIndex !== -1 && endTokenIndex !== -1) {
  184. return BlockCommentCommand._createRemoveBlockCommentOperations(new Range(startLineNumber, startTokenIndex + startToken.length + 1, endLineNumber, endTokenIndex + 1), startToken, endToken);
  185. }
  186. return null;
  187. }
  188. /**
  189. * Given an unsuccessful analysis, delegate to the block comment command
  190. */
  191. _executeBlockComment(model, builder, s) {
  192. model.tokenizeIfCheap(s.startLineNumber);
  193. let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1);
  194. let config = LanguageConfigurationRegistry.getComments(languageId);
  195. if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
  196. // Mode does not support block comments
  197. return;
  198. }
  199. const startToken = config.blockCommentStartToken;
  200. const endToken = config.blockCommentEndToken;
  201. let ops = this._attemptRemoveBlockComment(model, s, startToken, endToken);
  202. if (!ops) {
  203. if (s.isEmpty()) {
  204. const lineContent = model.getLineContent(s.startLineNumber);
  205. let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
  206. if (firstNonWhitespaceIndex === -1) {
  207. // Line is empty or contains only whitespace
  208. firstNonWhitespaceIndex = lineContent.length;
  209. }
  210. ops = BlockCommentCommand._createAddBlockCommentOperations(new Range(s.startLineNumber, firstNonWhitespaceIndex + 1, s.startLineNumber, lineContent.length + 1), startToken, endToken, this._insertSpace);
  211. }
  212. else {
  213. ops = BlockCommentCommand._createAddBlockCommentOperations(new Range(s.startLineNumber, model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), startToken, endToken, this._insertSpace);
  214. }
  215. if (ops.length === 1) {
  216. // Leave cursor after token and Space
  217. this._deltaColumn = startToken.length + 1;
  218. }
  219. }
  220. this._selectionId = builder.trackSelection(s);
  221. for (const op of ops) {
  222. builder.addEditOperation(op.range, op.text);
  223. }
  224. }
  225. getEditOperations(model, builder) {
  226. let s = this._selection;
  227. this._moveEndPositionDown = false;
  228. if (s.startLineNumber === s.endLineNumber && this._ignoreFirstLine) {
  229. builder.addEditOperation(new Range(s.startLineNumber, model.getLineMaxColumn(s.startLineNumber), s.startLineNumber + 1, 1), s.startLineNumber === model.getLineCount() ? '' : '\n');
  230. this._selectionId = builder.trackSelection(s);
  231. return;
  232. }
  233. if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
  234. this._moveEndPositionDown = true;
  235. s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
  236. }
  237. const data = LineCommentCommand._gatherPreflightData(this._type, this._insertSpace, model, s.startLineNumber, s.endLineNumber, this._ignoreEmptyLines, this._ignoreFirstLine);
  238. if (data.supported) {
  239. return this._executeLineComments(model, builder, data, s);
  240. }
  241. return this._executeBlockComment(model, builder, s);
  242. }
  243. computeCursorState(model, helper) {
  244. let result = helper.getTrackedSelection(this._selectionId);
  245. if (this._moveEndPositionDown) {
  246. result = result.setEndPosition(result.endLineNumber + 1, 1);
  247. }
  248. return new Selection(result.selectionStartLineNumber, result.selectionStartColumn + this._deltaColumn, result.positionLineNumber, result.positionColumn + this._deltaColumn);
  249. }
  250. /**
  251. * Generate edit operations in the remove line comment case
  252. */
  253. static _createRemoveLineCommentsOperations(lines, startLineNumber) {
  254. let res = [];
  255. for (let i = 0, len = lines.length; i < len; i++) {
  256. const lineData = lines[i];
  257. if (lineData.ignore) {
  258. continue;
  259. }
  260. res.push(EditOperation.delete(new Range(startLineNumber + i, lineData.commentStrOffset + 1, startLineNumber + i, lineData.commentStrOffset + lineData.commentStrLength + 1)));
  261. }
  262. return res;
  263. }
  264. /**
  265. * Generate edit operations in the add line comment case
  266. */
  267. _createAddLineCommentsOperations(lines, startLineNumber) {
  268. let res = [];
  269. const afterCommentStr = this._insertSpace ? ' ' : '';
  270. for (let i = 0, len = lines.length; i < len; i++) {
  271. const lineData = lines[i];
  272. if (lineData.ignore) {
  273. continue;
  274. }
  275. res.push(EditOperation.insert(new Position(startLineNumber + i, lineData.commentStrOffset + 1), lineData.commentStr + afterCommentStr));
  276. }
  277. return res;
  278. }
  279. static nextVisibleColumn(currentVisibleColumn, tabSize, isTab, columnSize) {
  280. if (isTab) {
  281. return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize));
  282. }
  283. return currentVisibleColumn + columnSize;
  284. }
  285. /**
  286. * Adjust insertion points to have them vertically aligned in the add line comment case
  287. */
  288. static _normalizeInsertionPoint(model, lines, startLineNumber, tabSize) {
  289. let minVisibleColumn = 1073741824 /* MAX_SAFE_SMALL_INTEGER */;
  290. let j;
  291. let lenJ;
  292. for (let i = 0, len = lines.length; i < len; i++) {
  293. if (lines[i].ignore) {
  294. continue;
  295. }
  296. const lineContent = model.getLineContent(startLineNumber + i);
  297. let currentVisibleColumn = 0;
  298. for (let j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
  299. currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === 9 /* Tab */, 1);
  300. }
  301. if (currentVisibleColumn < minVisibleColumn) {
  302. minVisibleColumn = currentVisibleColumn;
  303. }
  304. }
  305. minVisibleColumn = Math.floor(minVisibleColumn / tabSize) * tabSize;
  306. for (let i = 0, len = lines.length; i < len; i++) {
  307. if (lines[i].ignore) {
  308. continue;
  309. }
  310. const lineContent = model.getLineContent(startLineNumber + i);
  311. let currentVisibleColumn = 0;
  312. for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
  313. currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === 9 /* Tab */, 1);
  314. }
  315. if (currentVisibleColumn > minVisibleColumn) {
  316. lines[i].commentStrOffset = j - 1;
  317. }
  318. else {
  319. lines[i].commentStrOffset = j;
  320. }
  321. }
  322. }
  323. }