b5ae508dae077d782f07cc4401285261d9411495bda131bbef13141e10de2eed9d37e4d0b2d96ef9b64e98b31aeecd862b687322b4aa8e61271bc0f0a633fb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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 { Emitter } from '../../../../base/common/event.js';
  6. import * as strings from '../../../../base/common/strings.js';
  7. import { Range } from '../../core/range.js';
  8. import { ApplyEditsResult } from '../../model.js';
  9. import { PieceTreeBase } from './pieceTreeBase.js';
  10. import { countEOL } from '../../core/eolCounter.js';
  11. import { TextChange } from '../../core/textChange.js';
  12. import { Disposable } from '../../../../base/common/lifecycle.js';
  13. export class PieceTreeTextBuffer extends Disposable {
  14. constructor(chunks, BOM, eol, containsRTL, containsUnusualLineTerminators, isBasicASCII, eolNormalized) {
  15. super();
  16. this._onDidChangeContent = this._register(new Emitter());
  17. this._BOM = BOM;
  18. this._mightContainNonBasicASCII = !isBasicASCII;
  19. this._mightContainRTL = containsRTL;
  20. this._mightContainUnusualLineTerminators = containsUnusualLineTerminators;
  21. this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized);
  22. }
  23. mightContainRTL() {
  24. return this._mightContainRTL;
  25. }
  26. mightContainUnusualLineTerminators() {
  27. return this._mightContainUnusualLineTerminators;
  28. }
  29. resetMightContainUnusualLineTerminators() {
  30. this._mightContainUnusualLineTerminators = false;
  31. }
  32. mightContainNonBasicASCII() {
  33. return this._mightContainNonBasicASCII;
  34. }
  35. getBOM() {
  36. return this._BOM;
  37. }
  38. getEOL() {
  39. return this._pieceTree.getEOL();
  40. }
  41. createSnapshot(preserveBOM) {
  42. return this._pieceTree.createSnapshot(preserveBOM ? this._BOM : '');
  43. }
  44. getOffsetAt(lineNumber, column) {
  45. return this._pieceTree.getOffsetAt(lineNumber, column);
  46. }
  47. getPositionAt(offset) {
  48. return this._pieceTree.getPositionAt(offset);
  49. }
  50. getRangeAt(start, length) {
  51. const end = start + length;
  52. const startPosition = this.getPositionAt(start);
  53. const endPosition = this.getPositionAt(end);
  54. return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
  55. }
  56. getValueInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
  57. if (range.isEmpty()) {
  58. return '';
  59. }
  60. const lineEnding = this._getEndOfLine(eol);
  61. return this._pieceTree.getValueInRange(range, lineEnding);
  62. }
  63. getValueLengthInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
  64. if (range.isEmpty()) {
  65. return 0;
  66. }
  67. if (range.startLineNumber === range.endLineNumber) {
  68. return (range.endColumn - range.startColumn);
  69. }
  70. const startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn);
  71. const endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn);
  72. return endOffset - startOffset;
  73. }
  74. getCharacterCountInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
  75. if (this._mightContainNonBasicASCII) {
  76. // we must count by iterating
  77. let result = 0;
  78. const fromLineNumber = range.startLineNumber;
  79. const toLineNumber = range.endLineNumber;
  80. for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
  81. const lineContent = this.getLineContent(lineNumber);
  82. const fromOffset = (lineNumber === fromLineNumber ? range.startColumn - 1 : 0);
  83. const toOffset = (lineNumber === toLineNumber ? range.endColumn - 1 : lineContent.length);
  84. for (let offset = fromOffset; offset < toOffset; offset++) {
  85. if (strings.isHighSurrogate(lineContent.charCodeAt(offset))) {
  86. result = result + 1;
  87. offset = offset + 1;
  88. }
  89. else {
  90. result = result + 1;
  91. }
  92. }
  93. }
  94. result += this._getEndOfLine(eol).length * (toLineNumber - fromLineNumber);
  95. return result;
  96. }
  97. return this.getValueLengthInRange(range, eol);
  98. }
  99. getLength() {
  100. return this._pieceTree.getLength();
  101. }
  102. getLineCount() {
  103. return this._pieceTree.getLineCount();
  104. }
  105. getLinesContent() {
  106. return this._pieceTree.getLinesContent();
  107. }
  108. getLineContent(lineNumber) {
  109. return this._pieceTree.getLineContent(lineNumber);
  110. }
  111. getLineCharCode(lineNumber, index) {
  112. return this._pieceTree.getLineCharCode(lineNumber, index);
  113. }
  114. getLineLength(lineNumber) {
  115. return this._pieceTree.getLineLength(lineNumber);
  116. }
  117. getLineFirstNonWhitespaceColumn(lineNumber) {
  118. const result = strings.firstNonWhitespaceIndex(this.getLineContent(lineNumber));
  119. if (result === -1) {
  120. return 0;
  121. }
  122. return result + 1;
  123. }
  124. getLineLastNonWhitespaceColumn(lineNumber) {
  125. const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber));
  126. if (result === -1) {
  127. return 0;
  128. }
  129. return result + 2;
  130. }
  131. _getEndOfLine(eol) {
  132. switch (eol) {
  133. case 1 /* EndOfLinePreference.LF */:
  134. return '\n';
  135. case 2 /* EndOfLinePreference.CRLF */:
  136. return '\r\n';
  137. case 0 /* EndOfLinePreference.TextDefined */:
  138. return this.getEOL();
  139. default:
  140. throw new Error('Unknown EOL preference');
  141. }
  142. }
  143. setEOL(newEOL) {
  144. this._pieceTree.setEOL(newEOL);
  145. }
  146. applyEdits(rawOperations, recordTrimAutoWhitespace, computeUndoEdits) {
  147. let mightContainRTL = this._mightContainRTL;
  148. let mightContainUnusualLineTerminators = this._mightContainUnusualLineTerminators;
  149. let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
  150. let canReduceOperations = true;
  151. let operations = [];
  152. for (let i = 0; i < rawOperations.length; i++) {
  153. const op = rawOperations[i];
  154. if (canReduceOperations && op._isTracked) {
  155. canReduceOperations = false;
  156. }
  157. const validatedRange = op.range;
  158. if (op.text) {
  159. let textMightContainNonBasicASCII = true;
  160. if (!mightContainNonBasicASCII) {
  161. textMightContainNonBasicASCII = !strings.isBasicASCII(op.text);
  162. mightContainNonBasicASCII = textMightContainNonBasicASCII;
  163. }
  164. if (!mightContainRTL && textMightContainNonBasicASCII) {
  165. // check if the new inserted text contains RTL
  166. mightContainRTL = strings.containsRTL(op.text);
  167. }
  168. if (!mightContainUnusualLineTerminators && textMightContainNonBasicASCII) {
  169. // check if the new inserted text contains unusual line terminators
  170. mightContainUnusualLineTerminators = strings.containsUnusualLineTerminators(op.text);
  171. }
  172. }
  173. let validText = '';
  174. let eolCount = 0;
  175. let firstLineLength = 0;
  176. let lastLineLength = 0;
  177. if (op.text) {
  178. let strEOL;
  179. [eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);
  180. const bufferEOL = this.getEOL();
  181. const expectedStrEOL = (bufferEOL === '\r\n' ? 2 /* StringEOL.CRLF */ : 1 /* StringEOL.LF */);
  182. if (strEOL === 0 /* StringEOL.Unknown */ || strEOL === expectedStrEOL) {
  183. validText = op.text;
  184. }
  185. else {
  186. validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);
  187. }
  188. }
  189. operations[i] = {
  190. sortIndex: i,
  191. identifier: op.identifier || null,
  192. range: validatedRange,
  193. rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),
  194. rangeLength: this.getValueLengthInRange(validatedRange),
  195. text: validText,
  196. eolCount: eolCount,
  197. firstLineLength: firstLineLength,
  198. lastLineLength: lastLineLength,
  199. forceMoveMarkers: Boolean(op.forceMoveMarkers),
  200. isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false
  201. };
  202. }
  203. // Sort operations ascending
  204. operations.sort(PieceTreeTextBuffer._sortOpsAscending);
  205. let hasTouchingRanges = false;
  206. for (let i = 0, count = operations.length - 1; i < count; i++) {
  207. const rangeEnd = operations[i].range.getEndPosition();
  208. const nextRangeStart = operations[i + 1].range.getStartPosition();
  209. if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
  210. if (nextRangeStart.isBefore(rangeEnd)) {
  211. // overlapping ranges
  212. throw new Error('Overlapping ranges are not allowed!');
  213. }
  214. hasTouchingRanges = true;
  215. }
  216. }
  217. if (canReduceOperations) {
  218. operations = this._reduceOperations(operations);
  219. }
  220. // Delta encode operations
  221. const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
  222. const newTrimAutoWhitespaceCandidates = [];
  223. if (recordTrimAutoWhitespace) {
  224. for (let i = 0; i < operations.length; i++) {
  225. const op = operations[i];
  226. const reverseRange = reverseRanges[i];
  227. if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {
  228. // Record already the future line numbers that might be auto whitespace removal candidates on next edit
  229. for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
  230. let currentLineContent = '';
  231. if (lineNumber === reverseRange.startLineNumber) {
  232. currentLineContent = this.getLineContent(op.range.startLineNumber);
  233. if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
  234. continue;
  235. }
  236. }
  237. newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
  238. }
  239. }
  240. }
  241. }
  242. let reverseOperations = null;
  243. if (computeUndoEdits) {
  244. let reverseRangeDeltaOffset = 0;
  245. reverseOperations = [];
  246. for (let i = 0; i < operations.length; i++) {
  247. const op = operations[i];
  248. const reverseRange = reverseRanges[i];
  249. const bufferText = this.getValueInRange(op.range);
  250. const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
  251. reverseRangeDeltaOffset += (op.text.length - bufferText.length);
  252. reverseOperations[i] = {
  253. sortIndex: op.sortIndex,
  254. identifier: op.identifier,
  255. range: reverseRange,
  256. text: bufferText,
  257. textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
  258. };
  259. }
  260. // Can only sort reverse operations when the order is not significant
  261. if (!hasTouchingRanges) {
  262. reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
  263. }
  264. }
  265. this._mightContainRTL = mightContainRTL;
  266. this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators;
  267. this._mightContainNonBasicASCII = mightContainNonBasicASCII;
  268. const contentChanges = this._doApplyEdits(operations);
  269. let trimAutoWhitespaceLineNumbers = null;
  270. if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) {
  271. // sort line numbers auto whitespace removal candidates for next edit descending
  272. newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber);
  273. trimAutoWhitespaceLineNumbers = [];
  274. for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) {
  275. const lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber;
  276. if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) {
  277. // Do not have the same line number twice
  278. continue;
  279. }
  280. const prevContent = newTrimAutoWhitespaceCandidates[i].oldContent;
  281. const lineContent = this.getLineContent(lineNumber);
  282. if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) {
  283. continue;
  284. }
  285. trimAutoWhitespaceLineNumbers.push(lineNumber);
  286. }
  287. }
  288. this._onDidChangeContent.fire();
  289. return new ApplyEditsResult(reverseOperations, contentChanges, trimAutoWhitespaceLineNumbers);
  290. }
  291. /**
  292. * Transform operations such that they represent the same logic edit,
  293. * but that they also do not cause OOM crashes.
  294. */
  295. _reduceOperations(operations) {
  296. if (operations.length < 1000) {
  297. // We know from empirical testing that a thousand edits work fine regardless of their shape.
  298. return operations;
  299. }
  300. // At one point, due to how events are emitted and how each operation is handled,
  301. // some operations can trigger a high amount of temporary string allocations,
  302. // that will immediately get edited again.
  303. // e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line
  304. // Therefore, the strategy is to collapse all the operations into a huge single edit operation
  305. return [this._toSingleEditOperation(operations)];
  306. }
  307. _toSingleEditOperation(operations) {
  308. let forceMoveMarkers = false;
  309. const firstEditRange = operations[0].range;
  310. const lastEditRange = operations[operations.length - 1].range;
  311. const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn);
  312. let lastEndLineNumber = firstEditRange.startLineNumber;
  313. let lastEndColumn = firstEditRange.startColumn;
  314. const result = [];
  315. for (let i = 0, len = operations.length; i < len; i++) {
  316. const operation = operations[i];
  317. const range = operation.range;
  318. forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;
  319. // (1) -- Push old text
  320. result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
  321. // (2) -- Push new text
  322. if (operation.text.length > 0) {
  323. result.push(operation.text);
  324. }
  325. lastEndLineNumber = range.endLineNumber;
  326. lastEndColumn = range.endColumn;
  327. }
  328. const text = result.join('');
  329. const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
  330. return {
  331. sortIndex: 0,
  332. identifier: operations[0].identifier,
  333. range: entireEditRange,
  334. rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),
  335. rangeLength: this.getValueLengthInRange(entireEditRange, 0 /* EndOfLinePreference.TextDefined */),
  336. text: text,
  337. eolCount: eolCount,
  338. firstLineLength: firstLineLength,
  339. lastLineLength: lastLineLength,
  340. forceMoveMarkers: forceMoveMarkers,
  341. isAutoWhitespaceEdit: false
  342. };
  343. }
  344. _doApplyEdits(operations) {
  345. operations.sort(PieceTreeTextBuffer._sortOpsDescending);
  346. const contentChanges = [];
  347. // operations are from bottom to top
  348. for (let i = 0; i < operations.length; i++) {
  349. const op = operations[i];
  350. const startLineNumber = op.range.startLineNumber;
  351. const startColumn = op.range.startColumn;
  352. const endLineNumber = op.range.endLineNumber;
  353. const endColumn = op.range.endColumn;
  354. if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {
  355. // no-op
  356. continue;
  357. }
  358. if (op.text) {
  359. // replacement
  360. this._pieceTree.delete(op.rangeOffset, op.rangeLength);
  361. this._pieceTree.insert(op.rangeOffset, op.text, true);
  362. }
  363. else {
  364. // deletion
  365. this._pieceTree.delete(op.rangeOffset, op.rangeLength);
  366. }
  367. const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
  368. contentChanges.push({
  369. range: contentChangeRange,
  370. rangeLength: op.rangeLength,
  371. text: op.text,
  372. rangeOffset: op.rangeOffset,
  373. forceMoveMarkers: op.forceMoveMarkers
  374. });
  375. }
  376. return contentChanges;
  377. }
  378. findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount) {
  379. return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
  380. }
  381. /**
  382. * Assumes `operations` are validated and sorted ascending
  383. */
  384. static _getInverseEditRanges(operations) {
  385. const result = [];
  386. let prevOpEndLineNumber = 0;
  387. let prevOpEndColumn = 0;
  388. let prevOp = null;
  389. for (let i = 0, len = operations.length; i < len; i++) {
  390. const op = operations[i];
  391. let startLineNumber;
  392. let startColumn;
  393. if (prevOp) {
  394. if (prevOp.range.endLineNumber === op.range.startLineNumber) {
  395. startLineNumber = prevOpEndLineNumber;
  396. startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn);
  397. }
  398. else {
  399. startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber);
  400. startColumn = op.range.startColumn;
  401. }
  402. }
  403. else {
  404. startLineNumber = op.range.startLineNumber;
  405. startColumn = op.range.startColumn;
  406. }
  407. let resultRange;
  408. if (op.text.length > 0) {
  409. // the operation inserts something
  410. const lineCount = op.eolCount + 1;
  411. if (lineCount === 1) {
  412. // single line insert
  413. resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);
  414. }
  415. else {
  416. // multi line insert
  417. resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 1);
  418. }
  419. }
  420. else {
  421. // There is nothing to insert
  422. resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);
  423. }
  424. prevOpEndLineNumber = resultRange.endLineNumber;
  425. prevOpEndColumn = resultRange.endColumn;
  426. result.push(resultRange);
  427. prevOp = op;
  428. }
  429. return result;
  430. }
  431. static _sortOpsAscending(a, b) {
  432. const r = Range.compareRangesUsingEnds(a.range, b.range);
  433. if (r === 0) {
  434. return a.sortIndex - b.sortIndex;
  435. }
  436. return r;
  437. }
  438. static _sortOpsDescending(a, b) {
  439. const r = Range.compareRangesUsingEnds(a.range, b.range);
  440. if (r === 0) {
  441. return b.sortIndex - a.sortIndex;
  442. }
  443. return -r;
  444. }
  445. }