modelLineProjectionData.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 { Position } from '../core/position.js';
  6. /**
  7. * *input*:
  8. * ```
  9. * xxxxxxxxxxxxxxxxxxxxxxxxxxx
  10. * ```
  11. *
  12. * -> Applying injections `[i...i]`, *inputWithInjections*:
  13. * ```
  14. * xxxxxx[iiiiiiiiii]xxxxxxxxxxxxxxxxx[ii]xxxx
  15. * ```
  16. *
  17. * -> breaking at offsets `|` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xxxxxx[ii]xxxx|`:
  18. * ```
  19. * xxxxxx[iiiiiii
  20. * iii]xxxxxxxxxxx
  21. * xxxxxx[ii]xxxx
  22. * ```
  23. *
  24. * -> applying wrappedTextIndentLength, *output*:
  25. * ```
  26. * xxxxxx[iiiiiii
  27. * iii]xxxxxxxxxxx
  28. * xxxxxx[ii]xxxx
  29. * ```
  30. */
  31. export class ModelLineProjectionData {
  32. constructor(injectionOffsets,
  33. /**
  34. * `injectionOptions.length` must equal `injectionOffsets.length`
  35. */
  36. injectionOptions,
  37. /**
  38. * Refers to offsets after applying injections to the source.
  39. * The last break offset indicates the length of the source after applying injections.
  40. */
  41. breakOffsets,
  42. /**
  43. * Refers to offsets after applying injections
  44. */
  45. breakOffsetsVisibleColumn, wrappedTextIndentLength) {
  46. this.injectionOffsets = injectionOffsets;
  47. this.injectionOptions = injectionOptions;
  48. this.breakOffsets = breakOffsets;
  49. this.breakOffsetsVisibleColumn = breakOffsetsVisibleColumn;
  50. this.wrappedTextIndentLength = wrappedTextIndentLength;
  51. }
  52. getOutputLineCount() {
  53. return this.breakOffsets.length;
  54. }
  55. getMinOutputOffset(outputLineIndex) {
  56. if (outputLineIndex > 0) {
  57. return this.wrappedTextIndentLength;
  58. }
  59. return 0;
  60. }
  61. getLineLength(outputLineIndex) {
  62. // These offsets refer to model text with injected text.
  63. const startOffset = outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0;
  64. const endOffset = this.breakOffsets[outputLineIndex];
  65. let lineLength = endOffset - startOffset;
  66. if (outputLineIndex > 0) {
  67. lineLength += this.wrappedTextIndentLength;
  68. }
  69. return lineLength;
  70. }
  71. getMaxOutputOffset(outputLineIndex) {
  72. return this.getLineLength(outputLineIndex);
  73. }
  74. translateToInputOffset(outputLineIndex, outputOffset) {
  75. if (outputLineIndex > 0) {
  76. outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);
  77. }
  78. const offsetInInputWithInjection = outputLineIndex === 0 ? outputOffset : this.breakOffsets[outputLineIndex - 1] + outputOffset;
  79. let offsetInInput = offsetInInputWithInjection;
  80. if (this.injectionOffsets !== null) {
  81. for (let i = 0; i < this.injectionOffsets.length; i++) {
  82. if (offsetInInput > this.injectionOffsets[i]) {
  83. if (offsetInInput < this.injectionOffsets[i] + this.injectionOptions[i].content.length) {
  84. // `inputOffset` is within injected text
  85. offsetInInput = this.injectionOffsets[i];
  86. }
  87. else {
  88. offsetInInput -= this.injectionOptions[i].content.length;
  89. }
  90. }
  91. else {
  92. break;
  93. }
  94. }
  95. }
  96. return offsetInInput;
  97. }
  98. translateToOutputPosition(inputOffset, affinity = 2 /* None */) {
  99. let inputOffsetInInputWithInjection = inputOffset;
  100. if (this.injectionOffsets !== null) {
  101. for (let i = 0; i < this.injectionOffsets.length; i++) {
  102. if (inputOffset < this.injectionOffsets[i]) {
  103. break;
  104. }
  105. if (affinity !== 1 /* Right */ && inputOffset === this.injectionOffsets[i]) {
  106. break;
  107. }
  108. inputOffsetInInputWithInjection += this.injectionOptions[i].content.length;
  109. }
  110. }
  111. return this.offsetInInputWithInjectionsToOutputPosition(inputOffsetInInputWithInjection, affinity);
  112. }
  113. offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections, affinity = 2 /* None */) {
  114. let low = 0;
  115. let high = this.breakOffsets.length - 1;
  116. let mid = 0;
  117. let midStart = 0;
  118. while (low <= high) {
  119. mid = low + ((high - low) / 2) | 0;
  120. const midStop = this.breakOffsets[mid];
  121. midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0;
  122. if (affinity === 0 /* Left */) {
  123. if (offsetInInputWithInjections <= midStart) {
  124. high = mid - 1;
  125. }
  126. else if (offsetInInputWithInjections > midStop) {
  127. low = mid + 1;
  128. }
  129. else {
  130. break;
  131. }
  132. }
  133. else {
  134. if (offsetInInputWithInjections < midStart) {
  135. high = mid - 1;
  136. }
  137. else if (offsetInInputWithInjections >= midStop) {
  138. low = mid + 1;
  139. }
  140. else {
  141. break;
  142. }
  143. }
  144. }
  145. let outputOffset = offsetInInputWithInjections - midStart;
  146. if (mid > 0) {
  147. outputOffset += this.wrappedTextIndentLength;
  148. }
  149. return new OutputPosition(mid, outputOffset);
  150. }
  151. normalizeOutputPosition(outputLineIndex, outputOffset, affinity) {
  152. if (this.injectionOffsets !== null) {
  153. const offsetInInputWithInjections = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);
  154. const normalizedOffsetInUnwrappedLine = this.normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity);
  155. if (normalizedOffsetInUnwrappedLine !== offsetInInputWithInjections) {
  156. // injected text caused a change
  157. return this.offsetInInputWithInjectionsToOutputPosition(normalizedOffsetInUnwrappedLine, affinity);
  158. }
  159. }
  160. if (affinity === 0 /* Left */) {
  161. if (outputLineIndex > 0 && outputOffset === this.getMinOutputOffset(outputLineIndex)) {
  162. return new OutputPosition(outputLineIndex - 1, this.getMaxOutputOffset(outputLineIndex - 1));
  163. }
  164. }
  165. else if (affinity === 1 /* Right */) {
  166. const maxOutputLineIndex = this.getOutputLineCount() - 1;
  167. if (outputLineIndex < maxOutputLineIndex && outputOffset === this.getMaxOutputOffset(outputLineIndex)) {
  168. return new OutputPosition(outputLineIndex + 1, this.getMinOutputOffset(outputLineIndex + 1));
  169. }
  170. }
  171. return new OutputPosition(outputLineIndex, outputOffset);
  172. }
  173. outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset) {
  174. if (outputLineIndex > 0) {
  175. outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);
  176. }
  177. const result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset;
  178. return result;
  179. }
  180. normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity) {
  181. const injectedText = this.getInjectedTextAtOffset(offsetInInputWithInjections);
  182. if (!injectedText) {
  183. return offsetInInputWithInjections;
  184. }
  185. if (affinity === 2 /* None */) {
  186. if (offsetInInputWithInjections === injectedText.offsetInInputWithInjections + injectedText.length) {
  187. // go to the end of this injected text
  188. return injectedText.offsetInInputWithInjections + injectedText.length;
  189. }
  190. else {
  191. // go to the start of this injected text
  192. return injectedText.offsetInInputWithInjections;
  193. }
  194. }
  195. if (affinity === 1 /* Right */) {
  196. let result = injectedText.offsetInInputWithInjections + injectedText.length;
  197. let index = injectedText.injectedTextIndex;
  198. // traverse all injected text that touch each other
  199. while (index + 1 < this.injectionOffsets.length && this.injectionOffsets[index + 1] === this.injectionOffsets[index]) {
  200. result += this.injectionOptions[index + 1].content.length;
  201. index++;
  202. }
  203. return result;
  204. }
  205. // affinity is left
  206. let result = injectedText.offsetInInputWithInjections;
  207. let index = injectedText.injectedTextIndex;
  208. // traverse all injected text that touch each other
  209. while (index - 1 >= 0 && this.injectionOffsets[index - 1] === this.injectionOffsets[index]) {
  210. result -= this.injectionOptions[index - 1].content.length;
  211. index++;
  212. }
  213. return result;
  214. }
  215. getInjectedText(outputLineIndex, outputOffset) {
  216. const offset = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);
  217. const injectedText = this.getInjectedTextAtOffset(offset);
  218. if (!injectedText) {
  219. return null;
  220. }
  221. return {
  222. options: this.injectionOptions[injectedText.injectedTextIndex]
  223. };
  224. }
  225. getInjectedTextAtOffset(offsetInInputWithInjections) {
  226. const injectionOffsets = this.injectionOffsets;
  227. const injectionOptions = this.injectionOptions;
  228. if (injectionOffsets !== null) {
  229. let totalInjectedTextLengthBefore = 0;
  230. for (let i = 0; i < injectionOffsets.length; i++) {
  231. const length = injectionOptions[i].content.length;
  232. const injectedTextStartOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore;
  233. const injectedTextEndOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore + length;
  234. if (injectedTextStartOffsetInInputWithInjections > offsetInInputWithInjections) {
  235. // Injected text starts later.
  236. break; // All later injected texts have an even larger offset.
  237. }
  238. if (offsetInInputWithInjections <= injectedTextEndOffsetInInputWithInjections) {
  239. // Injected text ends after or with the given position (but also starts with or before it).
  240. return {
  241. injectedTextIndex: i,
  242. offsetInInputWithInjections: injectedTextStartOffsetInInputWithInjections,
  243. length
  244. };
  245. }
  246. totalInjectedTextLengthBefore += length;
  247. }
  248. }
  249. return undefined;
  250. }
  251. }
  252. export class InjectedText {
  253. constructor(options) {
  254. this.options = options;
  255. }
  256. }
  257. export class OutputPosition {
  258. constructor(outputLineIndex, outputOffset) {
  259. this.outputLineIndex = outputLineIndex;
  260. this.outputOffset = outputOffset;
  261. }
  262. toString() {
  263. return `${this.outputLineIndex}:${this.outputOffset}`;
  264. }
  265. toPosition(baseLineNumber) {
  266. return new Position(baseLineNumber + this.outputLineIndex, this.outputOffset + 1);
  267. }
  268. }