@pierre/diffs 1.3.0-beta.9 → 1.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +9 -0
  2. package/dist/components/CodeView.d.ts +12 -13
  3. package/dist/components/CodeView.d.ts.map +1 -1
  4. package/dist/components/CodeView.js +50 -58
  5. package/dist/components/CodeView.js.map +1 -1
  6. package/dist/components/File.d.ts +10 -3
  7. package/dist/components/File.d.ts.map +1 -1
  8. package/dist/components/File.js +41 -10
  9. package/dist/components/File.js.map +1 -1
  10. package/dist/components/FileDiff.d.ts +37 -6
  11. package/dist/components/FileDiff.d.ts.map +1 -1
  12. package/dist/components/FileDiff.js +202 -19
  13. package/dist/components/FileDiff.js.map +1 -1
  14. package/dist/components/UnresolvedFile.d.ts.map +1 -1
  15. package/dist/components/UnresolvedFile.js +7 -2
  16. package/dist/components/UnresolvedFile.js.map +1 -1
  17. package/dist/components/VirtualizedFile.d.ts +2 -1
  18. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  19. package/dist/components/VirtualizedFile.js +9 -5
  20. package/dist/components/VirtualizedFile.js.map +1 -1
  21. package/dist/components/VirtualizedFileDiff.d.ts +14 -0
  22. package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
  23. package/dist/components/VirtualizedFileDiff.js +61 -3
  24. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  25. package/dist/components/Virtualizer.d.ts +14 -2
  26. package/dist/components/Virtualizer.d.ts.map +1 -1
  27. package/dist/components/Virtualizer.js +27 -2
  28. package/dist/components/Virtualizer.js.map +1 -1
  29. package/dist/edit/index.d.ts +5 -0
  30. package/dist/edit/index.js +3 -0
  31. package/dist/editor/command.d.ts +1 -1
  32. package/dist/editor/command.d.ts.map +1 -1
  33. package/dist/editor/command.js +11 -0
  34. package/dist/editor/command.js.map +1 -1
  35. package/dist/editor/editStack.d.ts +6 -2
  36. package/dist/editor/editStack.d.ts.map +1 -1
  37. package/dist/editor/editStack.js +53 -12
  38. package/dist/editor/editStack.js.map +1 -1
  39. package/dist/editor/editor.d.ts +51 -23
  40. package/dist/editor/editor.d.ts.map +1 -1
  41. package/dist/editor/editor.js +977 -241
  42. package/dist/editor/editor.js.map +1 -1
  43. package/dist/editor/editor2.js +1 -1
  44. package/dist/editor/languages.d.ts +26 -0
  45. package/dist/editor/languages.d.ts.map +1 -0
  46. package/dist/editor/languages.js +363 -0
  47. package/dist/editor/languages.js.map +1 -0
  48. package/dist/editor/lineAnnotations.d.ts.map +1 -1
  49. package/dist/editor/lineAnnotations.js +25 -26
  50. package/dist/editor/lineAnnotations.js.map +1 -1
  51. package/dist/editor/marker.d.ts +5 -3
  52. package/dist/editor/marker.d.ts.map +1 -1
  53. package/dist/editor/marker.js +111 -91
  54. package/dist/editor/marker.js.map +1 -1
  55. package/dist/editor/matchBrackets.d.ts +3 -2
  56. package/dist/editor/matchBrackets.d.ts.map +1 -1
  57. package/dist/editor/matchBrackets.js +2 -10
  58. package/dist/editor/matchBrackets.js.map +1 -1
  59. package/dist/editor/pieceTable.d.ts +2 -1
  60. package/dist/editor/pieceTable.d.ts.map +1 -1
  61. package/dist/editor/pieceTable.js +103 -39
  62. package/dist/editor/pieceTable.js.map +1 -1
  63. package/dist/editor/platform.js +2 -0
  64. package/dist/editor/platform.js.map +1 -1
  65. package/dist/editor/popover.d.ts +25 -5
  66. package/dist/editor/popover.d.ts.map +1 -1
  67. package/dist/editor/popover.js +60 -14
  68. package/dist/editor/popover.js.map +1 -1
  69. package/dist/editor/searchPanel.d.ts.map +1 -1
  70. package/dist/editor/searchPanel.js +9 -2
  71. package/dist/editor/searchPanel.js.map +1 -1
  72. package/dist/editor/selection.d.ts +31 -11
  73. package/dist/editor/selection.d.ts.map +1 -1
  74. package/dist/editor/selection.js +198 -70
  75. package/dist/editor/selection.js.map +1 -1
  76. package/dist/editor/selectionAction.d.ts +4 -3
  77. package/dist/editor/selectionAction.d.ts.map +1 -1
  78. package/dist/editor/selectionAction.js +9 -5
  79. package/dist/editor/selectionAction.js.map +1 -1
  80. package/dist/editor/stateStorage.d.ts +15 -0
  81. package/dist/editor/stateStorage.d.ts.map +1 -0
  82. package/dist/editor/stateStorage.js +65 -0
  83. package/dist/editor/stateStorage.js.map +1 -0
  84. package/dist/editor/textDocument.d.ts +12 -77
  85. package/dist/editor/textDocument.d.ts.map +1 -1
  86. package/dist/editor/textDocument.js +76 -34
  87. package/dist/editor/textDocument.js.map +1 -1
  88. package/dist/editor/textMeasure.d.ts +7 -12
  89. package/dist/editor/textMeasure.d.ts.map +1 -1
  90. package/dist/editor/textMeasure.js +40 -32
  91. package/dist/editor/textMeasure.js.map +1 -1
  92. package/dist/editor/{tokenzier.d.ts → tokenizer.d.ts} +3 -3
  93. package/dist/editor/tokenizer.d.ts.map +1 -0
  94. package/dist/editor/{tokenzier.js → tokenizer.js} +170 -50
  95. package/dist/editor/tokenizer.js.map +1 -0
  96. package/dist/editor/utils.js +1 -1
  97. package/dist/editor/utils.js.map +1 -1
  98. package/dist/index.d.ts +4 -3
  99. package/dist/index.js +6 -5
  100. package/dist/managers/InteractionManager.d.ts +18 -7
  101. package/dist/managers/InteractionManager.d.ts.map +1 -1
  102. package/dist/managers/InteractionManager.js +154 -99
  103. package/dist/managers/InteractionManager.js.map +1 -1
  104. package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
  105. package/dist/managers/UniversalRenderingManager.js +1 -2
  106. package/dist/managers/UniversalRenderingManager.js.map +1 -1
  107. package/dist/react/CodeView.d.ts +13 -13
  108. package/dist/react/CodeView.d.ts.map +1 -1
  109. package/dist/react/CodeView.js +37 -28
  110. package/dist/react/CodeView.js.map +1 -1
  111. package/dist/react/EditContext.d.ts +20 -0
  112. package/dist/react/EditContext.d.ts.map +1 -0
  113. package/dist/react/EditContext.js +20 -0
  114. package/dist/react/EditContext.js.map +1 -0
  115. package/dist/react/File.d.ts +2 -1
  116. package/dist/react/File.d.ts.map +1 -1
  117. package/dist/react/File.js +3 -2
  118. package/dist/react/File.js.map +1 -1
  119. package/dist/react/FileDiff.d.ts +2 -2
  120. package/dist/react/FileDiff.d.ts.map +1 -1
  121. package/dist/react/FileDiff.js +3 -2
  122. package/dist/react/FileDiff.js.map +1 -1
  123. package/dist/react/MultiFileDiff.d.ts +2 -2
  124. package/dist/react/MultiFileDiff.d.ts.map +1 -1
  125. package/dist/react/MultiFileDiff.js +3 -2
  126. package/dist/react/MultiFileDiff.js.map +1 -1
  127. package/dist/react/PatchDiff.d.ts +2 -2
  128. package/dist/react/PatchDiff.d.ts.map +1 -1
  129. package/dist/react/PatchDiff.js +3 -2
  130. package/dist/react/PatchDiff.js.map +1 -1
  131. package/dist/react/UnresolvedFile.d.ts +1 -1
  132. package/dist/react/UnresolvedFile.js.map +1 -1
  133. package/dist/react/index.d.ts +4 -4
  134. package/dist/react/index.js +3 -3
  135. package/dist/react/types.d.ts +9 -1
  136. package/dist/react/types.d.ts.map +1 -1
  137. package/dist/react/utils/useFileDiffInstance.d.ts +5 -3
  138. package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
  139. package/dist/react/utils/useFileDiffInstance.js +24 -34
  140. package/dist/react/utils/useFileDiffInstance.js.map +1 -1
  141. package/dist/react/utils/useFileInstance.d.ts +5 -3
  142. package/dist/react/utils/useFileInstance.d.ts.map +1 -1
  143. package/dist/react/utils/useFileInstance.js +23 -33
  144. package/dist/react/utils/useFileInstance.js.map +1 -1
  145. package/dist/react/utils/useUnresolvedFileInstance.js +4 -4
  146. package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
  147. package/dist/renderers/DiffHunksRenderer.d.ts +41 -1
  148. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  149. package/dist/renderers/DiffHunksRenderer.js +167 -29
  150. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  151. package/dist/renderers/FileRenderer.d.ts +20 -1
  152. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  153. package/dist/renderers/FileRenderer.js +119 -29
  154. package/dist/renderers/FileRenderer.js.map +1 -1
  155. package/dist/ssr/index.d.ts +2 -2
  156. package/dist/ssr/preloadDiffs.d.ts.map +1 -1
  157. package/dist/ssr/preloadDiffs.js +3 -1
  158. package/dist/ssr/preloadDiffs.js.map +1 -1
  159. package/dist/ssr/preloadFile.d.ts.map +1 -1
  160. package/dist/ssr/preloadFile.js +2 -0
  161. package/dist/ssr/preloadFile.js.map +1 -1
  162. package/dist/style.js +1 -1
  163. package/dist/types.d.ts +192 -42
  164. package/dist/types.d.ts.map +1 -1
  165. package/dist/utils/annotationHelpers.d.ts +20 -0
  166. package/dist/utils/annotationHelpers.d.ts.map +1 -0
  167. package/dist/utils/annotationHelpers.js +29 -0
  168. package/dist/utils/annotationHelpers.js.map +1 -0
  169. package/dist/utils/computeFileOffsets.d.ts.map +1 -1
  170. package/dist/utils/computeFileOffsets.js +34 -5
  171. package/dist/utils/computeFileOffsets.js.map +1 -1
  172. package/dist/utils/editSessionHunks.d.ts +66 -0
  173. package/dist/utils/editSessionHunks.d.ts.map +1 -0
  174. package/dist/utils/editSessionHunks.js +437 -0
  175. package/dist/utils/editSessionHunks.js.map +1 -0
  176. package/dist/utils/getDiffHunksRendererOptions.d.ts.map +1 -1
  177. package/dist/utils/getDiffHunksRendererOptions.js +2 -1
  178. package/dist/utils/getDiffHunksRendererOptions.js.map +1 -1
  179. package/dist/utils/getFileRendererOptions.d.ts.map +1 -1
  180. package/dist/utils/getFileRendererOptions.js +2 -1
  181. package/dist/utils/getFileRendererOptions.js.map +1 -1
  182. package/dist/utils/getHunkSideBoundaries.d.ts +7 -0
  183. package/dist/utils/getHunkSideBoundaries.d.ts.map +1 -0
  184. package/dist/utils/getHunkSideBoundaries.js +12 -0
  185. package/dist/utils/getHunkSideBoundaries.js.map +1 -0
  186. package/dist/utils/getTotalLineCountFromHunks.d.ts.map +1 -1
  187. package/dist/utils/getTotalLineCountFromHunks.js +2 -1
  188. package/dist/utils/getTotalLineCountFromHunks.js.map +1 -1
  189. package/dist/utils/hydratePartialDiff.d.ts.map +1 -1
  190. package/dist/utils/hydratePartialDiff.js +4 -3
  191. package/dist/utils/hydratePartialDiff.js.map +1 -1
  192. package/dist/utils/isGutterUtilityPath.d.ts +5 -0
  193. package/dist/utils/isGutterUtilityPath.d.ts.map +1 -0
  194. package/dist/utils/isGutterUtilityPath.js +12 -0
  195. package/dist/utils/isGutterUtilityPath.js.map +1 -0
  196. package/dist/utils/iterateOverDiff.d.ts.map +1 -1
  197. package/dist/utils/iterateOverDiff.js +17 -12
  198. package/dist/utils/iterateOverDiff.js.map +1 -1
  199. package/dist/utils/parseMergeConflictDiffFromFile.d.ts.map +1 -1
  200. package/dist/utils/parseMergeConflictDiffFromFile.js +9 -6
  201. package/dist/utils/parseMergeConflictDiffFromFile.js.map +1 -1
  202. package/dist/utils/parsePatchFiles.d.ts.map +1 -1
  203. package/dist/utils/parsePatchFiles.js +6 -3
  204. package/dist/utils/parsePatchFiles.js.map +1 -1
  205. package/dist/utils/realignChangeContent.d.ts +33 -0
  206. package/dist/utils/realignChangeContent.d.ts.map +1 -0
  207. package/dist/utils/realignChangeContent.js +148 -0
  208. package/dist/utils/realignChangeContent.js.map +1 -0
  209. package/dist/utils/renderDiffWithHighlighter.js +1 -1
  210. package/dist/utils/resolveRegion.d.ts.map +1 -1
  211. package/dist/utils/resolveRegion.js +18 -2
  212. package/dist/utils/resolveRegion.js.map +1 -1
  213. package/dist/utils/shouldUseTokenTransformer.d.ts +7 -3
  214. package/dist/utils/shouldUseTokenTransformer.d.ts.map +1 -1
  215. package/dist/utils/shouldUseTokenTransformer.js +1 -1
  216. package/dist/utils/shouldUseTokenTransformer.js.map +1 -1
  217. package/dist/utils/updateDiffHunks.d.ts +8 -2
  218. package/dist/utils/updateDiffHunks.d.ts.map +1 -1
  219. package/dist/utils/updateDiffHunks.js +9 -7
  220. package/dist/utils/updateDiffHunks.js.map +1 -1
  221. package/dist/utils/virtualDiffLayout.d.ts +42 -2
  222. package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
  223. package/dist/utils/virtualDiffLayout.js +101 -7
  224. package/dist/utils/virtualDiffLayout.js.map +1 -1
  225. package/dist/worker/WorkerPoolManager.d.ts +2 -1
  226. package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
  227. package/dist/worker/WorkerPoolManager.js +3 -0
  228. package/dist/worker/WorkerPoolManager.js.map +1 -1
  229. package/dist/worker/worker-portable.js +60 -20
  230. package/dist/worker/worker-portable.js.map +1 -1
  231. package/dist/worker/worker.js +61 -19
  232. package/dist/worker/worker.js.map +1 -1
  233. package/package.json +10 -7
  234. package/dist/editor/index.d.ts +0 -3
  235. package/dist/editor/index.js +0 -3
  236. package/dist/editor/tokenzier.d.ts.map +0 -1
  237. package/dist/editor/tokenzier.js.map +0 -1
  238. package/dist/react/EditorContext.d.ts +0 -15
  239. package/dist/react/EditorContext.d.ts.map +0 -1
  240. package/dist/react/EditorContext.js +0 -23
  241. package/dist/react/EditorContext.js.map +0 -1
