modelLineProjectionData.js 13 KB

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