@pierre/diffs 1.3.0-beta.8 → 1.3.0-rc.1
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.
- package/dist/components/CodeView.d.ts +12 -12
- package/dist/components/CodeView.d.ts.map +1 -1
- package/dist/components/CodeView.js +51 -43
- package/dist/components/CodeView.js.map +1 -1
- package/dist/components/File.d.ts +7 -2
- package/dist/components/File.d.ts.map +1 -1
- package/dist/components/File.js +36 -8
- package/dist/components/File.js.map +1 -1
- package/dist/components/FileDiff.d.ts +34 -6
- package/dist/components/FileDiff.d.ts.map +1 -1
- package/dist/components/FileDiff.js +198 -17
- package/dist/components/FileDiff.js.map +1 -1
- package/dist/components/FileStream.d.ts.map +1 -1
- package/dist/components/FileStream.js +2 -1
- package/dist/components/FileStream.js.map +1 -1
- package/dist/components/VirtualizedFile.d.ts +2 -1
- package/dist/components/VirtualizedFile.d.ts.map +1 -1
- package/dist/components/VirtualizedFile.js +9 -5
- package/dist/components/VirtualizedFile.js.map +1 -1
- package/dist/components/VirtualizedFileDiff.d.ts +14 -0
- package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
- package/dist/components/VirtualizedFileDiff.js +61 -3
- package/dist/components/VirtualizedFileDiff.js.map +1 -1
- package/dist/components/Virtualizer.d.ts +14 -2
- package/dist/components/Virtualizer.d.ts.map +1 -1
- package/dist/components/Virtualizer.js +29 -3
- package/dist/components/Virtualizer.js.map +1 -1
- package/dist/edit/index.d.ts +5 -0
- package/dist/edit/index.js +3 -0
- package/dist/editor/command.d.ts +1 -1
- package/dist/editor/command.d.ts.map +1 -1
- package/dist/editor/command.js +11 -0
- package/dist/editor/command.js.map +1 -1
- package/dist/editor/editStack.d.ts +6 -2
- package/dist/editor/editStack.d.ts.map +1 -1
- package/dist/editor/editStack.js +54 -13
- package/dist/editor/editStack.js.map +1 -1
- package/dist/editor/editor.d.ts +51 -23
- package/dist/editor/editor.d.ts.map +1 -1
- package/dist/editor/editor.js +995 -242
- package/dist/editor/editor.js.map +1 -1
- package/dist/editor/editor2.js +1 -1
- package/dist/editor/languages.d.ts +26 -0
- package/dist/editor/languages.d.ts.map +1 -0
- package/dist/editor/languages.js +363 -0
- package/dist/editor/languages.js.map +1 -0
- package/dist/editor/lineAnnotations.d.ts.map +1 -1
- package/dist/editor/lineAnnotations.js +25 -26
- package/dist/editor/lineAnnotations.js.map +1 -1
- package/dist/editor/marker.d.ts +5 -3
- package/dist/editor/marker.d.ts.map +1 -1
- package/dist/editor/marker.js +111 -91
- package/dist/editor/marker.js.map +1 -1
- package/dist/editor/matchBrackets.d.ts +3 -2
- package/dist/editor/matchBrackets.d.ts.map +1 -1
- package/dist/editor/matchBrackets.js +2 -10
- package/dist/editor/matchBrackets.js.map +1 -1
- package/dist/editor/pieceTable.d.ts +2 -1
- package/dist/editor/pieceTable.d.ts.map +1 -1
- package/dist/editor/pieceTable.js +103 -39
- package/dist/editor/pieceTable.js.map +1 -1
- package/dist/editor/platform.js +2 -0
- package/dist/editor/platform.js.map +1 -1
- package/dist/editor/popover.d.ts +25 -5
- package/dist/editor/popover.d.ts.map +1 -1
- package/dist/editor/popover.js +60 -14
- package/dist/editor/popover.js.map +1 -1
- package/dist/editor/searchPanel.d.ts +1 -0
- package/dist/editor/searchPanel.d.ts.map +1 -1
- package/dist/editor/searchPanel.js +15 -8
- package/dist/editor/searchPanel.js.map +1 -1
- package/dist/editor/selection.d.ts +31 -11
- package/dist/editor/selection.d.ts.map +1 -1
- package/dist/editor/selection.js +198 -70
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/selectionAction.d.ts +4 -3
- package/dist/editor/selectionAction.d.ts.map +1 -1
- package/dist/editor/selectionAction.js +9 -5
- package/dist/editor/selectionAction.js.map +1 -1
- package/dist/editor/stateStorage.d.ts +15 -0
- package/dist/editor/stateStorage.d.ts.map +1 -0
- package/dist/editor/stateStorage.js +65 -0
- package/dist/editor/stateStorage.js.map +1 -0
- package/dist/editor/textDocument.d.ts +12 -77
- package/dist/editor/textDocument.d.ts.map +1 -1
- package/dist/editor/textDocument.js +76 -34
- package/dist/editor/textDocument.js.map +1 -1
- package/dist/editor/textMeasure.d.ts +7 -12
- package/dist/editor/textMeasure.d.ts.map +1 -1
- package/dist/editor/textMeasure.js +40 -32
- package/dist/editor/textMeasure.js.map +1 -1
- package/dist/editor/{tokenzier.d.ts → tokenizer.d.ts} +3 -3
- package/dist/editor/tokenizer.d.ts.map +1 -0
- package/dist/editor/{tokenzier.js → tokenizer.js} +91 -26
- package/dist/editor/tokenizer.js.map +1 -0
- package/dist/editor/utils.js +1 -1
- package/dist/editor/utils.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +7 -6
- package/dist/managers/InteractionManager.d.ts +18 -7
- package/dist/managers/InteractionManager.d.ts.map +1 -1
- package/dist/managers/InteractionManager.js +154 -99
- package/dist/managers/InteractionManager.js.map +1 -1
- package/dist/managers/UniversalRenderingManager.d.ts +2 -1
- package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
- package/dist/managers/UniversalRenderingManager.js +7 -3
- package/dist/managers/UniversalRenderingManager.js.map +1 -1
- package/dist/react/CodeView.d.ts +13 -13
- package/dist/react/CodeView.d.ts.map +1 -1
- package/dist/react/CodeView.js +38 -29
- package/dist/react/CodeView.js.map +1 -1
- package/dist/react/EditContext.d.ts +20 -0
- package/dist/react/EditContext.d.ts.map +1 -0
- package/dist/react/EditContext.js +20 -0
- package/dist/react/EditContext.js.map +1 -0
- package/dist/react/File.d.ts +2 -1
- package/dist/react/File.d.ts.map +1 -1
- package/dist/react/File.js +3 -2
- package/dist/react/File.js.map +1 -1
- package/dist/react/FileDiff.d.ts +2 -2
- package/dist/react/FileDiff.d.ts.map +1 -1
- package/dist/react/FileDiff.js +3 -2
- package/dist/react/FileDiff.js.map +1 -1
- package/dist/react/MultiFileDiff.d.ts +2 -2
- package/dist/react/MultiFileDiff.d.ts.map +1 -1
- package/dist/react/MultiFileDiff.js +3 -2
- package/dist/react/MultiFileDiff.js.map +1 -1
- package/dist/react/PatchDiff.d.ts +2 -2
- package/dist/react/PatchDiff.d.ts.map +1 -1
- package/dist/react/PatchDiff.js +3 -2
- package/dist/react/PatchDiff.js.map +1 -1
- package/dist/react/UnresolvedFile.d.ts +1 -1
- package/dist/react/UnresolvedFile.js.map +1 -1
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +3 -3
- package/dist/react/types.d.ts +9 -1
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/utils/useFileDiffInstance.d.ts +5 -3
- package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
- package/dist/react/utils/useFileDiffInstance.js +28 -27
- package/dist/react/utils/useFileDiffInstance.js.map +1 -1
- package/dist/react/utils/useFileInstance.d.ts +5 -3
- package/dist/react/utils/useFileInstance.d.ts.map +1 -1
- package/dist/react/utils/useFileInstance.js +29 -25
- package/dist/react/utils/useFileInstance.js.map +1 -1
- package/dist/react/utils/useUnresolvedFileInstance.js +3 -3
- package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
- package/dist/renderers/DiffHunksRenderer.d.ts +33 -1
- package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
- package/dist/renderers/DiffHunksRenderer.js +134 -21
- package/dist/renderers/DiffHunksRenderer.js.map +1 -1
- package/dist/renderers/FileRenderer.d.ts +2 -0
- package/dist/renderers/FileRenderer.d.ts.map +1 -1
- package/dist/renderers/FileRenderer.js +33 -11
- package/dist/renderers/FileRenderer.js.map +1 -1
- package/dist/ssr/FileDiffReact.js +1 -1
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/preloadDiffs.js +1 -1
- package/dist/style.js +1 -1
- package/dist/types.d.ts +185 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/annotationHelpers.d.ts +20 -0
- package/dist/utils/annotationHelpers.d.ts.map +1 -0
- package/dist/utils/annotationHelpers.js +29 -0
- package/dist/utils/annotationHelpers.js.map +1 -0
- package/dist/utils/computeFileOffsets.d.ts.map +1 -1
- package/dist/utils/computeFileOffsets.js +34 -5
- package/dist/utils/computeFileOffsets.js.map +1 -1
- package/dist/utils/editSessionHunks.d.ts +66 -0
- package/dist/utils/editSessionHunks.d.ts.map +1 -0
- package/dist/utils/editSessionHunks.js +437 -0
- package/dist/utils/editSessionHunks.js.map +1 -0
- package/dist/utils/getHunkSideBoundaries.d.ts +7 -0
- package/dist/utils/getHunkSideBoundaries.d.ts.map +1 -0
- package/dist/utils/getHunkSideBoundaries.js +12 -0
- package/dist/utils/getHunkSideBoundaries.js.map +1 -0
- package/dist/utils/getTotalLineCountFromHunks.d.ts.map +1 -1
- package/dist/utils/getTotalLineCountFromHunks.js +2 -1
- package/dist/utils/getTotalLineCountFromHunks.js.map +1 -1
- package/dist/utils/hydratePartialDiff.d.ts.map +1 -1
- package/dist/utils/hydratePartialDiff.js +4 -3
- package/dist/utils/hydratePartialDiff.js.map +1 -1
- package/dist/utils/isGutterUtilityPath.d.ts +5 -0
- package/dist/utils/isGutterUtilityPath.d.ts.map +1 -0
- package/dist/utils/isGutterUtilityPath.js +12 -0
- package/dist/utils/isGutterUtilityPath.js.map +1 -0
- package/dist/utils/iterateOverDiff.d.ts.map +1 -1
- package/dist/utils/iterateOverDiff.js +17 -12
- package/dist/utils/iterateOverDiff.js.map +1 -1
- package/dist/utils/parseMergeConflictDiffFromFile.d.ts.map +1 -1
- package/dist/utils/parseMergeConflictDiffFromFile.js +9 -6
- package/dist/utils/parseMergeConflictDiffFromFile.js.map +1 -1
- package/dist/utils/parsePatchFiles.d.ts.map +1 -1
- package/dist/utils/parsePatchFiles.js +6 -3
- package/dist/utils/parsePatchFiles.js.map +1 -1
- package/dist/utils/realignChangeContent.d.ts +33 -0
- package/dist/utils/realignChangeContent.d.ts.map +1 -0
- package/dist/utils/realignChangeContent.js +148 -0
- package/dist/utils/realignChangeContent.js.map +1 -0
- package/dist/utils/renderDiffWithHighlighter.js +1 -1
- package/dist/utils/resolveRegion.d.ts.map +1 -1
- package/dist/utils/resolveRegion.js +18 -2
- package/dist/utils/resolveRegion.js.map +1 -1
- package/dist/utils/updateDiffHunks.d.ts +8 -2
- package/dist/utils/updateDiffHunks.d.ts.map +1 -1
- package/dist/utils/updateDiffHunks.js +9 -7
- package/dist/utils/updateDiffHunks.js.map +1 -1
- package/dist/utils/virtualDiffLayout.d.ts +42 -2
- package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
- package/dist/utils/virtualDiffLayout.js +101 -7
- package/dist/utils/virtualDiffLayout.js.map +1 -1
- package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
- package/dist/worker/WorkerPoolManager.js +4 -2
- package/dist/worker/WorkerPoolManager.js.map +1 -1
- package/dist/worker/types.d.ts +4 -0
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/worker-portable.js +60 -20
- package/dist/worker/worker-portable.js.map +1 -1
- package/dist/worker/worker.js +61 -19
- package/dist/worker/worker.js.map +1 -1
- package/package.json +13 -8
- package/dist/editor/index.d.ts +0 -3
- package/dist/editor/index.js +0 -3
- package/dist/editor/tokenzier.d.ts.map +0 -1
- package/dist/editor/tokenzier.js.map +0 -1
- package/dist/react/EditorContext.d.ts +0 -15
- package/dist/react/EditorContext.d.ts.map +0 -1
- package/dist/react/EditorContext.js +0 -23
- package/dist/react/EditorContext.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Virtualizer.js","names":[],"sources":["../../src/components/Virtualizer.ts"],"sourcesContent":["import { queueRender } 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 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 };
|
package/dist/editor/command.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/editor/command.js
CHANGED
|
@@ -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":";;
|
|
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":"
|
|
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"}
|
package/dist/editor/editStack.js
CHANGED
|
@@ -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:
|
|
@@ -115,22 +153,20 @@ function shouldCoalesceEditStackEntry(previousEntry, nextEntry) {
|
|
|
115
153
|
const previousWasInsert = previousForward.start <= previousForward.end && previousForward.text.length > 0 && !previousForward.text.includes("\n") && !previousInverse.text.includes("\n");
|
|
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
|
-
if (
|
|
119
|
-
mode ??=
|
|
120
|
-
if (mode !==
|
|
156
|
+
if (nextForward.start !== previousInverse.end) 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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
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) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editStack.js","names":["#maxEntries","#undoStack","#redoStack"],"sources":["../../src/editor/editStack.ts"],"sourcesContent":["import type { DiffLineAnnotation } from '../types';\nimport type { EditorSelection } from './selection';\nimport type { ResolvedTextEdit, TextDocument } from './textDocument';\n\n/** Largest number of undo or redo entries kept; oldest entries drop first once exceeded. */\nconst DEFAULT_EDIT_STACK_MAX_ENTRIES = 100;\n\n/** An entry in the edit stack. */\nexport interface EditStackEntry<LAnnotation> {\n /** Forward offset edits from the entry's base text to its final text. */\n forwardEdits: ResolvedTextEdit[];\n /** Inverse offset edits from the entry's final text back to its base text. */\n inverseEdits: ResolvedTextEdit[];\n /** Document version before the entry is applied. */\n versionBefore: number;\n /** Document version after the entry is applied. */\n versionAfter: number;\n /** Selection before the transaction. */\n selectionsBefore?: EditorSelection[];\n /** Selection after the transaction. */\n selectionsAfter?: EditorSelection[];\n /** Line annotations before the transaction. */\n lineAnnotationsBefore?: DiffLineAnnotation<LAnnotation>[];\n /** Line annotations after the transaction. */\n lineAnnotationsAfter?: DiffLineAnnotation<LAnnotation>[];\n /**\n * When `true`, this entry is its own undo step and never merges with the\n * entry before or after it. Set for paste and cut, which otherwise look like\n * typing and would merge into the previous keystroke.\n */\n undoBoundary?: boolean;\n}\n\n/** Options for the edit stack. */\nexport interface EditStackOptions {\n /** The maximum number of entries to keep in the undo stack. */\n maxEntries?: number;\n}\n\n/** A stack of edit entries. */\nexport class EditStack<LAnnotation> {\n #undoStack: EditStackEntry<LAnnotation>[] = [];\n #redoStack: EditStackEntry<LAnnotation>[] = [];\n #maxEntries: number;\n\n constructor(options?: EditStackOptions) {\n this.#maxEntries = Math.max(\n 1,\n options?.maxEntries ?? DEFAULT_EDIT_STACK_MAX_ENTRIES\n );\n }\n\n get canUndo(): boolean {\n return this.#undoStack.length > 0;\n }\n\n get canRedo(): boolean {\n return this.#redoStack.length > 0;\n }\n\n /** Clears both the undo and redo stacks. */\n clear(): void {\n this.#undoStack.length = 0;\n this.#redoStack.length = 0;\n }\n\n /** Clears the redo stack. */\n clearRedo(): void {\n this.#redoStack.length = 0;\n }\n\n /** Pushes a new entry onto the undo stack. */\n push(entry: EditStackEntry<LAnnotation>): void {\n this.#undoStack.push(entry);\n this.clearRedo();\n if (this.#undoStack.length > this.#maxEntries) {\n this.#undoStack.shift();\n }\n }\n\n /** Sets the selections after the last undo entry. */\n setLastUndoSelectionsAfter(selections: EditorSelection[]): void {\n const lastEntry = this.#undoStack[this.#undoStack.length - 1];\n if (lastEntry !== undefined) {\n lastEntry.selectionsAfter = selections.map((selection) => ({\n ...selection,\n }));\n }\n }\n\n /** Sets the line annotations after the last undo entry. */\n setLastUndoLineAnnotations(\n lineAnnotationsBefore: DiffLineAnnotation<LAnnotation>[],\n lineAnnotationsAfter: DiffLineAnnotation<LAnnotation>[]\n ): void {\n const lastEntry = this.#undoStack[this.#undoStack.length - 1];\n if (lastEntry !== undefined) {\n lastEntry.lineAnnotationsBefore = lineAnnotationsBefore.slice();\n lastEntry.lineAnnotationsAfter = lineAnnotationsAfter.slice();\n }\n }\n\n /** Returns the last undo entry, or `undefined` if empty. */\n peekUndo(): EditStackEntry<LAnnotation> | undefined {\n return this.#undoStack[this.#undoStack.length - 1];\n }\n\n /** Replaces the last undo entry with the given entry. */\n replaceLastUndo(entry: EditStackEntry<LAnnotation>): void {\n if (this.#undoStack.length === 0) {\n this.push(entry);\n return;\n }\n this.#undoStack[this.#undoStack.length - 1] = entry;\n this.clearRedo();\n }\n\n /** Moves the latest undo entry to the redo stack and returns it, or `undefined` if empty. */\n popUndoToRedo(): EditStackEntry<LAnnotation> | void {\n const entry = this.#undoStack.pop();\n if (entry !== undefined) {\n this.#redoStack.push(entry);\n return entry;\n }\n }\n\n /** Moves the latest redo entry back to the undo stack and returns it, or `undefined` if empty. */\n popRedoToUndo(): EditStackEntry<LAnnotation> | void {\n const entry = this.#redoStack.pop();\n if (entry !== undefined) {\n this.#undoStack.push(entry);\n return entry;\n }\n }\n}\n\nexport function createEditStackEntry<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n resolvedEdits: ResolvedTextEdit[],\n versionBefore: number,\n versionAfter: number,\n selectionsBefore?: EditorSelection[],\n selectionsAfter?: EditorSelection[],\n lineAnnotationsBefore?: DiffLineAnnotation<LAnnotation>[],\n lineAnnotationsAfter?: DiffLineAnnotation<LAnnotation>[]\n): EditStackEntry<LAnnotation> {\n const forwardEdits = [...resolvedEdits].sort((a, b) => a.start - b.start);\n const inverseEdits: ResolvedTextEdit[] = [];\n for (let i = 0, offsetDelta = 0; i < forwardEdits.length; i++) {\n const edit = forwardEdits[i];\n const replacedText = textDocument.getTextSlice(edit.start, edit.end);\n const startAfterEdit = edit.start + offsetDelta;\n inverseEdits.push({\n start: startAfterEdit,\n end: startAfterEdit + edit.text.length,\n text: replacedText,\n });\n offsetDelta += edit.text.length - (edit.end - edit.start);\n }\n return {\n forwardEdits: forwardEdits.map((edit) => ({ ...edit })),\n inverseEdits: inverseEdits,\n versionBefore,\n versionAfter,\n selectionsBefore: selectionsBefore?.map((selection) => ({\n ...selection,\n })),\n selectionsAfter: selectionsAfter?.map((selection) => ({ ...selection })),\n lineAnnotationsBefore: lineAnnotationsBefore?.slice(),\n lineAnnotationsAfter: lineAnnotationsAfter?.slice(),\n };\n}\n\n/** Determines if the change matches following modes:\n * - 'insert': simple typing\n * - 'backspace': backward delete\n * - 'delete': forward delete\n */\nexport function shouldCoalesceEditStackEntry<LAnnotation>(\n previousEntry: EditStackEntry<LAnnotation> | undefined,\n nextEntry: EditStackEntry<LAnnotation>\n): boolean {\n if (\n previousEntry === undefined ||\n previousEntry.undoBoundary === true ||\n nextEntry.undoBoundary === true ||\n previousEntry.forwardEdits.length === 0 ||\n previousEntry.forwardEdits.length !== previousEntry.inverseEdits.length ||\n previousEntry.forwardEdits.length !== nextEntry.forwardEdits.length ||\n nextEntry.forwardEdits.length !== nextEntry.inverseEdits.length\n ) {\n return false;\n }\n let mode: 'insert' | 'backspace' | 'delete' | undefined;\n for (let i = 0; i < previousEntry.forwardEdits.length; i++) {\n const previousForward = previousEntry.forwardEdits[i];\n const previousInverse = previousEntry.inverseEdits[i];\n const nextForward = nextEntry.forwardEdits[i];\n const nextInverse = nextEntry.inverseEdits[i];\n const mappedNextStart = mapOffsetAfterForwardBatchToBefore(\n nextForward.start,\n previousEntry.forwardEdits\n );\n const previousWasInsert =\n previousForward.start <= previousForward.end &&\n previousForward.text.length > 0 &&\n !previousForward.text.includes('\\n') &&\n !previousInverse.text.includes('\\n');\n const nextIsInsert =\n nextForward.start === nextForward.end &&\n nextForward.text.length > 0 &&\n !nextForward.text.includes('\\n') &&\n nextInverse.text.length === 0;\n if (previousWasInsert && nextIsInsert) {\n const expectedMappedNextStart = previousForward.end;\n // Allow continuing typing after replacing a selection (e.g. \"hello\" -> \"w\")\n // while still requiring that the cursor extension maps inside the same base range.\n if (mappedNextStart !== expectedMappedNextStart) {\n return false;\n }\n mode ??= 'insert';\n if (mode !== 'insert') {\n return false;\n }\n continue;\n }\n const previousWasDelete =\n previousForward.text.length === 0 &&\n previousForward.end > previousForward.start &&\n previousInverse.text.length > 0;\n const nextIsDelete =\n nextForward.text.length === 0 &&\n nextForward.end > nextForward.start &&\n nextInverse.text.length > 0;\n if (previousWasDelete && nextIsDelete) {\n if (mappedNextStart === previousForward.end) {\n mode ??= 'delete';\n if (mode !== 'delete') {\n return false;\n }\n continue;\n }\n if (\n mappedNextStart + (nextForward.end - nextForward.start) !==\n previousForward.start\n ) {\n return false;\n }\n mode ??= 'backspace';\n if (mode !== 'backspace') {\n return false;\n }\n continue;\n }\n return false;\n }\n return mode !== undefined;\n}\n\n/** Coalesce edit stack entries for simple typing and single-character deletes. */\nexport function coalesceEditStackEntries<LAnnotation>(\n previousEntry: EditStackEntry<LAnnotation>,\n nextEntry: EditStackEntry<LAnnotation>\n): EditStackEntry<LAnnotation> {\n const forwardEdits: ResolvedTextEdit[] = [];\n const replacedTexts: string[] = [];\n for (let i = 0; i < previousEntry.forwardEdits.length; i++) {\n const previousForward = previousEntry.forwardEdits[i];\n const previousInverse = previousEntry.inverseEdits[i];\n const nextForward = nextEntry.forwardEdits[i];\n const nextInverse = nextEntry.inverseEdits[i];\n const mappedNextStart = mapOffsetAfterForwardBatchToBefore(\n nextForward.start,\n previousEntry.forwardEdits\n );\n\n if (previousForward.text.length > 0) {\n forwardEdits.push({\n start: previousForward.start,\n end: previousForward.end,\n text: previousForward.text + nextForward.text,\n });\n replacedTexts.push(previousInverse.text);\n continue;\n }\n\n if (mappedNextStart === previousForward.end) {\n forwardEdits.push({\n start: previousForward.start,\n end: mappedNextStart + (nextForward.end - nextForward.start),\n text: '',\n });\n replacedTexts.push(previousInverse.text + nextInverse.text);\n continue;\n }\n\n forwardEdits.push({\n start: Math.min(previousForward.start, mappedNextStart),\n end: previousForward.end,\n text: '',\n });\n replacedTexts.push(nextInverse.text + previousInverse.text);\n }\n\n return {\n forwardEdits,\n inverseEdits: buildInverseEditsFromReplacedTexts(\n forwardEdits,\n replacedTexts\n ),\n versionBefore: previousEntry.versionBefore,\n versionAfter: nextEntry.versionAfter,\n selectionsBefore: previousEntry.selectionsBefore?.slice(),\n selectionsAfter: nextEntry.selectionsAfter?.slice(),\n lineAnnotationsBefore: previousEntry.lineAnnotationsBefore?.slice(),\n lineAnnotationsAfter: nextEntry.lineAnnotationsAfter?.slice(),\n };\n}\n\nfunction buildInverseEditsFromReplacedTexts(\n forwardEdits: readonly ResolvedTextEdit[],\n replacedTexts: readonly string[]\n): ResolvedTextEdit[] {\n const inverseEdits: ResolvedTextEdit[] = [];\n for (let i = 0, offsetDelta = 0; i < forwardEdits.length; i++) {\n const edit = forwardEdits[i];\n const startAfterEdit = edit.start + offsetDelta;\n inverseEdits.push({\n start: startAfterEdit,\n end: startAfterEdit + edit.text.length,\n text: replacedTexts[i],\n });\n offsetDelta += edit.text.length - (edit.end - edit.start);\n }\n return inverseEdits;\n}\n\nfunction mapOffsetAfterForwardBatchToBefore(\n offsetAfter: number,\n forwardEdits: readonly ResolvedTextEdit[]\n): number {\n let offset = offsetAfter;\n for (const edit of forwardEdits) {\n const oldLength = edit.end - edit.start;\n const newLength = edit.text.length;\n const delta = newLength - oldLength;\n if (offset < edit.start) {\n continue;\n }\n if (offset >= edit.start + newLength) {\n offset -= delta;\n continue;\n }\n offset = edit.start + Math.min(offset - edit.start, oldLength);\n }\n return offset;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"editStack.js","names":["#maxEntries","#undoStack","#redoStack","#canCoalesce"],"sources":["../../src/editor/editStack.ts"],"sourcesContent":["import type { DiffLineAnnotation, EditorSelection } from '../types';\nimport type { ResolvedTextEdit, TextDocument } from './textDocument';\n\n/** Largest number of undo or redo entries kept; oldest entries drop first once exceeded. */\nconst DEFAULT_EDIT_STACK_MAX_ENTRIES = 100;\n\nconst COALESCING_MODE_INSERT = 1;\nconst COALESCING_MODE_BACKSPACE = 2;\nconst COALESCING_MODE_DELETE = 3;\ntype EditStackCoalescingMode = 1 | 2 | 3;\n\n/** An entry in the edit stack. */\nexport interface EditStackEntry<LAnnotation> {\n /** Forward offset edits from the entry's base text to its final text. */\n forwardEdits: ResolvedTextEdit[];\n /** Inverse offset edits from the entry's final text back to its base text. */\n inverseEdits: ResolvedTextEdit[];\n /** Document version before the entry is applied. */\n versionBefore: number;\n /** Document version after the entry is applied. */\n versionAfter: number;\n /** Selection before the transaction. */\n selectionsBefore?: EditorSelection[];\n /** Selection after the transaction. */\n selectionsAfter?: EditorSelection[];\n /** Line annotations before the transaction. */\n lineAnnotationsBefore?: DiffLineAnnotation<LAnnotation>[];\n /** Line annotations after the transaction. */\n lineAnnotationsAfter?: DiffLineAnnotation<LAnnotation>[];\n /** Input mode inferred from the edit or retained by a coalesced run. */\n coalescingMode?: EditStackCoalescingMode;\n /**\n * When `true`, this entry is its own undo step and never merges with the\n * entry before or after it. Set for paste and cut, which otherwise look like\n * typing and would merge into the previous keystroke.\n */\n undoBoundary?: boolean;\n}\n\n/** Options for the edit stack. */\nexport interface EditStackOptions {\n /** The maximum number of entries to keep in the undo stack. */\n maxEntries?: number;\n}\n\n/** A stack of edit entries. */\nexport class EditStack<LAnnotation> {\n #undoStack: EditStackEntry<LAnnotation>[] = [];\n #redoStack: EditStackEntry<LAnnotation>[] = [];\n #maxEntries: number;\n #canCoalesce = false;\n\n constructor(options?: EditStackOptions) {\n this.#maxEntries = Math.max(\n 1,\n options?.maxEntries ?? DEFAULT_EDIT_STACK_MAX_ENTRIES\n );\n }\n\n get canUndo(): boolean {\n return this.#undoStack.length > 0;\n }\n\n get canRedo(): boolean {\n return this.#redoStack.length > 0;\n }\n\n /** Clears both the undo and redo stacks. */\n clear(): void {\n this.#undoStack.length = 0;\n this.#redoStack.length = 0;\n this.#canCoalesce = false;\n }\n\n /** Clears the redo stack. */\n clearRedo(): void {\n this.#redoStack.length = 0;\n }\n\n /** Pushes a new entry onto the undo stack. */\n push(entry: EditStackEntry<LAnnotation>): void {\n this.#undoStack.push(entry);\n this.clearRedo();\n this.#canCoalesce = true;\n if (this.#undoStack.length > this.#maxEntries) {\n this.#undoStack.shift();\n }\n }\n\n /** Sets the selections after the last undo entry. */\n setLastUndoSelectionsAfter(selections: EditorSelection[]): void {\n const lastEntry = this.#undoStack[this.#undoStack.length - 1];\n if (lastEntry !== undefined) {\n lastEntry.selectionsAfter = selections.map((selection) => ({\n ...selection,\n }));\n }\n }\n\n /** Sets the line annotations after the last undo entry. */\n setLastUndoLineAnnotations(\n lineAnnotationsBefore: DiffLineAnnotation<LAnnotation>[],\n lineAnnotationsAfter: DiffLineAnnotation<LAnnotation>[]\n ): void {\n const lastEntry = this.#undoStack[this.#undoStack.length - 1];\n if (lastEntry !== undefined) {\n lastEntry.lineAnnotationsBefore = lineAnnotationsBefore.slice();\n lastEntry.lineAnnotationsAfter = lineAnnotationsAfter.slice();\n }\n }\n\n /** Returns the last undo entry, or `undefined` if empty. */\n peekUndo(): EditStackEntry<LAnnotation> | undefined {\n return this.#undoStack[this.#undoStack.length - 1];\n }\n\n /** Returns the last undo entry only while its coalescing group is open. */\n peekUndoForCoalescing(): EditStackEntry<LAnnotation> | undefined {\n return this.#canCoalesce ? this.peekUndo() : undefined;\n }\n\n /** Replaces the last undo entry with the given entry. */\n replaceLastUndo(entry: EditStackEntry<LAnnotation>): void {\n if (this.#undoStack.length === 0) {\n this.push(entry);\n return;\n }\n this.#undoStack[this.#undoStack.length - 1] = entry;\n this.clearRedo();\n this.#canCoalesce = true;\n }\n\n /** Moves the latest undo entry to the redo stack and returns it, or `undefined` if empty. */\n popUndoToRedo(): EditStackEntry<LAnnotation> | void {\n const entry = this.#undoStack.pop();\n if (entry !== undefined) {\n this.#redoStack.push(entry);\n this.#canCoalesce = false;\n return entry;\n }\n }\n\n /** Moves the latest redo entry back to the undo stack and returns it, or `undefined` if empty. */\n popRedoToUndo(): EditStackEntry<LAnnotation> | void {\n const entry = this.#redoStack.pop();\n if (entry !== undefined) {\n this.#undoStack.push(entry);\n this.#canCoalesce = false;\n return entry;\n }\n }\n}\n\nexport function createEditStackEntry<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n resolvedEdits: ResolvedTextEdit[],\n versionBefore: number,\n versionAfter: number,\n selectionsBefore?: EditorSelection[],\n selectionsAfter?: EditorSelection[],\n lineAnnotationsBefore?: DiffLineAnnotation<LAnnotation>[],\n lineAnnotationsAfter?: DiffLineAnnotation<LAnnotation>[]\n): EditStackEntry<LAnnotation> {\n const forwardEdits = [...resolvedEdits].sort((a, b) => a.start - b.start);\n const inverseEdits: ResolvedTextEdit[] = [];\n let coalescingMode: EditStackCoalescingMode | undefined;\n if (selectionsBefore?.length === forwardEdits.length) {\n const caretOffsets: number[] = [];\n for (const selection of selectionsBefore) {\n if (\n selection.start.line !== selection.end.line ||\n selection.start.character !== selection.end.character\n ) {\n break;\n }\n caretOffsets.push(textDocument.offsetAt(selection.start));\n }\n if (caretOffsets.length === forwardEdits.length) {\n caretOffsets.sort((a, b) => a - b);\n let isBackspace = true;\n let isDelete = true;\n for (let i = 0; i < forwardEdits.length; i++) {\n const edit = forwardEdits[i];\n if (edit.text.length > 0 || edit.start === edit.end) {\n isBackspace = false;\n isDelete = false;\n break;\n }\n isBackspace &&= caretOffsets[i] === edit.end;\n isDelete &&= caretOffsets[i] === edit.start;\n }\n coalescingMode = isBackspace\n ? COALESCING_MODE_BACKSPACE\n : isDelete\n ? COALESCING_MODE_DELETE\n : undefined;\n }\n }\n for (let i = 0, offsetDelta = 0; i < forwardEdits.length; i++) {\n const edit = forwardEdits[i];\n const replacedText = textDocument.getTextSlice(edit.start, edit.end);\n const startAfterEdit = edit.start + offsetDelta;\n inverseEdits.push({\n start: startAfterEdit,\n end: startAfterEdit + edit.text.length,\n text: replacedText,\n });\n offsetDelta += edit.text.length - (edit.end - edit.start);\n }\n return {\n forwardEdits: forwardEdits.map((edit) => ({ ...edit })),\n inverseEdits: inverseEdits,\n versionBefore,\n versionAfter,\n selectionsBefore: selectionsBefore?.map((selection) => ({\n ...selection,\n })),\n selectionsAfter: selectionsAfter?.map((selection) => ({ ...selection })),\n lineAnnotationsBefore: lineAnnotationsBefore?.slice(),\n lineAnnotationsAfter: lineAnnotationsAfter?.slice(),\n coalescingMode,\n };\n}\n\n/** Determines if the change matches following modes:\n * - 'insert': simple typing\n * - 'backspace': backward delete\n * - 'delete': forward delete\n */\nexport function shouldCoalesceEditStackEntry<LAnnotation>(\n previousEntry: EditStackEntry<LAnnotation> | undefined,\n nextEntry: EditStackEntry<LAnnotation>\n): boolean {\n if (\n previousEntry === undefined ||\n previousEntry.undoBoundary === true ||\n nextEntry.undoBoundary === true ||\n previousEntry.forwardEdits.length === 0 ||\n previousEntry.forwardEdits.length !== previousEntry.inverseEdits.length ||\n previousEntry.forwardEdits.length !== nextEntry.forwardEdits.length ||\n nextEntry.forwardEdits.length !== nextEntry.inverseEdits.length\n ) {\n return false;\n }\n let mode: EditStackCoalescingMode | undefined;\n for (let i = 0; i < previousEntry.forwardEdits.length; i++) {\n const previousForward = previousEntry.forwardEdits[i];\n const previousInverse = previousEntry.inverseEdits[i];\n const nextForward = nextEntry.forwardEdits[i];\n const nextInverse = nextEntry.inverseEdits[i];\n const mappedNextStart = mapOffsetAfterForwardBatchToBefore(\n nextForward.start,\n previousEntry.forwardEdits\n );\n const previousWasInsert =\n previousForward.start <= previousForward.end &&\n previousForward.text.length > 0 &&\n !previousForward.text.includes('\\n') &&\n !previousInverse.text.includes('\\n');\n const nextIsInsert =\n nextForward.start === nextForward.end &&\n nextForward.text.length > 0 &&\n !nextForward.text.includes('\\n') &&\n nextInverse.text.length === 0;\n if (previousWasInsert && nextIsInsert) {\n // Merge only when the next insert starts exactly where the previous\n // inserted text ends, in after-edit offsets. `previousInverse.end` marks\n // that point, since undo replaces exactly the inserted range. A\n // base-offset check can't tell this apart from a caret that jumped to the\n // left edge: both edges of a pure insert map back to the same base offset,\n // so typing \"a\" then \"b\" at the left edge would merge as \"ab\" while the\n // buffer holds \"ba\", corrupting redo.\n if (nextForward.start !== previousInverse.end) {\n return false;\n }\n mode ??= COALESCING_MODE_INSERT;\n if (\n mode !== COALESCING_MODE_INSERT ||\n (previousEntry.coalescingMode !== undefined &&\n previousEntry.coalescingMode !== COALESCING_MODE_INSERT) ||\n (nextEntry.coalescingMode !== undefined &&\n nextEntry.coalescingMode !== COALESCING_MODE_INSERT)\n ) {\n return false;\n }\n continue;\n }\n const previousWasDelete =\n previousForward.text.length === 0 &&\n previousForward.end > previousForward.start &&\n previousInverse.text.length > 0;\n const nextIsDelete =\n nextForward.text.length === 0 &&\n nextForward.end > nextForward.start &&\n nextInverse.text.length > 0;\n if (previousWasDelete && nextIsDelete) {\n let nextMode: EditStackCoalescingMode;\n if (mappedNextStart === previousForward.end) {\n nextMode = COALESCING_MODE_DELETE;\n } else if (\n mappedNextStart + (nextForward.end - nextForward.start) !==\n previousForward.start\n ) {\n return false;\n } else {\n nextMode = COALESCING_MODE_BACKSPACE;\n }\n mode ??= nextMode;\n if (\n mode !== nextMode ||\n (previousEntry.coalescingMode !== undefined &&\n previousEntry.coalescingMode !== nextMode) ||\n (nextEntry.coalescingMode !== undefined &&\n nextEntry.coalescingMode !== nextMode)\n ) {\n return false;\n }\n continue;\n }\n return false;\n }\n return mode !== undefined;\n}\n\n/** Coalesce edit stack entries for simple typing and single-character deletes. */\nexport function coalesceEditStackEntries<LAnnotation>(\n previousEntry: EditStackEntry<LAnnotation>,\n nextEntry: EditStackEntry<LAnnotation>\n): EditStackEntry<LAnnotation> {\n const forwardEdits: ResolvedTextEdit[] = [];\n const replacedTexts: string[] = [];\n let coalescingMode: EditStackCoalescingMode | undefined;\n for (let i = 0; i < previousEntry.forwardEdits.length; i++) {\n const previousForward = previousEntry.forwardEdits[i];\n const previousInverse = previousEntry.inverseEdits[i];\n const nextForward = nextEntry.forwardEdits[i];\n const nextInverse = nextEntry.inverseEdits[i];\n const mappedNextStart = mapOffsetAfterForwardBatchToBefore(\n nextForward.start,\n previousEntry.forwardEdits\n );\n\n if (previousForward.text.length > 0) {\n coalescingMode ??= COALESCING_MODE_INSERT;\n forwardEdits.push({\n start: previousForward.start,\n end: previousForward.end,\n text: previousForward.text + nextForward.text,\n });\n replacedTexts.push(previousInverse.text);\n continue;\n }\n\n if (mappedNextStart === previousForward.end) {\n coalescingMode ??= COALESCING_MODE_DELETE;\n forwardEdits.push({\n start: previousForward.start,\n end: mappedNextStart + (nextForward.end - nextForward.start),\n text: '',\n });\n replacedTexts.push(previousInverse.text + nextInverse.text);\n continue;\n }\n\n coalescingMode ??= COALESCING_MODE_BACKSPACE;\n forwardEdits.push({\n start: Math.min(previousForward.start, mappedNextStart),\n end: previousForward.end,\n text: '',\n });\n replacedTexts.push(nextInverse.text + previousInverse.text);\n }\n\n return {\n forwardEdits,\n inverseEdits: buildInverseEditsFromReplacedTexts(\n forwardEdits,\n replacedTexts\n ),\n versionBefore: previousEntry.versionBefore,\n versionAfter: nextEntry.versionAfter,\n selectionsBefore: previousEntry.selectionsBefore?.slice(),\n selectionsAfter: nextEntry.selectionsAfter?.slice(),\n lineAnnotationsBefore: previousEntry.lineAnnotationsBefore?.slice(),\n lineAnnotationsAfter: nextEntry.lineAnnotationsAfter?.slice(),\n coalescingMode,\n };\n}\n\nfunction buildInverseEditsFromReplacedTexts(\n forwardEdits: readonly ResolvedTextEdit[],\n replacedTexts: readonly string[]\n): ResolvedTextEdit[] {\n const inverseEdits: ResolvedTextEdit[] = [];\n for (let i = 0, offsetDelta = 0; i < forwardEdits.length; i++) {\n const edit = forwardEdits[i];\n const startAfterEdit = edit.start + offsetDelta;\n inverseEdits.push({\n start: startAfterEdit,\n end: startAfterEdit + edit.text.length,\n text: replacedTexts[i],\n });\n offsetDelta += edit.text.length - (edit.end - edit.start);\n }\n return inverseEdits;\n}\n\nfunction mapOffsetAfterForwardBatchToBefore(\n offsetAfter: number,\n forwardEdits: readonly ResolvedTextEdit[]\n): number {\n let offset = offsetAfter;\n for (const edit of forwardEdits) {\n const oldLength = edit.end - edit.start;\n const newLength = edit.text.length;\n const delta = newLength - oldLength;\n if (offset < edit.start) {\n continue;\n }\n if (offset >= edit.start + newLength) {\n offset -= delta;\n continue;\n }\n offset = edit.start + Math.min(offset - edit.start, oldLength);\n }\n return offset;\n}\n"],"mappings":"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"}
|