@@ -95,6 +95,9 @@ var Virtualizer = class Virtualizer {
95
95
  });
96
96
  return this.windowSpecs;
97
97
  }
98
+ getRoot() {
99
+ return this.root;
100
+ }
98
101
  isInstanceVisible(elementTop, elementHeight) {
99
102
  const scrollTop = this.getScrollTop();
100
103
  const height = this.getHeight();
@@ -179,6 +182,9 @@ var Virtualizer = class Virtualizer {
179
182
  this.scrollTop = 0;
180
183
  this.height = 0;
181
184
  this.scrollHeight = 0;
185
+ this.scrollDirty = true;
186
+ this.heightDirty = true;
187
+ this.scrollHeightDirty = true;
182
188
  }
183
189
  getOffsetInScrollContainer(element) {
184
190
  return this.getScrollTop() + getRelativeBoundingTop(element, this.getScrollContainerElement());
@@ -225,7 +231,6 @@ var Virtualizer = class Virtualizer {
225
231
  computeRenderRangeAndEmit = () => {
226
232
  if (Virtualizer.__STOP) return;
227
233
  const wrapperDirty = this.heightDirty || this.scrollHeightDirty;
228
- if (!this.scrollDirty && !this.scrollHeightDirty && !this.heightDirty && this.renderedObservers === this.observers.size && !this.visibleInstancesDirty && this.instancesChanged.size === 0 && this.reconcileQueue.size === 0) return;
229
234
  let instancesHaveChanged = this.instancesChanged.size > 0;
230
235
  if (this.instancesChanged.size === 0) {
231
236
  const windowSpecs = createWindowFromScrollPosition({
@@ -356,6 +361,7 @@ var Virtualizer = class Virtualizer {
356
361
  }
357
362
  if (this.visibleInstancesDirty) queueRender(this.computeRenderRangeAndEmit);
358
363
  };
364
+ /** Return the logical vertical position for the scroll container */
359
365
  getScrollTop() {
360
366
  if (!this.scrollDirty) return this.scrollTop;
361
367
  this.scrollDirty = false;
@@ -364,10 +370,29 @@ var Virtualizer = class Virtualizer {
364
370
  if (this.root instanceof Document) return window.scrollY;
365
371
  return this.root.scrollTop;
366
372
  })();
367
- scrollTop = Math.max(0, Math.min(scrollTop, this.getScrollHeight() - this.getHeight()));
373
+ scrollTop = this.clampScrollTop(scrollTop);
368
374
  this.scrollTop = scrollTop;
369
375
  return scrollTop;
370
376
  }
377
+ /** Scroll to a logical vertical position and queue the normal render pass. */
378
+ scrollTo({ top, behavior = "auto" }) {
379
+ const { root } = this;
380
+ if (root == null) return;
381
+ const scrollTop = this.clampScrollTop(top);
382
+ if (root instanceof Document) window.scrollTo({
383
+ top: scrollTop,
384
+ behavior
385
+ });
386
+ else root.scrollTo({
387
+ top: scrollTop,
388
+ behavior
389
+ });
390
+ this.scrollDirty = true;
391
+ queueRender(this.computeRenderRangeAndEmit);
392
+ }
393
+ clampScrollTop(scrollTop) {
394
+ return Math.max(0, Math.min(scrollTop, this.getScrollHeight() - this.getHeight()));
395
+ }
371
396
  getScrollHeight() {
372
397
  if (!this.scrollHeightDirty) return this.scrollHeight;
373
398
  this.scrollHeightDirty = false;
@@ -1 +1 @@
1
- {"version":3,"file":"Virtualizer.js","names":[],"sources":["../../src/components/Virtualizer.ts"],"sourcesContent":["import {\n dequeueRender,\n queueRender,\n} from '../managers/UniversalRenderingManager';\nimport type { VirtualWindowSpecs } from '../types';\nimport { areVirtualWindowSpecsEqual } from '../utils/areVirtualWindowSpecsEqual';\nimport { createWindowFromScrollPosition } from '../utils/createWindowFromScrollPosition';\n\ninterface SubscribedInstance {\n onRender(dirty: boolean): boolean;\n reconcileHeights(): boolean;\n setVisibility(visible: boolean): void;\n}\n\ninterface ScrollAnchor {\n fileElement: HTMLElement;\n fileTypeOffset: 'top' | 'bottom';\n fileOffset: number;\n lineIndex: string | undefined;\n lineOffset: number | undefined;\n}\n\n// 800 seems like the healthy overscan required to\n// keep safari from blanking... if we catch it tho, maybe 900\nconst DEFAULT_OVERSCROLL_SIZE = 1000;\nconst INTERSECTION_OBSERVER_MARGIN = DEFAULT_OVERSCROLL_SIZE * 4;\nconst INTERSECTION_OBSERVER_THRESHOLD = [0, 0.000001, 0.99999, 1];\n\nexport interface VirtualizerConfig {\n /** Extra pixels rendered above and below the viewport to reduce blanking during fast scrolls. */\n overscrollSize: number;\n /** Margin used by IntersectionObserver to decide when items should be considered visible. */\n intersectionObserverMargin: number;\n /** Enables noisy resize logs to help tune metrics and investigate scroll jitter. */\n resizeDebugging: boolean;\n}\n\nconst DEFAULT_VIRTUALIZER_CONFIG: VirtualizerConfig = {\n overscrollSize: DEFAULT_OVERSCROLL_SIZE,\n intersectionObserverMargin: INTERSECTION_OBSERVER_MARGIN,\n resizeDebugging: false,\n};\n\nlet lastSize = 0;\n\nlet instance = -1;\n\nexport class Virtualizer {\n static __STOP: boolean = false;\n static __lastScrollPosition = 0;\n\n public readonly __id: string = `virtualizer-${++instance}`;\n public readonly config: VirtualizerConfig;\n public type = 'simple' as const;\n private intersectionObserver: IntersectionObserver | undefined;\n private scrollTop: number = 0;\n private height: number = 0;\n private scrollHeight: number = 0;\n private windowSpecs: VirtualWindowSpecs = { top: 0, bottom: 0 };\n private root: HTMLElement | Document | undefined;\n private contentContainer: HTMLElement | undefined;\n\n private resizeObserver: ResizeObserver | undefined;\n private observers: Map<HTMLElement, SubscribedInstance> = new Map();\n private visibleInstances: Map<HTMLElement, SubscribedInstance> = new Map();\n private visibleInstancesDirty: boolean = false;\n private instancesChanged: Set<SubscribedInstance> = new Set();\n // Instances whose content was (re)painted outside a virtualizer-driven\n // onRender — host/React render calls, async highlight completions — and\n // still need a measured-height reconciliation pass. See\n // requestHeightReconcile.\n private reconcileQueue: Set<SubscribedInstance> = new Set();\n\n private scrollDirty = true;\n private heightDirty = true;\n private scrollHeightDirty = true;\n private renderedObservers = 0;\n private connectQueue: Map<HTMLElement, SubscribedInstance> = new Map();\n\n constructor(config?: Partial<VirtualizerConfig>) {\n this.config = { ...DEFAULT_VIRTUALIZER_CONFIG, ...config };\n }\n\n setup(root: HTMLElement | Document, contentContainer?: Element): void {\n if (this.root != null) {\n return;\n }\n this.root = root;\n this.resizeObserver = new ResizeObserver(this.handleContainerResize);\n this.intersectionObserver = new IntersectionObserver(\n this.handleIntersectionChange,\n {\n root: this.root,\n threshold: INTERSECTION_OBSERVER_THRESHOLD,\n rootMargin: `${this.config.intersectionObserverMargin}px 0px ${this.config.intersectionObserverMargin}px 0px`,\n // FIXME(amadeus): Figure out the other settings we'll want in here, or\n // if we should make them configurable...\n }\n );\n if (root instanceof Document) {\n this.setupWindow();\n } else {\n this.setupElement(contentContainer);\n }\n\n // FIXME(amadeus): Remove me before release\n window.__INSTANCE = this;\n window.__TOGGLE = () => {\n if (Virtualizer.__STOP) {\n Virtualizer.__STOP = false;\n const scroller = this.getScrollContainerElement() ?? window;\n scroller.scrollTo({ top: Virtualizer.__lastScrollPosition });\n queueRender(this.computeRenderRangeAndEmit);\n } else {\n Virtualizer.__lastScrollPosition = this.getScrollTop();\n Virtualizer.__STOP = true;\n }\n };\n for (const [container, instance] of this.connectQueue.entries()) {\n this.connect(container, instance);\n }\n this.connectQueue.clear();\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n instanceChanged(instance: SubscribedInstance, domDirty: boolean): void {\n this.instancesChanged.add(instance);\n if (domDirty) {\n this.markDOMDirty();\n }\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n // The render loop only reconciles heights for instances it repainted\n // itself, but content can also land outside a virtualizer pass (a\n // host/React-driven render call, an async highlight completion). Those\n // renders queue themselves here so their measured line deltas — wrapped\n // lines, annotation heights — are re-captured; otherwise a layout reset\n // (e.g. an edit-mode expandUnchanged flip) leaves the instance stuck on\n // its baseline estimate and its placeholder renders the wrong height.\n requestHeightReconcile(instance: SubscribedInstance): void {\n this.reconcileQueue.add(instance);\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n getWindowSpecs(): VirtualWindowSpecs {\n if (this.windowSpecs.top === 0 && this.windowSpecs.bottom === 0) {\n this.windowSpecs = createWindowFromScrollPosition({\n scrollTop: this.getScrollTop(),\n height: this.getHeight(),\n scrollHeight: this.getScrollHeight(),\n overscrollSize: this.config.overscrollSize,\n });\n }\n return this.windowSpecs;\n }\n\n isInstanceVisible(elementTop: number, elementHeight: number): boolean {\n const scrollTop = this.getScrollTop();\n const height = this.getHeight();\n const margin = this.config.intersectionObserverMargin;\n const top = scrollTop - margin;\n const bottom = scrollTop + height + margin;\n return !(elementTop < top - elementHeight || elementTop > bottom);\n }\n\n private handleContainerResize = (entries: ResizeObserverEntry[]) => {\n if (this.root == null) return;\n let shouldQueueUpdate = false;\n for (const entry of entries) {\n const blockSize = entry.borderBoxSize[0].blockSize;\n if (this.root instanceof Document) {\n if (blockSize !== this.scrollHeight) {\n this.scrollHeightDirty = true;\n shouldQueueUpdate = true;\n if (this.config.resizeDebugging) {\n console.log('Virtualizer: content size change', this.__id, {\n sizeChange: blockSize - lastSize,\n newSize: blockSize,\n });\n lastSize = blockSize;\n }\n }\n } else {\n if (entry.target === this.root) {\n if (blockSize !== this.height) {\n this.heightDirty = true;\n shouldQueueUpdate = true;\n }\n } else if (entry.target === this.contentContainer) {\n this.scrollHeightDirty = true;\n shouldQueueUpdate = true;\n if (this.config.resizeDebugging) {\n console.log('Virtualizer: scroller size change', this.__id, {\n sizeChange: blockSize - lastSize,\n newSize: blockSize,\n });\n lastSize = blockSize;\n }\n }\n }\n }\n\n if (shouldQueueUpdate) {\n queueRender(this.computeRenderRangeAndEmit);\n }\n };\n\n private setupWindow() {\n if (this.root == null || !(this.root instanceof Document)) {\n throw new Error('Virtualizer.setupWindow: Invalid setup method');\n }\n window.addEventListener('scroll', this.handleWindowScroll, {\n passive: true,\n });\n window.addEventListener('resize', this.handleWindowResize, {\n passive: true,\n });\n this.resizeObserver?.observe(this.root.documentElement);\n }\n\n private setupElement(contentContainer: Element | undefined) {\n if (this.root == null || this.root instanceof Document) {\n throw new Error('Virtualizer.setupElement: Invalid setup method');\n }\n this.root.addEventListener('scroll', this.handleElementScroll, {\n passive: true,\n });\n this.resizeObserver?.observe(this.root);\n contentContainer ??= this.root.firstElementChild ?? undefined;\n if (contentContainer instanceof HTMLElement) {\n this.contentContainer = contentContainer;\n this.resizeObserver?.observe(contentContainer);\n }\n }\n\n cleanUp(): void {\n dequeueRender(this.computeRenderRangeAndEmit);\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = undefined;\n this.root?.removeEventListener('scroll', this.handleElementScroll);\n window.removeEventListener('scroll', this.handleWindowScroll);\n window.removeEventListener('resize', this.handleWindowResize);\n this.root = undefined;\n this.contentContainer = undefined;\n this.observers.clear();\n this.visibleInstances.clear();\n this.instancesChanged.clear();\n this.reconcileQueue.clear();\n this.connectQueue.clear();\n this.visibleInstancesDirty = false;\n this.windowSpecs = { top: 0, bottom: 0 };\n this.scrollTop = 0;\n this.height = 0;\n this.scrollHeight = 0;\n }\n\n getOffsetInScrollContainer(element: HTMLElement): number {\n return (\n this.getScrollTop() +\n getRelativeBoundingTop(element, this.getScrollContainerElement())\n );\n }\n\n connect(container: HTMLElement, instance: SubscribedInstance): () => void {\n if (this.observers.has(container)) {\n throw new Error('Virtualizer.connect: instance is already connected...');\n }\n // If we are racing against the intersectionObserver, then we should just\n // queue up the connection for when the observer does get set up\n if (this.intersectionObserver == null) {\n this.connectQueue.set(container, instance);\n } else {\n // FIXME(amadeus): Go through the connection phase a bit more closely...\n this.intersectionObserver.observe(container);\n this.observers.set(container, instance);\n this.instancesChanged.add(instance);\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n return () => this.disconnect(container);\n }\n\n disconnect(container: HTMLElement): void {\n const instance = this.observers.get(container);\n this.connectQueue.delete(container);\n if (instance == null) {\n return;\n }\n this.intersectionObserver?.unobserve(container);\n this.observers.delete(container);\n this.instancesChanged.delete(instance);\n this.reconcileQueue.delete(instance);\n if (this.visibleInstances.delete(container)) {\n this.visibleInstancesDirty = true;\n }\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n private handleWindowResize = () => {\n if (Virtualizer.__STOP || window.innerHeight === this.height) {\n return;\n }\n this.heightDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private handleWindowScroll = () => {\n if (\n Virtualizer.__STOP ||\n this.root == null ||\n !(this.root instanceof Document)\n ) {\n return;\n }\n this.scrollDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private handleElementScroll = () => {\n if (\n Virtualizer.__STOP ||\n this.root == null ||\n this.root instanceof Document\n ) {\n return;\n }\n this.scrollDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private computeRenderRangeAndEmit = () => {\n if (Virtualizer.__STOP) {\n return;\n }\n const wrapperDirty = this.heightDirty || this.scrollHeightDirty;\n if (\n !this.scrollDirty &&\n !this.scrollHeightDirty &&\n !this.heightDirty &&\n this.renderedObservers === this.observers.size &&\n !this.visibleInstancesDirty &&\n this.instancesChanged.size === 0 &&\n this.reconcileQueue.size === 0\n ) {\n // NOTE(amadeus): Is this a safe assumption/optimization?\n return;\n }\n let instancesHaveChanged = this.instancesChanged.size > 0;\n\n // If we got an emitted update from a bunch of instances, we should skip\n // the window check first and attempt to render with existing logic first\n // and then queue up a corrected render after\n if (this.instancesChanged.size === 0) {\n const windowSpecs = createWindowFromScrollPosition({\n scrollTop: this.getScrollTop(),\n height: this.getHeight(),\n scrollHeight: this.getScrollHeight(),\n overscrollSize: this.config.overscrollSize,\n });\n if (\n !wrapperDirty &&\n areVirtualWindowSpecsEqual(this.windowSpecs, windowSpecs) &&\n this.renderedObservers === this.observers.size &&\n !this.visibleInstancesDirty &&\n this.reconcileQueue.size === 0\n ) {\n return;\n }\n this.windowSpecs = windowSpecs;\n }\n this.visibleInstancesDirty = false;\n this.renderedObservers = this.observers.size;\n const anchor = this.getScrollAnchor(this.height);\n const updatedInstances = new Set<SubscribedInstance>();\n // NOTE(amadeus): If the wrapper is dirty, we need to force every component\n // to re-render\n for (const instance of wrapperDirty\n ? this.observers.values()\n : this.visibleInstances.values()) {\n if (instance.onRender(wrapperDirty)) {\n updatedInstances.add(instance);\n }\n }\n for (const instance of this.instancesChanged) {\n if (updatedInstances.has(instance)) continue;\n if (instance.onRender(wrapperDirty)) {\n updatedInstances.add(instance);\n }\n }\n\n this.scrollFix(anchor);\n\n for (const instance of updatedInstances) {\n this.reconcileQueue.delete(instance);\n instance.reconcileHeights();\n }\n // Reconcile externally-rendered instances that may have new heights\n for (const instance of this.reconcileQueue) {\n if (instance.reconcileHeights()) {\n instancesHaveChanged = true;\n }\n }\n this.reconcileQueue.clear();\n instancesHaveChanged ||= this.instancesChanged.size > 0;\n\n // Reconciliation reads virtualized offsets and can consume dirty geometry\n // flags, so mark after it when an instance update needs a corrected pass.\n if (instancesHaveChanged) {\n this.markDOMDirty();\n }\n\n if (instancesHaveChanged || wrapperDirty) {\n queueRender(this.computeRenderRangeAndEmit);\n }\n updatedInstances.clear();\n this.instancesChanged.clear();\n };\n\n private scrollFix(anchor: ScrollAnchor | undefined) {\n if (anchor == null) {\n return;\n }\n const scrollContainer = this.getScrollContainerElement();\n const { lineIndex, lineOffset, fileElement, fileOffset, fileTypeOffset } =\n anchor;\n if (lineIndex != null && lineOffset != null) {\n const element = fileElement.shadowRoot?.querySelector(\n `[data-line][data-line-index=\"${lineIndex}\"]`\n );\n if (element instanceof HTMLElement) {\n const top = getRelativeBoundingTop(element, scrollContainer);\n if (top !== lineOffset) {\n const scrollOffset = top - lineOffset;\n this.applyScrollFix(scrollOffset);\n }\n return;\n }\n }\n const top = getRelativeBoundingTop(fileElement, scrollContainer);\n if (fileTypeOffset === 'top') {\n if (top !== fileOffset) {\n this.applyScrollFix(top - fileOffset);\n }\n } else {\n const bottom = top + fileElement.getBoundingClientRect().height;\n if (bottom !== fileOffset) {\n this.applyScrollFix(bottom - fileOffset);\n }\n }\n }\n\n private applyScrollFix(scrollOffset: number) {\n if (this.root == null || this.root instanceof Document) {\n window.scrollTo({\n top: window.scrollY + scrollOffset,\n behavior: 'instant',\n });\n } else {\n this.root.scrollTo({\n top: this.root.scrollTop + scrollOffset,\n behavior: 'instant',\n });\n }\n // Because we fixed our scroll positions, it means something resized or\n // moved around, so we should mark everything as dirty so the\n // reconciliation call will get the latest data when figuring calling\n // .getOffsetInScrollContainer\n this.markDOMDirty();\n }\n\n // This function tries to figure out the closest file or line to the viewport\n // top that's visible to use as a relative marker for how to fix scroll\n // position after issuing dom updates\n private getScrollAnchor(viewportHeight: number): ScrollAnchor | undefined {\n const scrollContainer = this.getScrollContainerElement();\n let bestAnchor: ScrollAnchor | undefined;\n\n for (const [fileElement] of this.visibleInstances.entries()) {\n const fileTop = getRelativeBoundingTop(fileElement, scrollContainer);\n const fileBottom = fileTop + fileElement.offsetHeight;\n\n // Determine file offset and type based on position\n // Only use bottom anchor when entire file is above viewport\n let fileOffset: number;\n let fileTypeOffset: 'top' | 'bottom';\n if (fileBottom <= 0) {\n // Entire file is above viewport - use bottom as anchor\n fileOffset = fileBottom;\n fileTypeOffset = 'bottom';\n } else {\n // File is at least partially visible or below - use top\n fileOffset = fileTop;\n fileTypeOffset = 'top';\n }\n\n // Find the best line (first fully visible) within this file\n let bestLineIndex: string | undefined;\n let bestLineOffset: number | undefined;\n\n // Only search for lines if file potentially intersects viewport\n if (fileBottom > 0 && fileTop < viewportHeight) {\n for (const line of fileElement.shadowRoot?.querySelectorAll(\n '[data-line][data-line-index]'\n ) ?? []) {\n if (!(line instanceof HTMLElement)) continue;\n const lineIndex = line.dataset.lineIndex;\n if (lineIndex == null) continue;\n\n const lineOffset = getRelativeBoundingTop(line, scrollContainer);\n\n // Ignore lines with negative offsets (above viewport top)\n if (lineOffset < 0) continue;\n\n // First visible line in DOM order is the best one because\n // querySelectorAll will grab lines in order as they appear in the\n // DOM\n bestLineIndex = lineIndex;\n bestLineOffset = lineOffset;\n break;\n }\n }\n\n // If we already have an anchor with a visible line, skip files without one\n if (bestAnchor?.lineOffset != null && bestLineOffset == null) {\n continue;\n }\n\n // Decide if this file should become the new best anchor\n let shouldReplace = false;\n // If we don't already have an anchor we should set one\n if (bestAnchor == null) {\n shouldReplace = true;\n }\n // If we found a better line anchor, we should replace the old one\n else if (\n bestLineOffset != null &&\n (bestAnchor.lineOffset == null ||\n bestLineOffset < bestAnchor.lineOffset)\n ) {\n shouldReplace = true;\n }\n // Otherwise we need to compare file only anchors\n else if (bestLineOffset == null && bestAnchor.lineOffset == null) {\n // Favor files with their tops in view\n if (\n fileOffset >= 0 &&\n (bestAnchor.fileOffset < 0 || fileOffset < bestAnchor.fileOffset)\n ) {\n shouldReplace = true;\n }\n // Or the closest file\n else if (\n fileOffset < 0 &&\n bestAnchor.fileOffset < 0 &&\n fileOffset > bestAnchor.fileOffset\n ) {\n shouldReplace = true;\n }\n }\n\n if (shouldReplace) {\n bestAnchor = {\n fileElement,\n fileTypeOffset,\n fileOffset,\n lineIndex: bestLineIndex,\n lineOffset: bestLineOffset,\n };\n }\n }\n\n return bestAnchor;\n }\n\n private handleIntersectionChange = (\n entries: IntersectionObserverEntry[]\n ): void => {\n this.scrollDirty = true;\n for (const { target, isIntersecting } of entries) {\n if (!(target instanceof HTMLElement)) {\n throw new Error(\n 'Virtualizer.handleIntersectionChange: target not an HTMLElement'\n );\n }\n const instance = this.observers.get(target);\n // IntersectionObserver delivers entries asynchronously, so an entry can\n // arrive after the target was unobserved via disconnect() or releaseElement().\n if (instance == null) {\n continue;\n }\n if (isIntersecting && !this.visibleInstances.has(target)) {\n instance.setVisibility(true);\n this.visibleInstances.set(target, instance);\n this.visibleInstancesDirty = true;\n } else if (!isIntersecting && this.visibleInstances.has(target)) {\n instance.setVisibility(false);\n this.visibleInstances.delete(target);\n this.visibleInstancesDirty = true;\n }\n }\n\n if (this.visibleInstancesDirty) {\n // Since this call is already debounced, should we just call\n // computeRenderRangeAndEmit directly?\n queueRender(this.computeRenderRangeAndEmit);\n }\n // Debug logging for visible instances\n // console.log(\n // 'handleIntersectionChange',\n // ...Array.from(this.visibleInstances.keys())\n // );\n };\n\n private getScrollTop() {\n if (!this.scrollDirty) {\n return this.scrollTop;\n }\n this.scrollDirty = false;\n let scrollTop = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return window.scrollY;\n }\n return this.root.scrollTop;\n })();\n\n // Lets always make sure to clamp scroll position cases of\n // over/bounce scroll\n scrollTop = Math.max(\n 0,\n Math.min(scrollTop, this.getScrollHeight() - this.getHeight())\n );\n this.scrollTop = scrollTop;\n return scrollTop;\n }\n\n private getScrollHeight() {\n if (!this.scrollHeightDirty) {\n return this.scrollHeight;\n }\n this.scrollHeightDirty = false;\n this.scrollHeight = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return this.root.documentElement.scrollHeight;\n }\n return this.root.scrollHeight;\n })();\n return this.scrollHeight;\n }\n\n private getHeight() {\n if (!this.heightDirty) {\n return this.height;\n }\n this.heightDirty = false;\n this.height = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return globalThis.innerHeight;\n }\n return this.root.getBoundingClientRect().height;\n })();\n return this.height;\n }\n\n markDOMDirty(): void {\n this.scrollDirty = true;\n this.scrollHeightDirty = true;\n this.heightDirty = true;\n }\n\n private getScrollContainerElement(): HTMLElement | undefined {\n return this.root == null || this.root instanceof Document\n ? undefined\n : this.root;\n }\n}\n\n// This function is like a generalized getBoundingClientRect for it's relative\n// scroll container\nfunction getRelativeBoundingTop(\n element: HTMLElement,\n scrollContainer: HTMLElement | undefined\n) {\n const rect = element.getBoundingClientRect();\n const scrollContainerTop = scrollContainer?.getBoundingClientRect().top ?? 0;\n return rect.top - scrollContainerTop;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Virtualizer.js","names":[],"sources":["../../src/components/Virtualizer.ts"],"sourcesContent":["import {\n dequeueRender,\n queueRender,\n} from '../managers/UniversalRenderingManager';\nimport type { VirtualWindowSpecs } from '../types';\nimport { areVirtualWindowSpecsEqual } from '../utils/areVirtualWindowSpecsEqual';\nimport { createWindowFromScrollPosition } from '../utils/createWindowFromScrollPosition';\n\ninterface SubscribedInstance {\n onRender(dirty: boolean): boolean;\n reconcileHeights(): boolean;\n setVisibility(visible: boolean): void;\n}\n\ninterface ScrollAnchor {\n fileElement: HTMLElement;\n fileTypeOffset: 'top' | 'bottom';\n fileOffset: number;\n lineIndex: string | undefined;\n lineOffset: number | undefined;\n}\n\n// 800 seems like the healthy overscan required to\n// keep safari from blanking... if we catch it tho, maybe 900\nconst DEFAULT_OVERSCROLL_SIZE = 1000;\nconst INTERSECTION_OBSERVER_MARGIN = DEFAULT_OVERSCROLL_SIZE * 4;\nconst INTERSECTION_OBSERVER_THRESHOLD = [0, 0.000001, 0.99999, 1];\n\nexport interface VirtualizerConfig {\n /** Extra pixels rendered above and below the viewport to reduce blanking during fast scrolls. */\n overscrollSize: number;\n /** Margin used by IntersectionObserver to decide when items should be considered visible. */\n intersectionObserverMargin: number;\n /** Enables noisy resize logs to help tune metrics and investigate scroll jitter. */\n resizeDebugging: boolean;\n}\n\nexport interface VirtualizerScrollOptions {\n top: number;\n behavior?: ScrollBehavior;\n}\n\nconst DEFAULT_VIRTUALIZER_CONFIG: VirtualizerConfig = {\n overscrollSize: DEFAULT_OVERSCROLL_SIZE,\n intersectionObserverMargin: INTERSECTION_OBSERVER_MARGIN,\n resizeDebugging: false,\n};\n\nlet lastSize = 0;\n\nlet instance = -1;\n\nexport class Virtualizer {\n static __STOP: boolean = false;\n static __lastScrollPosition = 0;\n\n public readonly __id: string = `virtualizer-${++instance}`;\n public readonly config: VirtualizerConfig;\n public type = 'simple' as const;\n private intersectionObserver: IntersectionObserver | undefined;\n private scrollTop: number = 0;\n private height: number = 0;\n private scrollHeight: number = 0;\n private windowSpecs: VirtualWindowSpecs = { top: 0, bottom: 0 };\n private root: HTMLElement | Document | undefined;\n private contentContainer: HTMLElement | undefined;\n\n private resizeObserver: ResizeObserver | undefined;\n private observers: Map<HTMLElement, SubscribedInstance> = new Map();\n private visibleInstances: Map<HTMLElement, SubscribedInstance> = new Map();\n private visibleInstancesDirty: boolean = false;\n private instancesChanged: Set<SubscribedInstance> = new Set();\n // Instances whose content was (re)painted outside a virtualizer-driven\n // onRender — host/React render calls, async highlight completions — and\n // still need a measured-height reconciliation pass. See\n // requestHeightReconcile.\n private reconcileQueue: Set<SubscribedInstance> = new Set();\n\n private scrollDirty = true;\n private heightDirty = true;\n private scrollHeightDirty = true;\n private renderedObservers = 0;\n private connectQueue: Map<HTMLElement, SubscribedInstance> = new Map();\n\n constructor(config?: Partial<VirtualizerConfig>) {\n this.config = { ...DEFAULT_VIRTUALIZER_CONFIG, ...config };\n }\n\n setup(root: HTMLElement | Document, contentContainer?: Element): void {\n if (this.root != null) {\n return;\n }\n this.root = root;\n this.resizeObserver = new ResizeObserver(this.handleContainerResize);\n this.intersectionObserver = new IntersectionObserver(\n this.handleIntersectionChange,\n {\n root: this.root,\n threshold: INTERSECTION_OBSERVER_THRESHOLD,\n rootMargin: `${this.config.intersectionObserverMargin}px 0px ${this.config.intersectionObserverMargin}px 0px`,\n // FIXME(amadeus): Figure out the other settings we'll want in here, or\n // if we should make them configurable...\n }\n );\n if (root instanceof Document) {\n this.setupWindow();\n } else {\n this.setupElement(contentContainer);\n }\n\n // FIXME(amadeus): Remove me before release\n window.__INSTANCE = this;\n window.__TOGGLE = () => {\n if (Virtualizer.__STOP) {\n Virtualizer.__STOP = false;\n const scroller = this.getScrollContainerElement() ?? window;\n scroller.scrollTo({ top: Virtualizer.__lastScrollPosition });\n queueRender(this.computeRenderRangeAndEmit);\n } else {\n Virtualizer.__lastScrollPosition = this.getScrollTop();\n Virtualizer.__STOP = true;\n }\n };\n for (const [container, instance] of this.connectQueue.entries()) {\n this.connect(container, instance);\n }\n this.connectQueue.clear();\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n instanceChanged(instance: SubscribedInstance, domDirty: boolean): void {\n this.instancesChanged.add(instance);\n if (domDirty) {\n this.markDOMDirty();\n }\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n // The render loop only reconciles heights for instances it repainted\n // itself, but content can also land outside a virtualizer pass (a\n // host/React-driven render call, an async highlight completion). Those\n // renders queue themselves here so their measured line deltas — wrapped\n // lines, annotation heights — are re-captured; otherwise a layout reset\n // (e.g. an edit-session exit recompute) leaves the instance stuck on its\n // baseline estimate and its placeholder renders the wrong height.\n requestHeightReconcile(instance: SubscribedInstance): void {\n this.reconcileQueue.add(instance);\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n getWindowSpecs(): VirtualWindowSpecs {\n if (this.windowSpecs.top === 0 && this.windowSpecs.bottom === 0) {\n this.windowSpecs = createWindowFromScrollPosition({\n scrollTop: this.getScrollTop(),\n height: this.getHeight(),\n scrollHeight: this.getScrollHeight(),\n overscrollSize: this.config.overscrollSize,\n });\n }\n return this.windowSpecs;\n }\n\n getRoot(): HTMLElement | Document | undefined {\n return this.root;\n }\n\n isInstanceVisible(elementTop: number, elementHeight: number): boolean {\n const scrollTop = this.getScrollTop();\n const height = this.getHeight();\n const margin = this.config.intersectionObserverMargin;\n const top = scrollTop - margin;\n const bottom = scrollTop + height + margin;\n return !(elementTop < top - elementHeight || elementTop > bottom);\n }\n\n private handleContainerResize = (entries: ResizeObserverEntry[]) => {\n if (this.root == null) return;\n let shouldQueueUpdate = false;\n for (const entry of entries) {\n const blockSize = entry.borderBoxSize[0].blockSize;\n if (this.root instanceof Document) {\n if (blockSize !== this.scrollHeight) {\n this.scrollHeightDirty = true;\n shouldQueueUpdate = true;\n if (this.config.resizeDebugging) {\n console.log('Virtualizer: content size change', this.__id, {\n sizeChange: blockSize - lastSize,\n newSize: blockSize,\n });\n lastSize = blockSize;\n }\n }\n } else {\n if (entry.target === this.root) {\n if (blockSize !== this.height) {\n this.heightDirty = true;\n shouldQueueUpdate = true;\n }\n } else if (entry.target === this.contentContainer) {\n this.scrollHeightDirty = true;\n shouldQueueUpdate = true;\n if (this.config.resizeDebugging) {\n console.log('Virtualizer: scroller size change', this.__id, {\n sizeChange: blockSize - lastSize,\n newSize: blockSize,\n });\n lastSize = blockSize;\n }\n }\n }\n }\n\n if (shouldQueueUpdate) {\n queueRender(this.computeRenderRangeAndEmit);\n }\n };\n\n private setupWindow() {\n if (this.root == null || !(this.root instanceof Document)) {\n throw new Error('Virtualizer.setupWindow: Invalid setup method');\n }\n window.addEventListener('scroll', this.handleWindowScroll, {\n passive: true,\n });\n window.addEventListener('resize', this.handleWindowResize, {\n passive: true,\n });\n this.resizeObserver?.observe(this.root.documentElement);\n }\n\n private setupElement(contentContainer: Element | undefined) {\n if (this.root == null || this.root instanceof Document) {\n throw new Error('Virtualizer.setupElement: Invalid setup method');\n }\n this.root.addEventListener('scroll', this.handleElementScroll, {\n passive: true,\n });\n this.resizeObserver?.observe(this.root);\n contentContainer ??= this.root.firstElementChild ?? undefined;\n if (contentContainer instanceof HTMLElement) {\n this.contentContainer = contentContainer;\n this.resizeObserver?.observe(contentContainer);\n }\n }\n\n cleanUp(): void {\n dequeueRender(this.computeRenderRangeAndEmit);\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = undefined;\n this.root?.removeEventListener('scroll', this.handleElementScroll);\n window.removeEventListener('scroll', this.handleWindowScroll);\n window.removeEventListener('resize', this.handleWindowResize);\n this.root = undefined;\n this.contentContainer = undefined;\n this.observers.clear();\n this.visibleInstances.clear();\n this.instancesChanged.clear();\n this.reconcileQueue.clear();\n this.connectQueue.clear();\n this.visibleInstancesDirty = false;\n this.windowSpecs = { top: 0, bottom: 0 };\n this.scrollTop = 0;\n this.height = 0;\n this.scrollHeight = 0;\n this.scrollDirty = true;\n this.heightDirty = true;\n this.scrollHeightDirty = true;\n }\n\n getOffsetInScrollContainer(element: HTMLElement): number {\n return (\n this.getScrollTop() +\n getRelativeBoundingTop(element, this.getScrollContainerElement())\n );\n }\n\n connect(container: HTMLElement, instance: SubscribedInstance): () => void {\n if (this.observers.has(container)) {\n throw new Error('Virtualizer.connect: instance is already connected...');\n }\n // If we are racing against the intersectionObserver, then we should just\n // queue up the connection for when the observer does get set up\n if (this.intersectionObserver == null) {\n this.connectQueue.set(container, instance);\n } else {\n // FIXME(amadeus): Go through the connection phase a bit more closely...\n this.intersectionObserver.observe(container);\n this.observers.set(container, instance);\n this.instancesChanged.add(instance);\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n return () => this.disconnect(container);\n }\n\n disconnect(container: HTMLElement): void {\n const instance = this.observers.get(container);\n this.connectQueue.delete(container);\n if (instance == null) {\n return;\n }\n this.intersectionObserver?.unobserve(container);\n this.observers.delete(container);\n this.instancesChanged.delete(instance);\n this.reconcileQueue.delete(instance);\n if (this.visibleInstances.delete(container)) {\n this.visibleInstancesDirty = true;\n }\n this.markDOMDirty();\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n private handleWindowResize = () => {\n if (Virtualizer.__STOP || window.innerHeight === this.height) {\n return;\n }\n this.heightDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private handleWindowScroll = () => {\n if (\n Virtualizer.__STOP ||\n this.root == null ||\n !(this.root instanceof Document)\n ) {\n return;\n }\n this.scrollDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private handleElementScroll = () => {\n if (\n Virtualizer.__STOP ||\n this.root == null ||\n this.root instanceof Document\n ) {\n return;\n }\n this.scrollDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n };\n\n private computeRenderRangeAndEmit = () => {\n if (Virtualizer.__STOP) {\n return;\n }\n const wrapperDirty = this.heightDirty || this.scrollHeightDirty;\n let instancesHaveChanged = this.instancesChanged.size > 0;\n\n // If we got an emitted update from a bunch of instances, we should skip\n // the window check first and attempt to render with existing logic first\n // and then queue up a corrected render after\n if (this.instancesChanged.size === 0) {\n const windowSpecs = createWindowFromScrollPosition({\n scrollTop: this.getScrollTop(),\n height: this.getHeight(),\n scrollHeight: this.getScrollHeight(),\n overscrollSize: this.config.overscrollSize,\n });\n if (\n !wrapperDirty &&\n areVirtualWindowSpecsEqual(this.windowSpecs, windowSpecs) &&\n this.renderedObservers === this.observers.size &&\n !this.visibleInstancesDirty &&\n this.reconcileQueue.size === 0\n ) {\n return;\n }\n this.windowSpecs = windowSpecs;\n }\n this.visibleInstancesDirty = false;\n this.renderedObservers = this.observers.size;\n const anchor = this.getScrollAnchor(this.height);\n const updatedInstances = new Set<SubscribedInstance>();\n // NOTE(amadeus): If the wrapper is dirty, we need to force every component\n // to re-render\n for (const instance of wrapperDirty\n ? this.observers.values()\n : this.visibleInstances.values()) {\n if (instance.onRender(wrapperDirty)) {\n updatedInstances.add(instance);\n }\n }\n for (const instance of this.instancesChanged) {\n if (updatedInstances.has(instance)) continue;\n if (instance.onRender(wrapperDirty)) {\n updatedInstances.add(instance);\n }\n }\n\n this.scrollFix(anchor);\n\n for (const instance of updatedInstances) {\n this.reconcileQueue.delete(instance);\n instance.reconcileHeights();\n }\n // Reconcile externally-rendered instances that may have new heights\n for (const instance of this.reconcileQueue) {\n if (instance.reconcileHeights()) {\n instancesHaveChanged = true;\n }\n }\n this.reconcileQueue.clear();\n instancesHaveChanged ||= this.instancesChanged.size > 0;\n\n // Reconciliation reads virtualized offsets and can consume dirty geometry\n // flags, so mark after it when an instance update needs a corrected pass.\n if (instancesHaveChanged) {\n this.markDOMDirty();\n }\n\n if (instancesHaveChanged || wrapperDirty) {\n queueRender(this.computeRenderRangeAndEmit);\n }\n updatedInstances.clear();\n this.instancesChanged.clear();\n };\n\n private scrollFix(anchor: ScrollAnchor | undefined) {\n if (anchor == null) {\n return;\n }\n const scrollContainer = this.getScrollContainerElement();\n const { lineIndex, lineOffset, fileElement, fileOffset, fileTypeOffset } =\n anchor;\n if (lineIndex != null && lineOffset != null) {\n const element = fileElement.shadowRoot?.querySelector(\n `[data-line][data-line-index=\"${lineIndex}\"]`\n );\n if (element instanceof HTMLElement) {\n const top = getRelativeBoundingTop(element, scrollContainer);\n if (top !== lineOffset) {\n const scrollOffset = top - lineOffset;\n this.applyScrollFix(scrollOffset);\n }\n return;\n }\n }\n const top = getRelativeBoundingTop(fileElement, scrollContainer);\n if (fileTypeOffset === 'top') {\n if (top !== fileOffset) {\n this.applyScrollFix(top - fileOffset);\n }\n } else {\n const bottom = top + fileElement.getBoundingClientRect().height;\n if (bottom !== fileOffset) {\n this.applyScrollFix(bottom - fileOffset);\n }\n }\n }\n\n private applyScrollFix(scrollOffset: number) {\n if (this.root == null || this.root instanceof Document) {\n window.scrollTo({\n top: window.scrollY + scrollOffset,\n behavior: 'instant',\n });\n } else {\n this.root.scrollTo({\n top: this.root.scrollTop + scrollOffset,\n behavior: 'instant',\n });\n }\n // Because we fixed our scroll positions, it means something resized or\n // moved around, so we should mark everything as dirty so the\n // reconciliation call will get the latest data when figuring calling\n // .getOffsetInScrollContainer\n this.markDOMDirty();\n }\n\n // This function tries to figure out the closest file or line to the viewport\n // top that's visible to use as a relative marker for how to fix scroll\n // position after issuing dom updates\n private getScrollAnchor(viewportHeight: number): ScrollAnchor | undefined {\n const scrollContainer = this.getScrollContainerElement();\n let bestAnchor: ScrollAnchor | undefined;\n\n for (const [fileElement] of this.visibleInstances.entries()) {\n const fileTop = getRelativeBoundingTop(fileElement, scrollContainer);\n const fileBottom = fileTop + fileElement.offsetHeight;\n\n // Determine file offset and type based on position\n // Only use bottom anchor when entire file is above viewport\n let fileOffset: number;\n let fileTypeOffset: 'top' | 'bottom';\n if (fileBottom <= 0) {\n // Entire file is above viewport - use bottom as anchor\n fileOffset = fileBottom;\n fileTypeOffset = 'bottom';\n } else {\n // File is at least partially visible or below - use top\n fileOffset = fileTop;\n fileTypeOffset = 'top';\n }\n\n // Find the best line (first fully visible) within this file\n let bestLineIndex: string | undefined;\n let bestLineOffset: number | undefined;\n\n // Only search for lines if file potentially intersects viewport\n if (fileBottom > 0 && fileTop < viewportHeight) {\n for (const line of fileElement.shadowRoot?.querySelectorAll(\n '[data-line][data-line-index]'\n ) ?? []) {\n if (!(line instanceof HTMLElement)) continue;\n const lineIndex = line.dataset.lineIndex;\n if (lineIndex == null) continue;\n\n const lineOffset = getRelativeBoundingTop(line, scrollContainer);\n\n // Ignore lines with negative offsets (above viewport top)\n if (lineOffset < 0) continue;\n\n // First visible line in DOM order is the best one because\n // querySelectorAll will grab lines in order as they appear in the\n // DOM\n bestLineIndex = lineIndex;\n bestLineOffset = lineOffset;\n break;\n }\n }\n\n // If we already have an anchor with a visible line, skip files without one\n if (bestAnchor?.lineOffset != null && bestLineOffset == null) {\n continue;\n }\n\n // Decide if this file should become the new best anchor\n let shouldReplace = false;\n // If we don't already have an anchor we should set one\n if (bestAnchor == null) {\n shouldReplace = true;\n }\n // If we found a better line anchor, we should replace the old one\n else if (\n bestLineOffset != null &&\n (bestAnchor.lineOffset == null ||\n bestLineOffset < bestAnchor.lineOffset)\n ) {\n shouldReplace = true;\n }\n // Otherwise we need to compare file only anchors\n else if (bestLineOffset == null && bestAnchor.lineOffset == null) {\n // Favor files with their tops in view\n if (\n fileOffset >= 0 &&\n (bestAnchor.fileOffset < 0 || fileOffset < bestAnchor.fileOffset)\n ) {\n shouldReplace = true;\n }\n // Or the closest file\n else if (\n fileOffset < 0 &&\n bestAnchor.fileOffset < 0 &&\n fileOffset > bestAnchor.fileOffset\n ) {\n shouldReplace = true;\n }\n }\n\n if (shouldReplace) {\n bestAnchor = {\n fileElement,\n fileTypeOffset,\n fileOffset,\n lineIndex: bestLineIndex,\n lineOffset: bestLineOffset,\n };\n }\n }\n\n return bestAnchor;\n }\n\n private handleIntersectionChange = (\n entries: IntersectionObserverEntry[]\n ): void => {\n this.scrollDirty = true;\n for (const { target, isIntersecting } of entries) {\n if (!(target instanceof HTMLElement)) {\n throw new Error(\n 'Virtualizer.handleIntersectionChange: target not an HTMLElement'\n );\n }\n const instance = this.observers.get(target);\n // IntersectionObserver delivers entries asynchronously, so an entry can\n // arrive after the target was unobserved via disconnect() or releaseElement().\n if (instance == null) {\n continue;\n }\n if (isIntersecting && !this.visibleInstances.has(target)) {\n instance.setVisibility(true);\n this.visibleInstances.set(target, instance);\n this.visibleInstancesDirty = true;\n } else if (!isIntersecting && this.visibleInstances.has(target)) {\n instance.setVisibility(false);\n this.visibleInstances.delete(target);\n this.visibleInstancesDirty = true;\n }\n }\n\n if (this.visibleInstancesDirty) {\n // Since this call is already debounced, should we just call\n // computeRenderRangeAndEmit directly?\n queueRender(this.computeRenderRangeAndEmit);\n }\n // Debug logging for visible instances\n // console.log(\n // 'handleIntersectionChange',\n // ...Array.from(this.visibleInstances.keys())\n // );\n };\n\n /** Return the logical vertical position for the scroll container */\n public getScrollTop(): number {\n if (!this.scrollDirty) {\n return this.scrollTop;\n }\n this.scrollDirty = false;\n let scrollTop = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return window.scrollY;\n }\n return this.root.scrollTop;\n })();\n\n // Lets always make sure to clamp scroll position cases of\n // over/bounce scroll\n scrollTop = this.clampScrollTop(scrollTop);\n this.scrollTop = scrollTop;\n return scrollTop;\n }\n\n /** Scroll to a logical vertical position and queue the normal render pass. */\n public scrollTo({ top, behavior = 'auto' }: VirtualizerScrollOptions): void {\n const { root } = this;\n if (root == null) {\n return;\n }\n\n const scrollTop = this.clampScrollTop(top);\n if (root instanceof Document) {\n window.scrollTo({ top: scrollTop, behavior });\n } else {\n root.scrollTo({ top: scrollTop, behavior });\n }\n this.scrollDirty = true;\n queueRender(this.computeRenderRangeAndEmit);\n }\n\n private clampScrollTop(scrollTop: number): number {\n return Math.max(\n 0,\n Math.min(scrollTop, this.getScrollHeight() - this.getHeight())\n );\n }\n\n private getScrollHeight() {\n if (!this.scrollHeightDirty) {\n return this.scrollHeight;\n }\n this.scrollHeightDirty = false;\n this.scrollHeight = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return this.root.documentElement.scrollHeight;\n }\n return this.root.scrollHeight;\n })();\n return this.scrollHeight;\n }\n\n private getHeight() {\n if (!this.heightDirty) {\n return this.height;\n }\n this.heightDirty = false;\n this.height = (() => {\n if (this.root == null) {\n return 0;\n }\n if (this.root instanceof Document) {\n return globalThis.innerHeight;\n }\n return this.root.getBoundingClientRect().height;\n })();\n return this.height;\n }\n\n markDOMDirty(): void {\n this.scrollDirty = true;\n this.scrollHeightDirty = true;\n this.heightDirty = true;\n }\n\n private getScrollContainerElement(): HTMLElement | undefined {\n return this.root == null || this.root instanceof Document\n ? undefined\n : this.root;\n }\n}\n\n// This function is like a generalized getBoundingClientRect for it's relative\n// scroll container\nfunction getRelativeBoundingTop(\n element: HTMLElement,\n scrollContainer: HTMLElement | undefined\n) {\n const rect = element.getBoundingClientRect();\n const scrollContainerTop = scrollContainer?.getBoundingClientRect().top ?? 0;\n return rect.top - scrollContainerTop;\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { Position, Range, TextEdit } from "../types.js";
2
+ import { ResolvedTextEdit, TextDocument, TextDocumentChange } from "../editor/textDocument.js";
3
+ import { IStateStorage, PersistStateStorage } from "../editor/stateStorage.js";
4
+ import { Editor, EditorFocusOptions, EditorOptions } from "../editor/editor.js";
5
+ export { Editor, EditorFocusOptions, EditorOptions, type IStateStorage, type PersistStateStorage, type Position, type Range, ResolvedTextEdit, TextDocument, TextDocumentChange, type TextEdit };
@@ -0,0 +1,3 @@
1
+ import { TextDocument } from "../editor/textDocument.js";
2
+ import { Editor } from "../editor/editor.js";
3
+ export { Editor, TextDocument };
@@ -1,5 +1,5 @@
1
1
  //#region src/editor/command.d.ts
2
- type EditorCommand = 'indent' | 'outdent' | 'undo' | 'redo' | 'selectAll' | 'findNextMatch' | 'openSearchPanel' | 'openSearchReplacePanel' | 'moveLineUp' | 'moveLineDown' | 'moveCursorToDocStart' | 'moveCursorToDocEnd' | 'expandSelectionDocStart' | 'expandSelectionDocEnd';
2
+ type EditorCommand = 'indent' | 'outdent' | 'indentLess' | 'indentMore' | 'undo' | 'redo' | 'selectAll' | 'findNextMatch' | 'openSearchPanel' | 'openSearchReplacePanel' | 'moveLineUp' | 'moveLineDown' | 'copyLineUp' | 'copyLineDown' | 'simplifySelection' | 'insertBlankLine' | 'deleteHardLineForward' | 'toggleComment' | 'toggleBlockComment' | 'moveCursorToDocStart' | 'moveCursorToDocEnd' | 'expandSelectionDocStart' | 'expandSelectionDocEnd';
3
3
  declare function resolveEditorCommandFromKeyboardEvent(event: KeyboardEvent, isMac?: boolean): EditorCommand | undefined;
4
4
  declare function resolveFindAgainShortcut(event: KeyboardEvent, isMac?: boolean): 'next' | 'previous' | undefined;
5
5
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","names":[],"sources":["../../src/editor/command.ts"],"mappings":";KAEY,aAAA;AAAA,iBAqBI,qCAAA,CACd,KAAA,EAAO,aAAA,EACP,KAAA,aACC,aAAa;AAAA,iBA2EA,wBAAA,CACd,KAAA,EAAO,aAAa,EACpB,KAAA"}
1
+ {"version":3,"file":"command.d.ts","names":[],"sources":["../../src/editor/command.ts"],"mappings":";KAEY,aAAA;AAAA,iBA8BI,qCAAA,CACd,KAAA,EAAO,aAAA,EACP,KAAA,aACC,aAAa;AAAA,iBAqIA,wBAAA,CACd,KAAA,EAAO,aAAa,EACpB,KAAA"}
@@ -8,6 +8,13 @@ function resolveEditorCommandFromKeyboardEvent(event, isMac = isMacLike()) {
8
8
  const hasPrimaryModifier = isPrimaryModifier(event, isMac);
9
9
  const { shiftKey, altKey, key } = event;
10
10
  const normalizedKey = key.length === 1 ? key.toLowerCase() : key;
11
+ if (isMac && event.ctrlKey && !event.metaKey && !shiftKey && !altKey && (normalizedKey === "k" || event.code === "KeyK")) return "deleteHardLineForward";
12
+ if (!event.metaKey && !event.ctrlKey && shiftKey && altKey && (normalizedKey === "a" || event.code === "KeyA")) return "toggleBlockComment";
13
+ if (!event.metaKey && !event.ctrlKey && shiftKey && altKey) {
14
+ if (normalizedKey === "ArrowUp") return "copyLineUp";
15
+ if (normalizedKey === "ArrowDown") return "copyLineDown";
16
+ }
17
+ if (!event.metaKey && !event.ctrlKey && !shiftKey && !altKey && normalizedKey === "Escape") return "simplifySelection";
11
18
  if (!event.metaKey && !shiftKey) {
12
19
  if (!event.ctrlKey && altKey && normalizedKey === "ArrowUp") return "moveLineUp";
13
20
  if (!event.ctrlKey && altKey && normalizedKey === "ArrowDown") return "moveLineDown";
@@ -20,6 +27,10 @@ function resolveEditorCommandFromKeyboardEvent(event, isMac = isMacLike()) {
20
27
  if (altKey) return;
21
28
  if (!hasPrimaryModifier && normalizedKey === "Tab") return shiftKey ? "outdent" : "indent";
22
29
  if (!hasPrimaryModifier) return;
30
+ if (!shiftKey && normalizedKey === "Enter") return "insertBlankLine";
31
+ if (!shiftKey && (normalizedKey === "/" || event.code === "Slash")) return "toggleComment";
32
+ if (!shiftKey && normalizedKey === "[") return "indentLess";
33
+ if (!shiftKey && normalizedKey === "]") return "indentMore";
23
34
  if (normalizedKey === "z") return shiftKey ? "redo" : "undo";
24
35
  if (!isMac && normalizedKey === "y") return "redo";
25
36
  if (normalizedKey === "Home" || isMac && normalizedKey === "ArrowUp") {
@@ -1 +1 @@
1
- {"version":3,"file":"command.js","names":[],"sources":["../../src/editor/command.ts"],"sourcesContent":["import { isMacLike, isPrimaryModifier } from './platform';\n\nexport type EditorCommand =\n | 'indent'\n | 'outdent'\n | 'undo'\n | 'redo'\n | 'selectAll'\n | 'findNextMatch'\n | 'openSearchPanel'\n | 'openSearchReplacePanel'\n | 'moveLineUp'\n | 'moveLineDown'\n | 'moveCursorToDocStart'\n | 'moveCursorToDocEnd'\n | 'expandSelectionDocStart'\n | 'expandSelectionDocEnd';\n\nconst SHORTCUTS: Partial<Record<string, EditorCommand>> = {\n a: 'selectAll',\n d: 'findNextMatch',\n};\n\nexport function resolveEditorCommandFromKeyboardEvent(\n event: KeyboardEvent,\n isMac: boolean = isMacLike()\n): EditorCommand | undefined {\n const hasPrimaryModifier = isPrimaryModifier(event, isMac);\n const { shiftKey, altKey, key } = event;\n\n const normalizedKey = key.length === 1 ? key.toLowerCase() : key;\n\n if (!event.metaKey && !shiftKey) {\n if (!event.ctrlKey && altKey && normalizedKey === 'ArrowUp') {\n return 'moveLineUp';\n }\n if (!event.ctrlKey && altKey && normalizedKey === 'ArrowDown') {\n return 'moveLineDown';\n }\n if (\n (isMac || navigator.platform.includes('Linux')) &&\n event.ctrlKey &&\n altKey\n ) {\n if (normalizedKey === 'p' || event.code === 'KeyP') {\n return 'moveLineUp';\n }\n if (normalizedKey === 'n' || event.code === 'KeyN') {\n return 'moveLineDown';\n }\n }\n }\n\n // cmd/ctrl+f opens the search panel in find mode; adding alt opens it in\n // find/replace mode. macOS emits a dead key for Option+F (key === 'ƒ'), so\n // fall back to event.code to detect the physical F key.\n if (hasPrimaryModifier && (normalizedKey === 'f' || event.code === 'KeyF')) {\n return altKey ? 'openSearchReplacePanel' : 'openSearchPanel';\n }\n\n if (altKey) {\n return undefined;\n }\n\n if (!hasPrimaryModifier && normalizedKey === 'Tab') {\n return shiftKey ? 'outdent' : 'indent';\n }\n\n if (!hasPrimaryModifier) {\n return undefined;\n }\n\n if (normalizedKey === 'z') {\n return shiftKey ? 'redo' : 'undo';\n }\n\n if (!isMac && normalizedKey === 'y') {\n return 'redo';\n }\n\n if (normalizedKey === 'Home' || (isMac && normalizedKey === 'ArrowUp')) {\n if (shiftKey) {\n return 'expandSelectionDocStart';\n }\n return 'moveCursorToDocStart';\n }\n\n if (normalizedKey === 'End' || (isMac && normalizedKey === 'ArrowDown')) {\n if (shiftKey) {\n return 'expandSelectionDocEnd';\n }\n return 'moveCursorToDocEnd';\n }\n\n return SHORTCUTS[normalizedKey];\n}\n\n// Detects the native \"find again\" shortcut (cmd/ctrl+g for next, adding shift\n// for previous) so the find/replace panel can override the browser default and\n// step through matches. macOS can emit a dead key for the physical G, so fall\n// back to event.code like the cmd+f handling above.\nexport function resolveFindAgainShortcut(\n event: KeyboardEvent,\n isMac: boolean = isMacLike()\n): 'next' | 'previous' | undefined {\n if (event.altKey) {\n return undefined;\n }\n if (!isPrimaryModifier(event, isMac)) {\n return undefined;\n }\n const normalizedKey =\n event.key.length === 1 ? event.key.toLowerCase() : event.key;\n if (normalizedKey === 'g' || event.code === 'KeyG') {\n return event.shiftKey ? 'previous' : 'next';\n }\n return undefined;\n}\n"],"mappings":";;AAkBA,MAAM,YAAoD;CACxD,GAAG;CACH,GAAG;AACL;AAEA,SAAgB,sCACd,OACA,QAAiB,UAAU,GACA;CAC3B,MAAM,qBAAqB,kBAAkB,OAAO,KAAK;CACzD,MAAM,EAAE,UAAU,QAAQ,QAAQ;CAElC,MAAM,gBAAgB,IAAI,WAAW,IAAI,IAAI,YAAY,IAAI;CAE7D,IAAI,CAAC,MAAM,WAAW,CAAC,UAAU;EAC/B,IAAI,CAAC,MAAM,WAAW,UAAU,kBAAkB,WAChD,OAAO;EAET,IAAI,CAAC,MAAM,WAAW,UAAU,kBAAkB,aAChD,OAAO;EAET,KACG,SAAS,UAAU,SAAS,SAAS,OAAO,MAC7C,MAAM,WACN,QACA;GACA,IAAI,kBAAkB,OAAO,MAAM,SAAS,QAC1C,OAAO;GAET,IAAI,kBAAkB,OAAO,MAAM,SAAS,QAC1C,OAAO;EAEX;CACF;CAKA,IAAI,uBAAuB,kBAAkB,OAAO,MAAM,SAAS,SACjE,OAAO,SAAS,2BAA2B;CAG7C,IAAI,QACF;CAGF,IAAI,CAAC,sBAAsB,kBAAkB,OAC3C,OAAO,WAAW,YAAY;CAGhC,IAAI,CAAC,oBACH;CAGF,IAAI,kBAAkB,KACpB,OAAO,WAAW,SAAS;CAG7B,IAAI,CAAC,SAAS,kBAAkB,KAC9B,OAAO;CAGT,IAAI,kBAAkB,UAAW,SAAS,kBAAkB,WAAY;EACtE,IAAI,UACF,OAAO;EAET,OAAO;CACT;CAEA,IAAI,kBAAkB,SAAU,SAAS,kBAAkB,aAAc;EACvE,IAAI,UACF,OAAO;EAET,OAAO;CACT;CAEA,OAAO,UAAU;AACnB;AAMA,SAAgB,yBACd,OACA,QAAiB,UAAU,GACM;CACjC,IAAI,MAAM,QACR;CAEF,IAAI,CAAC,kBAAkB,OAAO,KAAK,GACjC;CAIF,KADE,MAAM,IAAI,WAAW,IAAI,MAAM,IAAI,YAAY,IAAI,MAAM,SACrC,OAAO,MAAM,SAAS,QAC1C,OAAO,MAAM,WAAW,aAAa;AAGzC"}
1
+ {"version":3,"file":"command.js","names":[],"sources":["../../src/editor/command.ts"],"sourcesContent":["import { isMacLike, isPrimaryModifier } from './platform';\n\nexport type EditorCommand =\n | 'indent'\n | 'outdent'\n | 'indentLess'\n | 'indentMore'\n | 'undo'\n | 'redo'\n | 'selectAll'\n | 'findNextMatch'\n | 'openSearchPanel'\n | 'openSearchReplacePanel'\n | 'moveLineUp'\n | 'moveLineDown'\n | 'copyLineUp'\n | 'copyLineDown'\n | 'simplifySelection'\n | 'insertBlankLine'\n | 'deleteHardLineForward'\n | 'toggleComment'\n | 'toggleBlockComment'\n | 'moveCursorToDocStart'\n | 'moveCursorToDocEnd'\n | 'expandSelectionDocStart'\n | 'expandSelectionDocEnd';\n\nconst SHORTCUTS: Partial<Record<string, EditorCommand>> = {\n a: 'selectAll',\n d: 'findNextMatch',\n};\n\nexport function resolveEditorCommandFromKeyboardEvent(\n event: KeyboardEvent,\n isMac: boolean = isMacLike()\n): EditorCommand | undefined {\n const hasPrimaryModifier = isPrimaryModifier(event, isMac);\n const { shiftKey, altKey, key } = event;\n\n const normalizedKey = key.length === 1 ? key.toLowerCase() : key;\n\n // Safari and Firefox do not report macOS control+k as a beforeinput action,\n // so resolve the native delete-to-line-end shortcut from keydown instead.\n if (\n isMac &&\n event.ctrlKey &&\n !event.metaKey &&\n !shiftKey &&\n !altKey &&\n (normalizedKey === 'k' || event.code === 'KeyK')\n ) {\n return 'deleteHardLineForward';\n }\n\n if (\n !event.metaKey &&\n !event.ctrlKey &&\n shiftKey &&\n altKey &&\n (normalizedKey === 'a' || event.code === 'KeyA')\n ) {\n return 'toggleBlockComment';\n }\n\n if (!event.metaKey && !event.ctrlKey && shiftKey && altKey) {\n if (normalizedKey === 'ArrowUp') {\n return 'copyLineUp';\n }\n if (normalizedKey === 'ArrowDown') {\n return 'copyLineDown';\n }\n }\n\n if (\n !event.metaKey &&\n !event.ctrlKey &&\n !shiftKey &&\n !altKey &&\n normalizedKey === 'Escape'\n ) {\n return 'simplifySelection';\n }\n\n if (!event.metaKey && !shiftKey) {\n if (!event.ctrlKey && altKey && normalizedKey === 'ArrowUp') {\n return 'moveLineUp';\n }\n if (!event.ctrlKey && altKey && normalizedKey === 'ArrowDown') {\n return 'moveLineDown';\n }\n if (\n (isMac || navigator.platform.includes('Linux')) &&\n event.ctrlKey &&\n altKey\n ) {\n if (normalizedKey === 'p' || event.code === 'KeyP') {\n return 'moveLineUp';\n }\n if (normalizedKey === 'n' || event.code === 'KeyN') {\n return 'moveLineDown';\n }\n }\n }\n\n // cmd/ctrl+f opens the search panel in find mode; adding alt opens it in\n // find/replace mode. macOS emits a dead key for Option+F (key === 'ƒ'), so\n // fall back to event.code to detect the physical F key.\n if (hasPrimaryModifier && (normalizedKey === 'f' || event.code === 'KeyF')) {\n return altKey ? 'openSearchReplacePanel' : 'openSearchPanel';\n }\n\n if (altKey) {\n return undefined;\n }\n\n if (!hasPrimaryModifier && normalizedKey === 'Tab') {\n return shiftKey ? 'outdent' : 'indent';\n }\n\n if (!hasPrimaryModifier) {\n return undefined;\n }\n\n if (!shiftKey && normalizedKey === 'Enter') {\n return 'insertBlankLine';\n }\n\n if (!shiftKey && (normalizedKey === '/' || event.code === 'Slash')) {\n return 'toggleComment';\n }\n\n if (!shiftKey && normalizedKey === '[') {\n return 'indentLess';\n }\n\n if (!shiftKey && normalizedKey === ']') {\n return 'indentMore';\n }\n\n if (normalizedKey === 'z') {\n return shiftKey ? 'redo' : 'undo';\n }\n\n if (!isMac && normalizedKey === 'y') {\n return 'redo';\n }\n\n if (normalizedKey === 'Home' || (isMac && normalizedKey === 'ArrowUp')) {\n if (shiftKey) {\n return 'expandSelectionDocStart';\n }\n return 'moveCursorToDocStart';\n }\n\n if (normalizedKey === 'End' || (isMac && normalizedKey === 'ArrowDown')) {\n if (shiftKey) {\n return 'expandSelectionDocEnd';\n }\n return 'moveCursorToDocEnd';\n }\n\n return SHORTCUTS[normalizedKey];\n}\n\n// Detects the native \"find again\" shortcut (cmd/ctrl+g for next, adding shift\n// for previous) so the find/replace panel can override the browser default and\n// step through matches. macOS can emit a dead key for the physical G, so fall\n// back to event.code like the cmd+f handling above.\nexport function resolveFindAgainShortcut(\n event: KeyboardEvent,\n isMac: boolean = isMacLike()\n): 'next' | 'previous' | undefined {\n if (event.altKey) {\n return undefined;\n }\n if (!isPrimaryModifier(event, isMac)) {\n return undefined;\n }\n const normalizedKey =\n event.key.length === 1 ? event.key.toLowerCase() : event.key;\n if (normalizedKey === 'g' || event.code === 'KeyG') {\n return event.shiftKey ? 'previous' : 'next';\n }\n return undefined;\n}\n"],"mappings":";;AA2BA,MAAM,YAAoD;CACxD,GAAG;CACH,GAAG;AACL;AAEA,SAAgB,sCACd,OACA,QAAiB,UAAU,GACA;CAC3B,MAAM,qBAAqB,kBAAkB,OAAO,KAAK;CACzD,MAAM,EAAE,UAAU,QAAQ,QAAQ;CAElC,MAAM,gBAAgB,IAAI,WAAW,IAAI,IAAI,YAAY,IAAI;CAI7D,IACE,SACA,MAAM,WACN,CAAC,MAAM,WACP,CAAC,YACD,CAAC,WACA,kBAAkB,OAAO,MAAM,SAAS,SAEzC,OAAO;CAGT,IACE,CAAC,MAAM,WACP,CAAC,MAAM,WACP,YACA,WACC,kBAAkB,OAAO,MAAM,SAAS,SAEzC,OAAO;CAGT,IAAI,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW,YAAY,QAAQ;EAC1D,IAAI,kBAAkB,WACpB,OAAO;EAET,IAAI,kBAAkB,aACpB,OAAO;CAEX;CAEA,IACE,CAAC,MAAM,WACP,CAAC,MAAM,WACP,CAAC,YACD,CAAC,UACD,kBAAkB,UAElB,OAAO;CAGT,IAAI,CAAC,MAAM,WAAW,CAAC,UAAU;EAC/B,IAAI,CAAC,MAAM,WAAW,UAAU,kBAAkB,WAChD,OAAO;EAET,IAAI,CAAC,MAAM,WAAW,UAAU,kBAAkB,aAChD,OAAO;EAET,KACG,SAAS,UAAU,SAAS,SAAS,OAAO,MAC7C,MAAM,WACN,QACA;GACA,IAAI,kBAAkB,OAAO,MAAM,SAAS,QAC1C,OAAO;GAET,IAAI,kBAAkB,OAAO,MAAM,SAAS,QAC1C,OAAO;EAEX;CACF;CAKA,IAAI,uBAAuB,kBAAkB,OAAO,MAAM,SAAS,SACjE,OAAO,SAAS,2BAA2B;CAG7C,IAAI,QACF;CAGF,IAAI,CAAC,sBAAsB,kBAAkB,OAC3C,OAAO,WAAW,YAAY;CAGhC,IAAI,CAAC,oBACH;CAGF,IAAI,CAAC,YAAY,kBAAkB,SACjC,OAAO;CAGT,IAAI,CAAC,aAAa,kBAAkB,OAAO,MAAM,SAAS,UACxD,OAAO;CAGT,IAAI,CAAC,YAAY,kBAAkB,KACjC,OAAO;CAGT,IAAI,CAAC,YAAY,kBAAkB,KACjC,OAAO;CAGT,IAAI,kBAAkB,KACpB,OAAO,WAAW,SAAS;CAG7B,IAAI,CAAC,SAAS,kBAAkB,KAC9B,OAAO;CAGT,IAAI,kBAAkB,UAAW,SAAS,kBAAkB,WAAY;EACtE,IAAI,UACF,OAAO;EAET,OAAO;CACT;CAEA,IAAI,kBAAkB,SAAU,SAAS,kBAAkB,aAAc;EACvE,IAAI,UACF,OAAO;EAET,OAAO;CACT;CAEA,OAAO,UAAU;AACnB;AAMA,SAAgB,yBACd,OACA,QAAiB,UAAU,GACM;CACjC,IAAI,MAAM,QACR;CAEF,IAAI,CAAC,kBAAkB,OAAO,KAAK,GACjC;CAIF,KADE,MAAM,IAAI,WAAW,IAAI,MAAM,IAAI,YAAY,IAAI,MAAM,SACrC,OAAO,MAAM,SAAS,QAC1C,OAAO,MAAM,WAAW,aAAa;AAGzC"}
@@ -1,8 +1,8 @@
1
- import { DiffLineAnnotation } from "../types.js";
1
+ import { DiffLineAnnotation, EditorSelection } from "../types.js";
2
2
  import { ResolvedTextEdit, TextDocument } from "./textDocument.js";
3
- import { EditorSelection } from "./selection.js";
4
3
 
5
4
  //#region src/editor/editStack.d.ts
5
+ type EditStackCoalescingMode = 1 | 2 | 3;
6
6
  /** An entry in the edit stack. */
7
7
  interface EditStackEntry<LAnnotation> {
8
8
  /** Forward offset edits from the entry's base text to its final text. */
@@ -21,6 +21,8 @@ interface EditStackEntry<LAnnotation> {
21
21
  lineAnnotationsBefore?: DiffLineAnnotation<LAnnotation>[];
22
22
  /** Line annotations after the transaction. */
23
23
  lineAnnotationsAfter?: DiffLineAnnotation<LAnnotation>[];
24
+ /** Input mode inferred from the edit or retained by a coalesced run. */
25
+ coalescingMode?: EditStackCoalescingMode;
24
26
  /**
25
27
  * When `true`, this entry is its own undo step and never merges with the
26
28
  * entry before or after it. Set for paste and cut, which otherwise look like
@@ -51,6 +53,8 @@ declare class EditStack<LAnnotation> {
51
53
  setLastUndoLineAnnotations(lineAnnotationsBefore: DiffLineAnnotation<LAnnotation>[], lineAnnotationsAfter: DiffLineAnnotation<LAnnotation>[]): void;
52
54
  /** Returns the last undo entry, or `undefined` if empty. */
53
55
  peekUndo(): EditStackEntry<LAnnotation> | undefined;
56
+ /** Returns the last undo entry only while its coalescing group is open. */
57
+ peekUndoForCoalescing(): EditStackEntry<LAnnotation> | undefined;
54
58
  /** Replaces the last undo entry with the given entry. */
55
59
  replaceLastUndo(entry: EditStackEntry<LAnnotation>): void;
56
60
  /** Moves the latest undo entry to the redo stack and returns it, or `undefined` if empty. */
@@ -1 +1 @@
1
- {"version":3,"file":"editStack.d.ts","names":[],"sources":["../../src/editor/editStack.ts"],"mappings":";;;;;;UAQiB,cAAA;EAAA;EAEf,YAAA,EAAc,gBAAA;EAFe;EAI7B,YAAA,EAAc,gBAAA;EAAA;EAEd,aAAA;EAMkB;EAJlB,YAAA;EAMwB;EAJxB,gBAAA,GAAmB,eAAA;EAMI;EAJvB,eAAA,GAAkB,eAAA;EAIuB;EAFzC,qBAAA,GAAwB,kBAAA,CAAmB,WAAA;EAZ3C;EAcA,oBAAA,GAAuB,kBAAA,CAAmB,WAAA;EAZ1C;;;;;EAkBA,YAAA;AAAA;;UAIe,gBAAA;EAZS;EAcxB,UAAU;AAAA;;cAIC,SAAA;EAAA;EAKX,WAAA,CAAY,OAAA,GAAU,gBAAA;EAAA,IAOlB,OAAA;EAAA,IAIA,OAAA;EAtB2B;EA2B/B,KAAA;EAzBU;EA+BV,SAAA;EA3BW;EAgCX,IAAA,CAAK,KAAA,EAAO,cAAA,CAAe,WAAA;EAhCP;EAyCpB,0BAAA,CAA2B,UAAA,EAAY,eAAA;EATZ;EAmB3B,0BAAA,CACE,qBAAA,EAAuB,kBAAA,CAAmB,WAAA,KAC1C,oBAAA,EAAsB,kBAAA,CAAmB,WAAA;EAZJ;EAsBvC,QAAA,IAAY,cAAA,CAAe,WAAA;EAXF;EAgBzB,eAAA,CAAgB,KAAA,EAAO,cAAA,CAAe,WAAA;EAfd;EAyBxB,aAAA,IAAiB,cAAA,CAAe,WAAA;EAfpB;EAwBZ,aAAA,IAAiB,cAAA,CAAe,WAAA;AAAA;AAAA,iBASlB,oBAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,aAAA,EAAe,gBAAA,IACf,aAAA,UACA,YAAA,UACA,gBAAA,GAAmB,eAAA,IACnB,eAAA,GAAkB,eAAA,IAClB,qBAAA,GAAwB,kBAAA,CAAmB,WAAA,KAC3C,oBAAA,GAAuB,kBAAA,CAAmB,WAAA,MACzC,cAAA,CAAe,WAAA;;;;;;iBAiCF,4BAAA,cACd,aAAA,EAAe,cAAA,CAAe,WAAA,eAC9B,SAAA,EAAW,cAAA,CAAe,WAAA;;iBAoFZ,wBAAA,cACd,aAAA,EAAe,cAAA,CAAe,WAAA,GAC9B,SAAA,EAAW,cAAA,CAAe,WAAA,IACzB,cAAA,CAAe,WAAA"}
1
+ {"version":3,"file":"editStack.d.ts","names":[],"sources":["../../src/editor/editStack.ts"],"mappings":";;;;KASK,uBAAA;;UAGY,cAAA;EAHW;EAK1B,YAAA,EAAc,gBAAA;EALY;EAO1B,YAAA,EAAc,gBAAA;EAJC;EAMf,aAAA;EAN6B;EAQ7B,YAAA;EAJc;EAMd,gBAAA,GAAmB,eAAA;EAED;EAAlB,eAAA,GAAkB,eAAA;EAEM;EAAxB,qBAAA,GAAwB,kBAAA,CAAmB,WAAA;EAEpB;EAAvB,oBAAA,GAAuB,kBAAA,CAAmB,WAAA;EAEF;EAAxC,cAAA,GAAiB,uBAAA;EAlBa;;;;;EAwB9B,YAAA;AAAA;;UAIe,gBAAA;EAhBf;EAkBA,UAAU;AAAA;;cAIC,SAAA;EAAA;EAMX,WAAA,CAAY,OAAA,GAAU,gBAAA;EAAA,IAOlB,OAAA;EAAA,IAIA,OAAA;EAjCa;EAsCjB,KAAA;EAhCY;EAuCZ,SAAA;EAnCe;EAwCf,IAAA,CAAK,KAAA,EAAO,cAAA,CAAe,WAAA;;EAU3B,0BAAA,CAA2B,UAAA,EAAY,eAAA;EAhD7B;EA0DV,0BAAA,CACE,qBAAA,EAAuB,kBAAA,CAAmB,WAAA,KAC1C,oBAAA,EAAsB,kBAAA,CAAmB,WAAA;EAxDvB;EAkEpB,QAAA,IAAY,cAAA,CAAe,WAAA;EA5DL;EAiEtB,qBAAA,IAAyB,cAAA,CAAe,WAAA;EArC5B;EA0CZ,eAAA,CAAgB,KAAA,EAAO,cAAA,CAAe,WAAA;EArBM;EAgC5C,aAAA,IAAiB,cAAA,CAAe,WAAA;EA/BW;EAyC3C,aAAA,IAAiB,cAAA,CAAe,WAAA;AAAA;AAAA,iBAUlB,oBAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,aAAA,EAAe,gBAAA,IACf,aAAA,UACA,YAAA,UACA,gBAAA,GAAmB,eAAA,IACnB,eAAA,GAAkB,eAAA,IAClB,qBAAA,GAAwB,kBAAA,CAAmB,WAAA,KAC3C,oBAAA,GAAuB,kBAAA,CAAmB,WAAA,MACzC,cAAA,CAAe,WAAA;;;;;;iBAmEF,4BAAA,cACd,aAAA,EAAe,cAAA,CAAe,WAAA,eAC9B,SAAA,EAAW,cAAA,CAAe,WAAA;;iBA8FZ,wBAAA,cACd,aAAA,EAAe,cAAA,CAAe,WAAA,GAC9B,SAAA,EAAW,cAAA,CAAe,WAAA,IACzB,cAAA,CAAe,WAAA"}
@@ -1,11 +1,15 @@
1
1
  //#region src/editor/editStack.ts
2
2
  /** Largest number of undo or redo entries kept; oldest entries drop first once exceeded. */
3
3
  const DEFAULT_EDIT_STACK_MAX_ENTRIES = 100;
4
+ const COALESCING_MODE_INSERT = 1;
5
+ const COALESCING_MODE_BACKSPACE = 2;
6
+ const COALESCING_MODE_DELETE = 3;
4
7
  /** A stack of edit entries. */
5
8
  var EditStack = class {
6
9
  #undoStack = [];
7
10
  #redoStack = [];
8
11
  #maxEntries;
12
+ #canCoalesce = false;
9
13
  constructor(options) {
10
14
  this.#maxEntries = Math.max(1, options?.maxEntries ?? DEFAULT_EDIT_STACK_MAX_ENTRIES);
11
15
  }
@@ -19,6 +23,7 @@ var EditStack = class {
19
23
  clear() {
20
24
  this.#undoStack.length = 0;
21
25
  this.#redoStack.length = 0;
26
+ this.#canCoalesce = false;
22
27
  }
23
28
  /** Clears the redo stack. */
24
29
  clearRedo() {
@@ -28,6 +33,7 @@ var EditStack = class {
28
33
  push(entry) {
29
34
  this.#undoStack.push(entry);
30
35
  this.clearRedo();
36
+ this.#canCoalesce = true;
31
37
  if (this.#undoStack.length > this.#maxEntries) this.#undoStack.shift();
32
38
  }
33
39
  /** Sets the selections after the last undo entry. */
@@ -47,6 +53,10 @@ var EditStack = class {
47
53
  peekUndo() {
48
54
  return this.#undoStack[this.#undoStack.length - 1];
49
55
  }
56
+ /** Returns the last undo entry only while its coalescing group is open. */
57
+ peekUndoForCoalescing() {
58
+ return this.#canCoalesce ? this.peekUndo() : void 0;
59
+ }
50
60
  /** Replaces the last undo entry with the given entry. */
51
61
  replaceLastUndo(entry) {
52
62
  if (this.#undoStack.length === 0) {
@@ -55,12 +65,14 @@ var EditStack = class {
55
65
  }
56
66
  this.#undoStack[this.#undoStack.length - 1] = entry;
57
67
  this.clearRedo();
68
+ this.#canCoalesce = true;
58
69
  }
59
70
  /** Moves the latest undo entry to the redo stack and returns it, or `undefined` if empty. */
60
71
  popUndoToRedo() {
61
72
  const entry = this.#undoStack.pop();
62
73
  if (entry !== void 0) {
63
74
  this.#redoStack.push(entry);
75
+ this.#canCoalesce = false;
64
76
  return entry;
65
77
  }
66
78
  }
@@ -69,6 +81,7 @@ var EditStack = class {
69
81
  const entry = this.#redoStack.pop();
70
82
  if (entry !== void 0) {
71
83
  this.#undoStack.push(entry);
84
+ this.#canCoalesce = false;
72
85
  return entry;
73
86
  }
74
87
  }
@@ -76,6 +89,30 @@ var EditStack = class {
76
89
  function createEditStackEntry(textDocument, resolvedEdits, versionBefore, versionAfter, selectionsBefore, selectionsAfter, lineAnnotationsBefore, lineAnnotationsAfter) {
77
90
  const forwardEdits = [...resolvedEdits].sort((a, b) => a.start - b.start);
78
91
  const inverseEdits = [];
92
+ let coalescingMode;
93
+ if (selectionsBefore?.length === forwardEdits.length) {
94
+ const caretOffsets = [];
95
+ for (const selection of selectionsBefore) {
96
+ if (selection.start.line !== selection.end.line || selection.start.character !== selection.end.character) break;
97
+ caretOffsets.push(textDocument.offsetAt(selection.start));
98
+ }
99
+ if (caretOffsets.length === forwardEdits.length) {
100
+ caretOffsets.sort((a, b) => a - b);
101
+ let isBackspace = true;
102
+ let isDelete = true;
103
+ for (let i = 0; i < forwardEdits.length; i++) {
104
+ const edit = forwardEdits[i];
105
+ if (edit.text.length > 0 || edit.start === edit.end) {
106
+ isBackspace = false;
107
+ isDelete = false;
108
+ break;
109
+ }
110
+ isBackspace &&= caretOffsets[i] === edit.end;
111
+ isDelete &&= caretOffsets[i] === edit.start;
112
+ }
113
+ coalescingMode = isBackspace ? COALESCING_MODE_BACKSPACE : isDelete ? COALESCING_MODE_DELETE : void 0;
114
+ }
115
+ }
79
116
  for (let i = 0, offsetDelta = 0; i < forwardEdits.length; i++) {
80
117
  const edit = forwardEdits[i];
81
118
  const replacedText = textDocument.getTextSlice(edit.start, edit.end);
@@ -95,7 +132,8 @@ function createEditStackEntry(textDocument, resolvedEdits, versionBefore, versio
95
132
  selectionsBefore: selectionsBefore?.map((selection) => ({ ...selection })),
96
133
  selectionsAfter: selectionsAfter?.map((selection) => ({ ...selection })),
97
134
  lineAnnotationsBefore: lineAnnotationsBefore?.slice(),
98
- lineAnnotationsAfter: lineAnnotationsAfter?.slice()
135
+ lineAnnotationsAfter: lineAnnotationsAfter?.slice(),
136
+ coalescingMode
99
137
  };
100
138
  }
101
139
  /** Determines if the change matches following modes:
@@ -116,21 +154,19 @@ function shouldCoalesceEditStackEntry(previousEntry, nextEntry) {
116
154
  const nextIsInsert = nextForward.start === nextForward.end && nextForward.text.length > 0 && !nextForward.text.includes("\n") && nextInverse.text.length === 0;
117
155
  if (previousWasInsert && nextIsInsert) {
118
156
  if (nextForward.start !== previousInverse.end) return false;
119
- mode ??= "insert";
120
- if (mode !== "insert") return false;
157
+ mode ??= COALESCING_MODE_INSERT;
158
+ if (mode !== COALESCING_MODE_INSERT || previousEntry.coalescingMode !== void 0 && previousEntry.coalescingMode !== COALESCING_MODE_INSERT || nextEntry.coalescingMode !== void 0 && nextEntry.coalescingMode !== COALESCING_MODE_INSERT) return false;
121
159
  continue;
122
160
  }
123
161
  const previousWasDelete = previousForward.text.length === 0 && previousForward.end > previousForward.start && previousInverse.text.length > 0;
124
162
  const nextIsDelete = nextForward.text.length === 0 && nextForward.end > nextForward.start && nextInverse.text.length > 0;
125
163
  if (previousWasDelete && nextIsDelete) {
126
- if (mappedNextStart === previousForward.end) {
127
- mode ??= "delete";
128
- if (mode !== "delete") return false;
129
- continue;
130
- }
131
- if (mappedNextStart + (nextForward.end - nextForward.start) !== previousForward.start) return false;
132
- mode ??= "backspace";
133
- if (mode !== "backspace") return false;
164
+ let nextMode;
165
+ if (mappedNextStart === previousForward.end) nextMode = COALESCING_MODE_DELETE;
166
+ else if (mappedNextStart + (nextForward.end - nextForward.start) !== previousForward.start) return false;
167
+ else nextMode = COALESCING_MODE_BACKSPACE;
168
+ mode ??= nextMode;
169
+ if (mode !== nextMode || previousEntry.coalescingMode !== void 0 && previousEntry.coalescingMode !== nextMode || nextEntry.coalescingMode !== void 0 && nextEntry.coalescingMode !== nextMode) return false;
134
170
  continue;
135
171
  }
136
172
  return false;
@@ -141,6 +177,7 @@ function shouldCoalesceEditStackEntry(previousEntry, nextEntry) {
141
177
  function coalesceEditStackEntries(previousEntry, nextEntry) {
142
178
  const forwardEdits = [];
143
179
  const replacedTexts = [];
180
+ let coalescingMode;
144
181
  for (let i = 0; i < previousEntry.forwardEdits.length; i++) {
145
182
  const previousForward = previousEntry.forwardEdits[i];
146
183
  const previousInverse = previousEntry.inverseEdits[i];
@@ -148,6 +185,7 @@ function coalesceEditStackEntries(previousEntry, nextEntry) {
148
185
  const nextInverse = nextEntry.inverseEdits[i];
149
186
  const mappedNextStart = mapOffsetAfterForwardBatchToBefore(nextForward.start, previousEntry.forwardEdits);
150
187
  if (previousForward.text.length > 0) {
188
+ coalescingMode ??= COALESCING_MODE_INSERT;
151
189
  forwardEdits.push({
152
190
  start: previousForward.start,
153
191
  end: previousForward.end,
@@ -157,6 +195,7 @@ function coalesceEditStackEntries(previousEntry, nextEntry) {
157
195
  continue;
158
196
  }
159
197
  if (mappedNextStart === previousForward.end) {
198
+ coalescingMode ??= COALESCING_MODE_DELETE;
160
199
  forwardEdits.push({
161
200
  start: previousForward.start,
162
201
  end: mappedNextStart + (nextForward.end - nextForward.start),
@@ -165,6 +204,7 @@ function coalesceEditStackEntries(previousEntry, nextEntry) {
165
204
  replacedTexts.push(previousInverse.text + nextInverse.text);
166
205
  continue;
167
206
  }
207
+ coalescingMode ??= COALESCING_MODE_BACKSPACE;
168
208
  forwardEdits.push({
169
209
  start: Math.min(previousForward.start, mappedNextStart),
170
210
  end: previousForward.end,
@@ -180,7 +220,8 @@ function coalesceEditStackEntries(previousEntry, nextEntry) {
180
220
  selectionsBefore: previousEntry.selectionsBefore?.slice(),
181
221
  selectionsAfter: nextEntry.selectionsAfter?.slice(),
182
222
  lineAnnotationsBefore: previousEntry.lineAnnotationsBefore?.slice(),
183
- lineAnnotationsAfter: nextEntry.lineAnnotationsAfter?.slice()
223
+ lineAnnotationsAfter: nextEntry.lineAnnotationsAfter?.slice(),
224
+ coalescingMode
184
225
  };
185
226
  }
186
227
  function buildInverseEditsFromReplacedTexts(forwardEdits, replacedTexts) {