domLineBreaksComputer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. var _a;
  6. import { createStringBuilder } from '../../common/core/stringBuilder.js';
  7. import * as strings from '../../../base/common/strings.js';
  8. import { Configuration } from '../config/configuration.js';
  9. import { LineInjectedText } from '../../common/model/textModelEvents.js';
  10. import { ModelLineProjectionData } from '../../common/viewModel/modelLineProjectionData.js';
  11. const ttPolicy = (_a = window.trustedTypes) === null || _a === void 0 ? void 0 : _a.createPolicy('domLineBreaksComputer', { createHTML: value => value });
  12. export class DOMLineBreaksComputerFactory {
  13. static create() {
  14. return new DOMLineBreaksComputerFactory();
  15. }
  16. constructor() {
  17. }
  18. createLineBreaksComputer(fontInfo, tabSize, wrappingColumn, wrappingIndent) {
  19. let requests = [];
  20. let injectedTexts = [];
  21. return {
  22. addRequest: (lineText, injectedText, previousLineBreakData) => {
  23. requests.push(lineText);
  24. injectedTexts.push(injectedText);
  25. },
  26. finalize: () => {
  27. return createLineBreaks(requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, injectedTexts);
  28. }
  29. };
  30. }
  31. }
  32. function createLineBreaks(requests, fontInfo, tabSize, firstLineBreakColumn, wrappingIndent, injectedTextsPerLine) {
  33. var _a;
  34. function createEmptyLineBreakWithPossiblyInjectedText(requestIdx) {
  35. const injectedTexts = injectedTextsPerLine[requestIdx];
  36. if (injectedTexts) {
  37. const lineText = LineInjectedText.applyInjectedText(requests[requestIdx], injectedTexts);
  38. const injectionOptions = injectedTexts.map(t => t.options);
  39. const injectionOffsets = injectedTexts.map(text => text.column - 1);
  40. // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK
  41. // because `breakOffsetsVisibleColumn` will never be used because it contains injected text
  42. return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);
  43. }
  44. else {
  45. return null;
  46. }
  47. }
  48. if (firstLineBreakColumn === -1) {
  49. const result = [];
  50. for (let i = 0, len = requests.length; i < len; i++) {
  51. result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
  52. }
  53. return result;
  54. }
  55. const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth);
  56. const additionalIndent = (wrappingIndent === 3 /* DeepIndent */ ? 2 : wrappingIndent === 2 /* Indent */ ? 1 : 0);
  57. const additionalIndentSize = Math.round(tabSize * additionalIndent);
  58. const additionalIndentLength = Math.ceil(fontInfo.spaceWidth * additionalIndentSize);
  59. const containerDomNode = document.createElement('div');
  60. Configuration.applyFontInfoSlow(containerDomNode, fontInfo);
  61. const sb = createStringBuilder(10000);
  62. const firstNonWhitespaceIndices = [];
  63. const wrappedTextIndentLengths = [];
  64. const renderLineContents = [];
  65. const allCharOffsets = [];
  66. const allVisibleColumns = [];
  67. for (let i = 0; i < requests.length; i++) {
  68. const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]);
  69. let firstNonWhitespaceIndex = 0;
  70. let wrappedTextIndentLength = 0;
  71. let width = overallWidth;
  72. if (wrappingIndent !== 0 /* None */) {
  73. firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
  74. if (firstNonWhitespaceIndex === -1) {
  75. // all whitespace line
  76. firstNonWhitespaceIndex = 0;
  77. }
  78. else {
  79. // Track existing indent
  80. for (let i = 0; i < firstNonWhitespaceIndex; i++) {
  81. const charWidth = (lineContent.charCodeAt(i) === 9 /* Tab */
  82. ? (tabSize - (wrappedTextIndentLength % tabSize))
  83. : 1);
  84. wrappedTextIndentLength += charWidth;
  85. }
  86. const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength);
  87. // Force sticking to beginning of line if no character would fit except for the indentation
  88. if (indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth) {
  89. firstNonWhitespaceIndex = 0;
  90. wrappedTextIndentLength = 0;
  91. }
  92. else {
  93. width = overallWidth - indentWidth;
  94. }
  95. }
  96. }
  97. const renderLineContent = lineContent.substr(firstNonWhitespaceIndex);
  98. const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength);
  99. firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex;
  100. wrappedTextIndentLengths[i] = wrappedTextIndentLength;
  101. renderLineContents[i] = renderLineContent;
  102. allCharOffsets[i] = tmp[0];
  103. allVisibleColumns[i] = tmp[1];
  104. }
  105. const html = sb.build();
  106. const trustedhtml = (_a = ttPolicy === null || ttPolicy === void 0 ? void 0 : ttPolicy.createHTML(html)) !== null && _a !== void 0 ? _a : html;
  107. containerDomNode.innerHTML = trustedhtml;
  108. containerDomNode.style.position = 'absolute';
  109. containerDomNode.style.top = '10000';
  110. containerDomNode.style.wordWrap = 'break-word';
  111. document.body.appendChild(containerDomNode);
  112. let range = document.createRange();
  113. const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0);
  114. let result = [];
  115. for (let i = 0; i < requests.length; i++) {
  116. const lineDomNode = lineDomNodes[i];
  117. const breakOffsets = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]);
  118. if (breakOffsets === null) {
  119. result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
  120. continue;
  121. }
  122. const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i];
  123. const wrappedTextIndentLength = wrappedTextIndentLengths[i] + additionalIndentSize;
  124. const visibleColumns = allVisibleColumns[i];
  125. const breakOffsetsVisibleColumn = [];
  126. for (let j = 0, len = breakOffsets.length; j < len; j++) {
  127. breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]];
  128. }
  129. if (firstNonWhitespaceIndex !== 0) {
  130. // All break offsets are relative to the renderLineContent, make them absolute again
  131. for (let j = 0, len = breakOffsets.length; j < len; j++) {
  132. breakOffsets[j] += firstNonWhitespaceIndex;
  133. }
  134. }
  135. let injectionOptions;
  136. let injectionOffsets;
  137. const curInjectedTexts = injectedTextsPerLine[i];
  138. if (curInjectedTexts) {
  139. injectionOptions = curInjectedTexts.map(t => t.options);
  140. injectionOffsets = curInjectedTexts.map(text => text.column - 1);
  141. }
  142. else {
  143. injectionOptions = null;
  144. injectionOffsets = null;
  145. }
  146. result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength);
  147. }
  148. document.body.removeChild(containerDomNode);
  149. return result;
  150. }
  151. function renderLine(lineContent, initialVisibleColumn, tabSize, width, sb, wrappingIndentLength) {
  152. if (wrappingIndentLength !== 0) {
  153. let hangingOffset = String(wrappingIndentLength);
  154. sb.appendASCIIString('<div style="text-indent: -');
  155. sb.appendASCIIString(hangingOffset);
  156. sb.appendASCIIString('px; padding-left: ');
  157. sb.appendASCIIString(hangingOffset);
  158. sb.appendASCIIString('px; box-sizing: border-box; width:');
  159. }
  160. else {
  161. sb.appendASCIIString('<div style="width:');
  162. }
  163. sb.appendASCIIString(String(width));
  164. sb.appendASCIIString('px;">');
  165. // if (containsRTL) {
  166. // sb.appendASCIIString('" dir="ltr');
  167. // }
  168. const len = lineContent.length;
  169. let visibleColumn = initialVisibleColumn;
  170. let charOffset = 0;
  171. let charOffsets = [];
  172. let visibleColumns = [];
  173. let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : 0 /* Null */);
  174. sb.appendASCIIString('<span>');
  175. for (let charIndex = 0; charIndex < len; charIndex++) {
  176. if (charIndex !== 0 && charIndex % 16384 /* SPAN_MODULO_LIMIT */ === 0) {
  177. sb.appendASCIIString('</span><span>');
  178. }
  179. charOffsets[charIndex] = charOffset;
  180. visibleColumns[charIndex] = visibleColumn;
  181. const charCode = nextCharCode;
  182. nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : 0 /* Null */);
  183. let producedCharacters = 1;
  184. let charWidth = 1;
  185. switch (charCode) {
  186. case 9 /* Tab */:
  187. producedCharacters = (tabSize - (visibleColumn % tabSize));
  188. charWidth = producedCharacters;
  189. for (let space = 1; space <= producedCharacters; space++) {
  190. if (space < producedCharacters) {
  191. sb.write1(0xA0); // &nbsp;
  192. }
  193. else {
  194. sb.appendASCII(32 /* Space */);
  195. }
  196. }
  197. break;
  198. case 32 /* Space */:
  199. if (nextCharCode === 32 /* Space */) {
  200. sb.write1(0xA0); // &nbsp;
  201. }
  202. else {
  203. sb.appendASCII(32 /* Space */);
  204. }
  205. break;
  206. case 60 /* LessThan */:
  207. sb.appendASCIIString('&lt;');
  208. break;
  209. case 62 /* GreaterThan */:
  210. sb.appendASCIIString('&gt;');
  211. break;
  212. case 38 /* Ampersand */:
  213. sb.appendASCIIString('&amp;');
  214. break;
  215. case 0 /* Null */:
  216. sb.appendASCIIString('&#00;');
  217. break;
  218. case 65279 /* UTF8_BOM */:
  219. case 8232 /* LINE_SEPARATOR */:
  220. case 8233 /* PARAGRAPH_SEPARATOR */:
  221. case 133 /* NEXT_LINE */:
  222. sb.write1(0xFFFD);
  223. break;
  224. default:
  225. if (strings.isFullWidthCharacter(charCode)) {
  226. charWidth++;
  227. }
  228. if (charCode < 32) {
  229. sb.write1(9216 + charCode);
  230. }
  231. else {
  232. sb.write1(charCode);
  233. }
  234. }
  235. charOffset += producedCharacters;
  236. visibleColumn += charWidth;
  237. }
  238. sb.appendASCIIString('</span>');
  239. charOffsets[lineContent.length] = charOffset;
  240. visibleColumns[lineContent.length] = visibleColumn;
  241. sb.appendASCIIString('</div>');
  242. return [charOffsets, visibleColumns];
  243. }
  244. function readLineBreaks(range, lineDomNode, lineContent, charOffsets) {
  245. if (lineContent.length <= 1) {
  246. return null;
  247. }
  248. const spans = Array.prototype.slice.call(lineDomNode.children, 0);
  249. const breakOffsets = [];
  250. try {
  251. discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets);
  252. }
  253. catch (err) {
  254. console.log(err);
  255. return null;
  256. }
  257. if (breakOffsets.length === 0) {
  258. return null;
  259. }
  260. breakOffsets.push(lineContent.length);
  261. return breakOffsets;
  262. }
  263. function discoverBreaks(range, spans, charOffsets, low, lowRects, high, highRects, result) {
  264. if (low === high) {
  265. return;
  266. }
  267. lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]);
  268. highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]);
  269. if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) {
  270. // same line
  271. return;
  272. }
  273. // there is at least one line break between these two offsets
  274. if (low + 1 === high) {
  275. // the two characters are adjacent, so the line break must be exactly between them
  276. result.push(high);
  277. return;
  278. }
  279. const mid = low + ((high - low) / 2) | 0;
  280. const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]);
  281. discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result);
  282. discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result);
  283. }
  284. function readClientRect(range, spans, startOffset, endOffset) {
  285. range.setStart(spans[(startOffset / 16384 /* SPAN_MODULO_LIMIT */) | 0].firstChild, startOffset % 16384 /* SPAN_MODULO_LIMIT */);
  286. range.setEnd(spans[(endOffset / 16384 /* SPAN_MODULO_LIMIT */) | 0].firstChild, endOffset % 16384 /* SPAN_MODULO_LIMIT */);
  287. return range.getClientRects();
  288. }