UNPKG

@pierre/diffs

Version:

1,209 lines (1,208 loc) 49.9 kB
import { dequeueRender, queueRender } from "./UniversalRenderingManager.js"; import { areSelectionsEqual } from "../utils/areSelectionsEqual.js"; import { areSelectionPointsEqual } from "../utils/areSelectionPointsEqual.js"; import { createGutterUtilityElement } from "../utils/createGutterUtilityElement.js"; import { isGutterUtilityPath } from "../utils/isGutterUtilityPath.js"; import { toHtml } from "hast-util-to-html"; //#region src/managers/InteractionManager.ts var InteractionManager = class { mode; options; hoveredLine; hoveredToken; pre; gutterUtilityLine; gutterUtilityContainer; gutterUtilityButton; gutterUtilitySlot; interactiveLinesAttr = false; interactiveLineNumbersAttr = false; hasPointerListeners = false; hasDocumentPointerListeners = false; selectedRange = null; selectedRangeHighlightSide; selectedRangeLineNumberOnly = false; editorActiveLine = null; editorActiveLineSide; editorLineNumberOnly = false; proposedSelectedRange; renderedSelectedLinesState; renderedEditorActiveLineState; selectionAnchor; pointerSession = { mode: "idle" }; constructor(mode, options) { this.mode = mode; this.options = options; } setOptions(options) { this.options = options; } cleanUp() { this.pre?.removeEventListener("click", this.handlePointerClick); this.pre?.removeEventListener("pointerdown", this.handlePointerDown); this.pre?.removeEventListener("pointermove", this.handlePointerMove); this.pre?.removeEventListener("pointerleave", this.handlePointerLeave); this.pre?.removeAttribute("data-interactive-lines"); this.pre?.removeAttribute("data-interactive-line-numbers"); this.pre = void 0; this.gutterUtilityContainer?.remove(); this.gutterUtilityLine = void 0; this.gutterUtilityContainer = void 0; this.gutterUtilityButton = void 0; this.gutterUtilitySlot = void 0; this.clearHoveredLine(); this.clearHoveredToken(); this.detachDocumentPointerListeners(); this.clearPointerSession(); dequeueRender(this.renderSelection); this.interactiveLinesAttr = false; this.interactiveLineNumbersAttr = false; this.hasPointerListeners = false; this.setSelectionDirty(); } setup(pre) { this.setSelectionDirty(); const { usesCustomGutterUtility = false, enableGutterUtility = false } = this.options; if (this.pre !== pre) { this.cleanUp(); this.pre = pre; } if (enableGutterUtility) this.ensureGutterUtilityNode(usesCustomGutterUtility); else if (this.gutterUtilityContainer != null) { this.gutterUtilityContainer.remove(); this.gutterUtilityLine = void 0; this.gutterUtilityContainer = void 0; this.gutterUtilityButton = void 0; this.gutterUtilitySlot = void 0; if (this.pointerSession.mode === "gutterSelecting") { this.clearPointerSession(); this.detachDocumentPointerListeners(); } } this.syncPointerListeners(pre); this.updateInteractiveLineAttributes(); this.renderSelection(); this.placeUtility(); } setSelectionDirty() { this.renderedSelectedLinesState = void 0; this.renderedEditorActiveLineState = void 0; } isSelectionDirty() { return this.renderedSelectedLinesState === void 0 || this.renderedEditorActiveLineState === void 0; } setSelection(range, options) { const isRangeChange = !(range === this.selectedRange || areSelectionsEqual(range ?? void 0, this.selectedRange ?? void 0)); const lineNumberOnly = options?.lineNumberOnly ?? false; const isStyleChange = options?.activeLineSide !== this.selectedRangeHighlightSide || lineNumberOnly !== this.selectedRangeLineNumberOnly; if (!this.isSelectionDirty() && !isRangeChange && !isStyleChange) return; this.proposedSelectedRange = void 0; this.selectedRange = range; this.selectedRangeHighlightSide = options?.activeLineSide; this.selectedRangeLineNumberOnly = lineNumberOnly; this.renderSelection(); this.placeUtility(); if (isRangeChange && options?.notify !== false) this.notifySelectionCommitted(range); } setEditorActiveLine(lineNumber, { lineNumberOnly = false, side } = {}) { const isLineChange = lineNumber !== this.editorActiveLine; const isStyleChange = side !== this.editorActiveLineSide || lineNumberOnly !== this.editorLineNumberOnly; if (!this.isSelectionDirty() && !isLineChange && !isStyleChange) return; this.editorActiveLine = lineNumber; this.editorActiveLineSide = side; this.editorLineNumberOnly = lineNumberOnly; this.renderSelection(); } getSelection() { return this.selectedRange; } getHoveredLine = () => { const gutterUtilityLine = this.gutterUtilityLine ?? this.hoveredLine; if (gutterUtilityLine != null) { if (this.mode === "diff" && gutterUtilityLine.type === "diff-line") return { lineNumber: gutterUtilityLine.lineNumber, side: gutterUtilityLine.annotationSide }; if (this.mode === "file" && gutterUtilityLine.type === "line") return { lineNumber: gutterUtilityLine.lineNumber }; } }; handlePointerClick = (event) => { const { onHunkExpand, onLineClick, onLineNumberClick, onTokenClick, onMergeConflictActionClick } = this.options; if (onHunkExpand == null && onLineClick == null && onLineNumberClick == null && onMergeConflictActionClick == null && onTokenClick == null) return; if (this.options.onGutterUtilityClick != null && isGutterUtilityPath(event.composedPath())) return; debugLogIfEnabled(this.options.__debugPointerEvents, "click", "FileDiff.DEBUG.handlePointerClick:", event); this.handlePointerEvent({ eventType: "click", event }); }; handlePointerMove = (event) => { if (event.pointerType !== "mouse") return; const { lineHoverHighlight = "disabled", onLineEnter, onLineLeave, onTokenEnter, onTokenLeave, enableGutterUtility = false } = this.options; if (lineHoverHighlight === "disabled" && !enableGutterUtility && onLineEnter == null && onLineLeave == null && onTokenEnter == null && onTokenLeave == null) return; debugLogIfEnabled(this.options.__debugPointerEvents, "move", "FileDiff.DEBUG.handlePointerMove:", event); this.handlePointerEvent({ eventType: "move", event }); }; handlePointerLeave = (event) => { const { __debugPointerEvents } = this.options; debugLogIfEnabled(__debugPointerEvents, "move", "FileDiff.DEBUG.handlePointerLeave: no event"); if (this.hoveredLine == null && this.hoveredToken == null) { debugLogIfEnabled(__debugPointerEvents, "move", "FileDiff.DEBUG.handlePointerLeave: returned early, no hovered line or token"); return; } if (this.hoveredToken != null) { this.options.onTokenLeave?.(this.hoveredToken, event); this.clearHoveredToken(); } if (this.hoveredLine != null) { this.options.onLineLeave?.({ ...this.hoveredLine, event }); this.clearHoveredLine(); } this.placeUtility(); }; handlePointerEvent({ eventType, event }) { const { __debugPointerEvents } = this.options; const composedPath = event.composedPath(); debugLogIfEnabled(__debugPointerEvents, eventType, "FileDiff.DEBUG.handlePointerEvent:", { eventType, composedPath }); const target = this.resolvePointerTarget(composedPath); debugLogIfEnabled(__debugPointerEvents, eventType, "FileDiff.DEBUG.handlePointerEvent: resolvePointerTarget result:", target); const { onLineClick, onLineNumberClick, onLineEnter, onLineLeave, onTokenClick, onTokenEnter, onTokenLeave, onHunkExpand, onMergeConflictActionClick } = this.options; switch (eventType) { case "move": { const sameLine = isHoverableLinePointerTarget(target) && this.hoveredLine?.lineElement === target.lineElement; if (!(isTokenPointerTarget(target) && this.hoveredToken?.tokenElement === target.tokenElement)) { if (this.hoveredToken != null) { onTokenLeave?.(this.hoveredToken, event); this.clearHoveredToken(); } if (isTokenPointerTarget(target)) { this.setHoveredToken(this.toTokenEventBaseProps(target)); onTokenEnter?.(this.hoveredToken, event); } } if (!sameLine) { if (this.hoveredLine != null) { onLineLeave?.({ ...this.hoveredLine, event }); this.clearHoveredLine(); } if (isHoverableLinePointerTarget(target)) { this.setHoveredLine(this.toEventBaseProps(target)); this.placeUtility(); onLineEnter?.({ ...this.hoveredLine, event }); } else this.placeUtility(); } break; } case "click": { if (target == null) break; if (isMergeConflictActionPointerTarget(target) && onMergeConflictActionClick != null) { onMergeConflictActionClick(target); break; } if (isExpandoPointerTarget(target) && onHunkExpand != null) { onHunkExpand(target.hunkIndex, target.all || event.shiftKey ? "both" : target.direction, target.all || event.shiftKey ? Number.POSITIVE_INFINITY : void 0); break; } if (!isHoverableLinePointerTarget(target)) break; if (isTokenPointerTarget(target) && onTokenClick != null) onTokenClick(this.toTokenEventBaseProps(target), event); const eventBase = this.toEventBaseProps(target); if (onLineNumberClick != null && target.numberColumn) onLineNumberClick({ ...eventBase, event }); else if (onLineClick != null) onLineClick({ ...eventBase, event }); break; } } } syncPointerListeners(pre) { const { __debugPointerEvents, lineHoverHighlight = "disabled", onLineClick, onLineNumberClick, onLineEnter, onLineLeave, onTokenClick, onTokenEnter, onTokenLeave, onHunkExpand, onMergeConflictActionClick, enableGutterUtility = false, enableLineSelection = false, onGutterUtilityClick } = this.options; const enableGutterSelection = onGutterUtilityClick != null; const shouldAttachPointerListeners = lineHoverHighlight !== "disabled" || onLineClick != null || onLineNumberClick != null || onLineEnter != null || onLineLeave != null || onTokenClick != null || onTokenEnter != null || onTokenLeave != null || onHunkExpand != null || onMergeConflictActionClick != null || enableGutterUtility || enableLineSelection || enableGutterSelection; if (shouldAttachPointerListeners && !this.hasPointerListeners) { pre.addEventListener("click", this.handlePointerClick); pre.addEventListener("pointerdown", this.handlePointerDown); pre.addEventListener("pointermove", this.handlePointerMove); pre.addEventListener("pointerleave", this.handlePointerLeave); this.hasPointerListeners = true; debugLogIfEnabled(__debugPointerEvents, "click", "FileDiff.DEBUG.attachEventListeners: Attaching click events for:", (() => { const reasons = []; if (__debugPointerEvents === "both" || __debugPointerEvents === "click") { if (onLineClick != null) reasons.push("onLineClick"); if (onLineNumberClick != null) reasons.push("onLineNumberClick"); if (onHunkExpand != null) reasons.push("expandable hunk separators"); if (onMergeConflictActionClick != null) reasons.push("merge conflict actions"); } return reasons; })()); debugLogIfEnabled(__debugPointerEvents, "move", "FileDiff.DEBUG.attachEventListeners: Attaching pointer move event"); debugLogIfEnabled(__debugPointerEvents, "move", "FileDiff.DEBUG.attachEventListeners: Attaching pointer leave event"); } else if (!shouldAttachPointerListeners && this.hasPointerListeners) { pre.removeEventListener("click", this.handlePointerClick); pre.removeEventListener("pointerdown", this.handlePointerDown); pre.removeEventListener("pointermove", this.handlePointerMove); pre.removeEventListener("pointerleave", this.handlePointerLeave); this.hasPointerListeners = false; } const hasActiveLineSelectionSession = this.pointerSession.mode === "selecting" || this.pointerSession.mode === "pendingSingleLineUnselect"; const hasActiveGutterSelectionSession = this.pointerSession.mode === "gutterSelecting"; if (!enableLineSelection && hasActiveLineSelectionSession || !enableGutterSelection && hasActiveGutterSelectionSession) { this.clearPointerSession(); this.detachDocumentPointerListeners(); this.selectionAnchor = void 0; this.clearPendingSingleLineState(); } } updateInteractiveLineAttributes() { if (this.pre == null) return; const { onLineClick, onLineNumberClick, enableLineSelection = false } = this.options; const shouldHaveInteractiveLines = onLineClick != null; const shouldHaveInteractiveLineNumbers = onLineNumberClick != null || enableLineSelection; if (shouldHaveInteractiveLines && !this.interactiveLinesAttr) { this.pre.setAttribute("data-interactive-lines", ""); this.interactiveLinesAttr = true; } else if (!shouldHaveInteractiveLines && this.interactiveLinesAttr) { this.pre.removeAttribute("data-interactive-lines"); this.interactiveLinesAttr = false; } if (shouldHaveInteractiveLineNumbers && !this.interactiveLineNumbersAttr) { this.pre.setAttribute("data-interactive-line-numbers", ""); this.interactiveLineNumbersAttr = true; } else if (!shouldHaveInteractiveLineNumbers && this.interactiveLineNumbersAttr) { this.pre.removeAttribute("data-interactive-line-numbers"); this.interactiveLineNumbersAttr = false; } } handlePointerDown = (event) => { if (event.pointerType === "mouse" && event.button !== 0 || this.pre == null || this.pointerSession.mode !== "idle") return; const path = event.composedPath(); if (isGutterUtilityPath(path) && this.options.onGutterUtilityClick != null) this.startGutterSelectionFromPointerDown(event); else { if (event.pointerType !== "mouse") this.revealUtilityFromGutterPath(path); this.startLineSelectionFromPointerDown(event); } }; startLineSelectionFromPointerDown(event) { const { enableLineSelection = false } = this.options; if (!enableLineSelection) return; const pointerInfo = this.resolveSelectionInfo(event, { source: "event-path", requireNumberColumn: true }); if (pointerInfo == null) return; const { pre } = this; if (pre == null) return; const { lineNumber, eventSide, lineIndex } = pointerInfo; if (event.shiftKey && this.selectedRange != null) { const rowRange = this.getIndexesFromSelection(this.selectedRange, pre.getAttribute("data-diff-type") === "split"); if (rowRange == null) return; const useStart = rowRange.start <= rowRange.end ? lineIndex >= rowRange.start : lineIndex <= rowRange.end; this.selectionAnchor = { lineNumber: useStart ? this.selectedRange.start : this.selectedRange.end, side: useStart ? this.selectedRange.side : this.selectedRange.endSide ?? this.selectedRange.side }; this.updateSelection(lineNumber, eventSide, false); this.notifySelectionStart(this.getCurrentSelectionRange()); this.pointerSession = { mode: "selecting", pointerId: event.pointerId }; this.attachDocumentPointerListeners(); return; } if (this.selectedRange?.start === lineNumber && this.selectedRange?.end === lineNumber) { const point = { lineNumber, side: eventSide }; this.selectionAnchor = point; this.pointerSession = { mode: "pendingSingleLineUnselect", pointerId: event.pointerId, anchor: point, pending: point }; this.attachDocumentPointerListeners(); return; } if (this.options.controlledSelection === true) this.proposedSelectedRange = null; else this.selectedRange = null; this.placeUtility(); this.selectionAnchor = { lineNumber, side: eventSide }; this.updateSelection(lineNumber, eventSide, false); this.notifySelectionStart(this.getCurrentSelectionRange()); this.pointerSession = { mode: "selecting", pointerId: event.pointerId }; this.attachDocumentPointerListeners(); } startGutterSelectionFromPointerDown(event) { const { onGutterUtilityClick } = this.options; if (onGutterUtilityClick == null) return; const selectedEndpoints = this.currentSelectionEnds(); const point = selectedEndpoints?.bottom ?? this.resolveSelectionPoint(event, { source: "event-path", excludeUtility: false }); const anchor = selectedEndpoints?.top ?? point; if (point == null || anchor == null) return; event.preventDefault(); event.stopPropagation(); this.pointerSession = { mode: "gutterSelecting", pointerId: event.pointerId, anchor, current: point }; this.selectionAnchor = { lineNumber: anchor.lineNumber, side: anchor.side }; this.updateSelection(point.lineNumber, point.side, false); this.notifySelectionStart(this.getCurrentSelectionRange()); this.attachDocumentPointerListeners(); } handleDocumentPointerMove = (event) => { switch (this.pointerSession.mode) { case "idle": return; case "gutterSelecting": { if (event.pointerId !== this.pointerSession.pointerId) return; event.preventDefault(); const point = this.resolveSelectionPoint(event, { source: "coordinates-first" }); if (point == null) return; this.pointerSession.current = point; this.updateSelection(point.lineNumber, point.side); return; } case "selecting": { if (event.pointerId !== this.pointerSession.pointerId) return; event.preventDefault(); const pointerInfo = this.resolveSelectionInfo(event, { source: "coordinates-first", requireNumberColumn: false }); if (pointerInfo == null || this.selectionAnchor == null) return; this.updateSelection(pointerInfo.lineNumber, pointerInfo.eventSide); return; } case "pendingSingleLineUnselect": { if (event.pointerId !== this.pointerSession.pointerId) return; event.preventDefault(); const pointerInfo = this.resolveSelectionInfo(event, { source: "coordinates-first", requireNumberColumn: false }); if (pointerInfo == null || this.selectionAnchor == null) return; const point = { lineNumber: pointerInfo.lineNumber, side: pointerInfo.eventSide }; if (areSelectionPointsEqual(this.pointerSession.pending, point)) return; this.updateSelection(pointerInfo.lineNumber, pointerInfo.eventSide, false); this.notifySelectionStart(this.getCurrentSelectionRange()); this.notifySelectionChangeDelta(); this.pointerSession = { mode: "selecting", pointerId: event.pointerId }; return; } } }; handleDocumentPointerUp = (event) => { const { onGutterUtilityClick } = this.options; switch (this.pointerSession.mode) { case "idle": return; case "gutterSelecting": { const { pointerSession: session } = this; if (event.pointerId !== session.pointerId) return; event.preventDefault(); const point = this.resolveSelectionPoint(event, { source: "coordinates-first" }); if (point != null) { session.current = point; this.updateSelection(point.lineNumber, point.side); } const completedRange = this.buildSelectedLineRange(session.anchor, session.current); onGutterUtilityClick?.({ ...completedRange }); this.selectionAnchor = void 0; this.notifySelectionEnd(completedRange); this.notifySelectionCommitted(completedRange); this.clearProposedSelection(); this.clearPointerSession(); this.detachDocumentPointerListeners(); return; } case "pendingSingleLineUnselect": if (event.pointerId !== this.pointerSession.pointerId) return; event.preventDefault(); this.updateSelection(null, void 0, false); this.selectionAnchor = void 0; this.clearPendingSingleLineState(); this.detachDocumentPointerListeners(); this.notifySelectionEnd(this.getCurrentSelectionRange()); this.notifySelectionCommitted(this.getCurrentSelectionRange()); this.clearProposedSelection(); return; case "selecting": if (event.pointerId !== this.pointerSession.pointerId) return; event.preventDefault(); this.selectionAnchor = void 0; this.detachDocumentPointerListeners(); this.clearPointerSession(); this.notifySelectionEnd(this.getCurrentSelectionRange()); this.notifySelectionCommitted(this.getCurrentSelectionRange()); this.clearProposedSelection(); } }; handleDocumentPointerCancel = (event) => { switch (this.pointerSession.mode) { case "idle": return; case "gutterSelecting": case "selecting": case "pendingSingleLineUnselect": if ("pointerId" in this.pointerSession) { if (event.pointerId !== this.pointerSession.pointerId) return; } this.selectionAnchor = void 0; this.clearProposedSelection(); this.clearPendingSingleLineState(); this.clearPointerSession(); this.detachDocumentPointerListeners(); } }; clearHoveredLine() { if (this.hoveredLine == null) return; this.hoveredLine.lineElement.removeAttribute("data-hovered"); this.hoveredLine.numberElement.removeAttribute("data-hovered"); this.hoveredLine = void 0; } setHoveredLine(hoveredLine) { const { lineHoverHighlight = "disabled" } = this.options; if (this.hoveredLine != null) this.clearHoveredLine(); this.hoveredLine = hoveredLine; if (lineHoverHighlight !== "disabled") { if (lineHoverHighlight === "both" || lineHoverHighlight === "line") this.hoveredLine.lineElement.setAttribute("data-hovered", ""); if (lineHoverHighlight === "both" || lineHoverHighlight === "number") this.hoveredLine.numberElement.setAttribute("data-hovered", ""); } } clearHoveredToken() { if (this.hoveredToken == null) return; this.hoveredToken = void 0; } setHoveredToken(hoveredToken) { if (this.hoveredToken != null) this.clearHoveredToken(); this.hoveredToken = hoveredToken; } ensureGutterUtilityNode(useCustomGutterUtility) { if (this.gutterUtilityContainer == null) { this.gutterUtilityContainer = document.createElement("div"); this.gutterUtilityContainer.setAttribute("data-gutter-utility-slot", ""); } if (useCustomGutterUtility) { if (this.gutterUtilityButton != null) { this.gutterUtilityButton.remove(); this.gutterUtilityButton = void 0; } if (this.gutterUtilitySlot == null) { this.gutterUtilitySlot = document.createElement("slot"); this.gutterUtilitySlot.name = "gutter-utility-slot"; } if (this.gutterUtilitySlot.parentNode !== this.gutterUtilityContainer) this.gutterUtilityContainer.replaceChildren(this.gutterUtilitySlot); } else { this.gutterUtilitySlot?.remove(); this.gutterUtilitySlot = void 0; if (this.gutterUtilityButton == null) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = toHtml(createGutterUtilityElement()); const utilityButton = tempDiv.firstElementChild; if (!(utilityButton instanceof HTMLButtonElement)) throw new Error("InteractionManager.ensureGutterUtilityNode: Node element should be a button"); utilityButton.remove(); this.gutterUtilityButton = utilityButton; } if (this.gutterUtilityButton.parentNode !== this.gutterUtilityContainer) this.gutterUtilityContainer.replaceChildren(this.gutterUtilityButton); } } revealUtilityFromGutterPath(path) { if (this.placeUtilityFromSelection()) return; const target = this.resolvePointerTarget(path); if (isLinePointerTarget(target) && target.numberColumn) this.showUtilityOnLine(this.toEventBaseProps(target)); } placeUtility() { if (this.placeUtilityFromSelection()) return; if (this.hoveredLine != null) { this.showUtilityOnLine(this.hoveredLine); return; } this.hideUtility(); } placeUtilityFromSelection() { const endpoints = this.currentSelectionEnds(); if (endpoints == null) return false; const target = this.targetForSelectionPoint(endpoints.bottom); if (target == null) this.hideUtility(); else this.showUtilityOnLine(this.toEventBaseProps(target)); return true; } showUtilityOnLine(line) { if (this.gutterUtilityContainer == null) return; this.gutterUtilityLine = line; line.numberElement.appendChild(this.gutterUtilityContainer); } hideUtility() { this.gutterUtilityContainer?.remove(); this.gutterUtilityLine = void 0; } currentSelectionEnds() { const range = this.getCurrentSelectionRange(); return range == null ? void 0 : this.selectionEnds(range); } selectionEnds(range) { const start = { lineNumber: range.start, side: range.side }; const end = { lineNumber: range.end, side: range.endSide ?? range.side }; const startIndex = this.selectionPointRowIndex(start); const endIndex = this.selectionPointRowIndex(end); if (startIndex == null || endIndex == null) return; return startIndex > endIndex ? { top: end, bottom: start } : { top: start, bottom: end }; } selectionPointRowIndex(point) { const indexes = this.getLineIndex(point.lineNumber, point.side); if (indexes == null) return; return this.isSplitDiff() ? indexes[1] : indexes[0]; } targetForSelectionPoint(point) { if (this.pre == null) return; const indexes = this.getLineIndex(point.lineNumber, point.side); if (indexes == null) return; const lineIndex = this.mode === "diff" ? `${indexes[0]},${indexes[1]}` : `${indexes[0]}`; const candidates = this.pre.querySelectorAll(`[data-column-number="${point.lineNumber}"][data-line-index="${lineIndex}"]`); for (const element of candidates) { if (!(element instanceof HTMLElement)) continue; const target = this.resolvePointerTarget(getElementPath(element)); if (!isLinePointerTarget(target)) continue; if (this.mode === "diff" && point.side != null && target.side !== point.side) continue; return target; } } attachDocumentPointerListeners() { if (this.hasDocumentPointerListeners) return; document.addEventListener("pointermove", this.handleDocumentPointerMove); document.addEventListener("pointerup", this.handleDocumentPointerUp); document.addEventListener("pointercancel", this.handleDocumentPointerCancel); this.hasDocumentPointerListeners = true; } detachDocumentPointerListeners() { if (!this.hasDocumentPointerListeners) return; document.removeEventListener("pointermove", this.handleDocumentPointerMove); document.removeEventListener("pointerup", this.handleDocumentPointerUp); document.removeEventListener("pointercancel", this.handleDocumentPointerCancel); this.hasDocumentPointerListeners = false; } clearPointerSession() { this.pointerSession = { mode: "idle" }; } clearPendingSingleLineState() { if (this.pointerSession.mode === "pendingSingleLineUnselect") this.pointerSession = { mode: "idle" }; } selectionInfoFromPath(path, requireNumberColumn) { const target = this.resolvePointerTarget(path); if (!isLinePointerTarget(target)) return; if (requireNumberColumn && !target.numberColumn) return; if (target.splitLineIndex == null) return; return { lineIndex: target.splitLineIndex, lineNumber: target.lineNumber, eventSide: this.mode === "diff" ? target.side : void 0 }; } resolveSelectionInfo(event, options) { const path = this.resolveSelectionPath(event, options); return path != null ? this.selectionInfoFromPath(path, options.requireNumberColumn) : void 0; } selectionPointFromPath(path) { const target = this.resolvePointerTarget(path); if (!isLinePointerTarget(target)) return; return { lineNumber: target.lineNumber, side: this.mode === "diff" ? target.side : void 0 }; } resolveSelectionPoint(event, options) { const path = this.resolveSelectionPath(event, options); return path != null ? this.selectionPointFromPath(path) : void 0; } resolveSelectionPath(event, options) { const excludeUtility = options.excludeUtility !== false; switch (options.source) { case "event-path": return this.pathFromEventPath(event.composedPath(), excludeUtility); case "coordinates-first": { const coordinatePath = this.pathFromCoordinates(event, excludeUtility); if (coordinatePath !== void 0) return coordinatePath ?? void 0; return this.pathFromEventPath(event.composedPath(), excludeUtility); } } } pathFromCoordinates(event, excludeUtility) { const coordinateTarget = this.hitTest(event); if (coordinateTarget === void 0) return; if (coordinateTarget === null) return null; return this.pathFromElement(coordinateTarget, excludeUtility) ?? null; } pathFromEventPath(path, excludeUtility) { if (excludeUtility && isGutterUtilityPath(path)) return; for (const element of path) if (element instanceof Element) return this.pathFromElement(element, excludeUtility); } pathFromElement(element, excludeUtility) { const path = getElementPath(element); if (excludeUtility && isGutterUtilityPath(path)) return; const row = closestSelectableRow(element); if (row != null) return getElementPath(row); return this.pathFromAnnotationSlot(element); } pathFromAnnotationSlot(element) { const annotatedLine = annotationRowFromSlot(element)?.previousElementSibling; if (annotatedLine instanceof HTMLElement && (annotatedLine.hasAttribute("data-line") || annotatedLine.hasAttribute("data-column-number"))) return getElementPath(annotatedLine); const point = selectionPointFromAnnotationSlotName(getAnnotationSlotName(element)); if (point == null) return; const target = this.targetForSelectionPoint(point); return target != null ? getElementPath(target.lineElement) : void 0; } hitTest(event) { if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return; const root = this.pre?.getRootNode(); const elementFromPointRoot = hasElementFromPoint(root) ? root : hasElementFromPoint(document) ? document : void 0; if (elementFromPointRoot == null) return; return elementFromPointRoot.elementFromPoint(event.clientX, event.clientY); } getLineIndex(lineNumber, side) { const { getLineIndex } = this.options; return getLineIndex != null ? getLineIndex(lineNumber, side) : [lineNumber - 1, lineNumber - 1]; } getCurrentSelectionRange() { return this.proposedSelectedRange !== void 0 ? this.proposedSelectedRange : this.selectedRange; } clearProposedSelection() { this.proposedSelectedRange = void 0; } updateSelection(currentLine, side, emitChange = true) { const previousRange = this.getCurrentSelectionRange(); let nextRange; if (currentLine == null) nextRange = null; else { const anchorSide = this.selectionAnchor?.side ?? side; const anchorLine = this.selectionAnchor?.lineNumber ?? currentLine; nextRange = this.buildSelectionRange(anchorLine, currentLine, anchorSide, side); } if (areSelectionsEqual(previousRange ?? void 0, nextRange ?? void 0)) return; this.selectedRangeHighlightSide = void 0; this.selectedRangeLineNumberOnly = false; if (this.options.controlledSelection === true) this.proposedSelectedRange = nextRange; else { this.selectedRange = nextRange; queueRender(this.renderSelection); } this.placeUtility(); if (emitChange) this.notifySelectionChangeDelta(); } getIndexesFromSelection(selectedRange, split) { if (this.pre == null) return; const startIndexes = this.getLineIndex(selectedRange.start, selectedRange.side); const finalIndexes = this.getLineIndex(selectedRange.end, selectedRange.endSide ?? selectedRange.side); return startIndexes != null && finalIndexes != null ? { start: split ? startIndexes[1] : startIndexes[0], end: split ? finalIndexes[1] : finalIndexes[0] } : void 0; } getSelectionRenderState() { return { selectedLines: { range: this.selectedRange, highlightSide: this.selectedRangeHighlightSide, lineNumberOnly: this.selectedRangeLineNumberOnly }, editorActiveLine: { range: this.editorActiveLine == null ? null : { start: this.editorActiveLine, end: this.editorActiveLine, side: this.editorActiveLineSide }, highlightSide: this.editorActiveLineSide, lineNumberOnly: this.editorLineNumberOnly } }; } resolveLineRenderRange(state, split, source) { if (state.range == null) return; const rowRange = this.getIndexesFromSelection(state.range, split); if (rowRange == null) { console.error({ rowRange, range: state.range }); throw new Error(`InteractionManager.renderSelection: No valid ${source} rowRange`); } return rowRange; } getLineRenderColumns(selectedLines, selectedRowRange, editorActiveLine, editorRowRange) { if (this.pre == null || selectedRowRange == null && editorRowRange == null) return []; const { children: codeElements } = this.pre; if (codeElements.length > 2) { console.error(codeElements); throw new Error("InteractionManager.renderSelection: Somehow there are more than 2 code elements..."); } const columns = []; for (const code of codeElements) { const side = code.hasAttribute("data-deletions") ? "deletions" : code.hasAttribute("data-additions") ? "additions" : void 0; const renderSelectedLines = selectedLines != null && selectedRowRange != null && (selectedLines.highlightSide == null || side == null || selectedLines.highlightSide === side); const renderEditorActiveLine = editorActiveLine != null && editorRowRange != null && (editorActiveLine.highlightSide == null || side == null || editorActiveLine.highlightSide === side); if (!renderSelectedLines && !renderEditorActiveLine) continue; const [gutter, content] = code.children; if (!(gutter instanceof HTMLElement) || !(content instanceof HTMLElement)) throw new Error("InteractionManager.renderSelection: missing gutter or content element"); if (content.children.length !== gutter.children.length) throw new Error("InteractionManager.renderSelection: gutter and content children dont match, something is wrong"); columns.push({ content, gutter, renderEditorActiveLine, renderSelectedLines }); } return columns; } renderSelection = () => { dequeueRender(this.renderSelection); const renderState = this.getSelectionRenderState(); if (this.pre == null) return; const { editorActiveLine, selectedLines } = renderState; const selectedLinesChanged = !areLineRenderStatesEqual(this.renderedSelectedLinesState, selectedLines); const editorActiveLineChanged = !areLineRenderStatesEqual(this.renderedEditorActiveLineState, editorActiveLine); if (!selectedLinesChanged && !editorActiveLineChanged) return; if (selectedLinesChanged) this.renderedSelectedLinesState = void 0; if (editorActiveLineChanged) this.renderedEditorActiveLineState = void 0; const split = this.pre.getAttribute("data-diff-type") === "split"; const selectedRowRange = !selectedLinesChanged ? void 0 : this.resolveLineRenderRange(selectedLines, split, "selected-lines"); const editorRowRange = !editorActiveLineChanged ? void 0 : this.resolveLineRenderRange(editorActiveLine, split, "editor-active-line"); const columns = this.getLineRenderColumns(selectedLinesChanged ? selectedLines : void 0, selectedRowRange, editorActiveLineChanged ? editorActiveLine : void 0, editorRowRange); const selectedLinesComplete = selectedLines.range == null || columns.some((column) => column.renderSelectedLines); const editorActiveLineComplete = editorActiveLine.range == null || columns.some((column) => column.renderEditorActiveLine); if (selectedLinesChanged) for (const element of this.pre.querySelectorAll("[data-selected-line]")) element.removeAttribute("data-selected-line"); if (editorActiveLineChanged) for (const element of this.pre.querySelectorAll("[data-editor-active-line]")) element.removeAttribute("data-editor-active-line"); const selectedFirst = selectedRowRange == null ? void 0 : Math.min(selectedRowRange.start, selectedRowRange.end); const selectedLast = selectedRowRange == null ? void 0 : Math.max(selectedRowRange.start, selectedRowRange.end); const selectedIsSingle = selectedFirst === selectedLast; const editorLineIndex = editorRowRange?.start; for (const column of columns) { const { content, gutter, renderEditorActiveLine, renderSelectedLines } = column; const finalLineIndex = Math.max(renderSelectedLines ? selectedLast ?? -Infinity : -Infinity, renderEditorActiveLine ? editorLineIndex ?? -Infinity : -Infinity); const len = content.children.length; for (let i = 0; i < len; i++) { const contentElement = content.children[i]; const gutterElement = gutter.children[i]; if (!(contentElement instanceof HTMLElement) || !(gutterElement instanceof HTMLElement)) continue; const lineIndex = this.parseLineIndex(contentElement, split); if ((lineIndex ?? 0) > finalLineIndex) break; if (lineIndex == null) continue; if (renderSelectedLines && selectedFirst != null && selectedLast != null && lineIndex >= selectedFirst && lineIndex <= selectedLast) { let attributeValue = selectedIsSingle ? "single" : lineIndex === selectedFirst ? "first" : lineIndex === selectedLast ? "last" : ""; gutterElement.setAttribute("data-selected-line", attributeValue); if (!selectedLines.lineNumberOnly) { contentElement.setAttribute("data-selected-line", attributeValue); if (gutterElement.nextSibling instanceof HTMLElement && contentElement.nextSibling instanceof HTMLElement && (contentElement.nextSibling.hasAttribute("data-line-annotation") || contentElement.nextSibling.hasAttribute("data-merge-conflict-actions"))) { if (selectedIsSingle) { attributeValue = "last"; contentElement.setAttribute("data-selected-line", "first"); } else if (lineIndex === selectedFirst) attributeValue = ""; else if (lineIndex === selectedLast) contentElement.setAttribute("data-selected-line", ""); contentElement.nextSibling.setAttribute("data-selected-line", attributeValue); gutterElement.nextSibling.setAttribute("data-selected-line", attributeValue); } } } if (renderEditorActiveLine && lineIndex === editorLineIndex) { gutterElement.setAttribute("data-editor-active-line", ""); if (!editorActiveLine.lineNumberOnly) contentElement.setAttribute("data-editor-active-line", ""); } } } if (selectedLinesChanged && selectedLinesComplete) this.renderedSelectedLinesState = selectedLines; if (editorActiveLineChanged && editorActiveLineComplete) this.renderedEditorActiveLineState = editorActiveLine; }; notifySelectionCommitted(range) { this.options.onLineSelected?.(range); } notifySelectionChangeDelta() { this.options.onLineSelectionChange?.(this.getCurrentSelectionRange() ?? null); } notifySelectionStart(range) { this.options.onLineSelectionStart?.(range); } notifySelectionEnd(range) { this.options.onLineSelectionEnd?.(range); } toEventBaseProps(target) { if (this.mode === "file") return { type: "line", lineElement: target.lineElement, lineNumber: target.lineNumber, numberColumn: target.numberColumn, numberElement: target.numberElement }; return { type: "diff-line", annotationSide: target.side, lineType: target.lineType, lineElement: target.lineElement, numberElement: target.numberElement, lineNumber: target.lineNumber, numberColumn: target.numberColumn }; } toTokenEventBaseProps({ lineCharEnd, lineCharStart, lineNumber, side, tokenElement, tokenText }) { if (this.mode === "file") return { type: "token", lineCharEnd, lineCharStart, lineNumber, tokenElement, tokenText }; return { type: "token", lineCharEnd, lineCharStart, lineNumber, side, tokenElement, tokenText }; } buildSelectedLineRange(anchor, current) { return this.buildSelectionRange(anchor.lineNumber, current.lineNumber, anchor.side, current.side); } buildSelectionRange(start, end, side, endSide) { return { start, end, ...side != null ? { side } : {}, ...side !== endSide && endSide != null ? { endSide } : {} }; } resolvePointerTarget(path) { let numberColumn = false; let lineType; let codeElement; let lineElement; let lineIndexValue; let numberElement; let tokenElement; let tokenInfo; let expandInfo; let lineNumber; let mergeConflictActionTarget; for (const element of path) { if (!(element instanceof HTMLElement)) continue; if (mergeConflictActionTarget == null && element.hasAttribute("data-merge-conflict-action")) { const resolutionValue = element.getAttribute("data-merge-conflict-action") ?? void 0; const conflictIndexValue = element.getAttribute("data-merge-conflict-conflict-index") ?? void 0; const conflictIndex = conflictIndexValue != null ? Number.parseInt(conflictIndexValue, 10) : NaN; if (isMergeConflictResolution(resolutionValue) && Number.isFinite(conflictIndex)) mergeConflictActionTarget = { kind: "merge-conflict-action", resolution: resolutionValue, conflictIndex }; } if (tokenElement == null && element.hasAttribute("data-char")) { tokenElement = element; const startAttr = element.getAttribute("data-char"); if (startAttr != null) { const lineCharStart = Number.parseInt(startAttr, 10); if (!Number.isNaN(lineCharStart)) { const tokenText = element.textContent ?? ""; const lineCharEnd = lineCharStart + tokenText.length; if (tokenText.trim() !== "" || this.options.enableTokenInteractionsOnWhitespace === true) tokenInfo = { tokenElement, lineCharStart, lineCharEnd, tokenText }; continue; } } } const columnNumber = numberElement == null ? element.getAttribute("data-column-number") ?? void 0 : void 0; if (columnNumber != null) { numberElement = element; lineNumber = Number.parseInt(columnNumber, 10); numberColumn = true; lineType = getLineTypeFromElement(element); lineIndexValue = element.getAttribute("data-line-index") ?? void 0; continue; } const lineAttr = lineElement == null ? element.getAttribute("data-line") ?? void 0 : void 0; if (lineAttr != null) { lineElement = element; lineNumber = Number.parseInt(lineAttr, 10); lineType = getLineTypeFromElement(element); lineIndexValue = element.getAttribute("data-line-index") ?? void 0; continue; } if (expandInfo == null && (element.hasAttribute("data-expand-button") || element.hasAttribute("data-unmodified-lines"))) { expandInfo = { hunkIndex: void 0, direction: (() => { if (element.hasAttribute("data-expand-up")) return "up"; if (element.hasAttribute("data-expand-down")) return "down"; return "both"; })(), all: element.hasAttribute("data-expand-all-button") }; continue; } const expandIndexValue = expandInfo != null ? element.getAttribute("data-expand-index") ?? void 0 : void 0; if (expandInfo != null && expandIndexValue != null) { const expandIndex = Number.parseInt(expandIndexValue, 10); if (!Number.isNaN(expandIndex)) expandInfo.hunkIndex = expandIndex; continue; } if (codeElement == null && element.hasAttribute("data-code")) { codeElement = element; break; } } if (mergeConflictActionTarget != null) return mergeConflictActionTarget; if (expandInfo?.hunkIndex != null) return { type: "line-info", hunkIndex: expandInfo.hunkIndex, direction: expandInfo.direction, all: expandInfo.all }; lineElement ??= lineIndexValue != null ? queryHTMLElement(codeElement, `[data-line][data-line-index="${lineIndexValue}"]`) : void 0; numberElement ??= lineIndexValue != null ? queryHTMLElement(codeElement, `[data-column-number][data-line-index="${lineIndexValue}"]`) : void 0; if (codeElement == null || lineElement == null || numberElement == null || lineType == null || lineNumber == null || Number.isNaN(lineNumber)) return; const splitLineIndex = this.parseLineIndex(lineElement, this.isSplitDiff()); if (tokenInfo != null) { if (this.mode === "file") return { kind: "token", lineType, lineElement, lineNumber, numberColumn, numberElement, side: void 0, splitLineIndex, ...tokenInfo }; return { kind: "token", lineType, lineElement, lineNumber, numberColumn, numberElement, side: getAnnotationSide(lineType, codeElement), splitLineIndex, ...tokenInfo }; } if (this.mode === "file") return { kind: "line", lineType, lineElement, lineNumber, numberColumn, numberElement, side: void 0, splitLineIndex }; return { kind: "line", lineType, lineElement, lineNumber, numberColumn, numberElement, side: getAnnotationSide(lineType, codeElement), splitLineIndex }; } isSplitDiff() { return this.pre?.getAttribute("data-diff-type") === "split"; } parseLineIndex(element, split) { const lineIndexes = (element.getAttribute("data-line-index") ?? "").split(",").map((value) => Number.parseInt(value, 10)).filter((value) => !Number.isNaN(value)); if (split && lineIndexes.length === 2) return lineIndexes[1]; if (!split) return lineIndexes[0]; } }; function pluckInteractionOptions({ enableTokenInteractionsOnWhitespace, enableGutterUtility, lineHoverHighlight, onGutterUtilityClick, onLineClick, onLineEnter, onLineLeave, onLineNumberClick, onTokenClick, onTokenEnter, onTokenLeave, renderGutterUtility, __debugPointerEvents, enableLineSelection, controlledSelection, onLineSelected, onLineSelectionStart, onLineSelectionChange, onLineSelectionEnd }, onHunkExpand, getLineIndex, onMergeConflictActionClick) { return { enableTokenInteractionsOnWhitespace, enableGutterUtility: resolveEnableGutterUtilityOption({ enableGutterUtility, renderGutterUtility, onGutterUtilityClick }), usesCustomGutterUtility: renderGutterUtility != null, lineHoverHighlight, onGutterUtilityClick, onHunkExpand, onMergeConflictActionClick, onLineClick, onLineEnter, onLineLeave, onLineNumberClick, onTokenClick, onTokenEnter, onTokenLeave, __debugPointerEvents, enableLineSelection, controlledSelection, onLineSelected, onLineSelectionStart, onLineSelectionChange, onLineSelectionEnd, getLineIndex }; } function resolveEnableGutterUtilityOption({ enableGutterUtility, renderGutterUtility, onGutterUtilityClick }) { if (onGutterUtilityClick != null && renderGutterUtility != null) throw new Error("Cannot use both 'onGutterUtilityClick' and 'renderGutterUtility'. Use only one gutter utility API."); return enableGutterUtility ?? false; } function isLinePointerTarget(target) { return target != null && "kind" in target && target.kind === "line"; } function isTokenPointerTarget(target) { return target != null && "kind" in target && target.kind === "token"; } function isHoverableLinePointerTarget(target) { return isLinePointerTarget(target) || isTokenPointerTarget(target); } function isExpandoPointerTarget(target) { return "type" in target && target.type === "line-info"; } function isMergeConflictActionPointerTarget(target) { return "kind" in target && target.kind === "merge-conflict-action"; } function isMergeConflictResolution(value) { return value === "current" || value === "incoming" || value === "both"; } function queryHTMLElement(parent, query) { const element = parent?.querySelector(query); return element instanceof HTMLElement ? element : void 0; } function getElementPath(element) { const path = []; let current = element; while (current != null) { path.push(current); current = current.parentNode; } return path; } function closestSelectableRow(element) { const row = element.closest("[data-line], [data-column-number]"); if (row instanceof HTMLElement) return row; const annotationRow = element.closest("[data-line-annotation], [data-gutter-buffer=\"annotation\"]"); if (!(annotationRow instanceof HTMLElement)) return; const previousRow = annotationRow.previousElementSibling; return previousRow instanceof HTMLElement && (previousRow.hasAttribute("data-line") || previousRow.hasAttribute("data-column-number")) ? previousRow : void 0; } function annotationRowFromSlot(element) { const slotted = element.closest("[slot^=\"annotation-\"]"); const row = (slotted instanceof HTMLElement && slotted.assignedSlot != null ? slotted.assignedSlot : element.tagName === "SLOT" ? element : void 0)?.closest("[data-line-annotation]"); return row instanceof HTMLElement ? row : void 0; } function getAnnotationSlotName(element) { const slottedElement = element.closest("[slot^=\"annotation-\"]"); if (slottedElement instanceof HTMLElement) return slottedElement.getAttribute("slot") ?? void 0; if (element instanceof HTMLElement) { const slotName = element.getAttribute("name") ?? void 0; return slotName != null && slotName.startsWith("annotation-") ? slotName : void 0; } } function selectionPointFromAnnotationSlotName(slotName) { if (slotName == null) return; const match = /^annotation-(?:(additions|deletions)-)?(\d+)$/.exec(slotName); if (match == null) return; const lineNumber = Number.parseInt(match[2], 10); if (!Number.isFinite(lineNumber) || lineNumber <= 0) return; return { lineNumber, side: match[1] }; } function hasElementFromPoint(value) { return value != null && typeof value.elementFromPoint === "function"; } function areSelectionRangesEqual(first, second) { return first === second || areSelectionsEqual(first ?? void 0, second ?? void 0); } function areLineRenderStatesEqual(first, second) { return first?.highlightSide === second.highlightSide && first?.lineNumberOnly === second.lineNumberOnly && areSelectionRangesEqual(first?.range ?? null, second.range); } function getAnnotationSide(lineType, codeElement) { switch (lineType) { case "change-deletion": return "deletions"; case "change-addition": return "additions"; default: return codeElement.hasAttribute("data-deletions") ? "deletions" : "additions"; } } function getLineTypeFromElement(element) { const lineType = element.getAttribute("data-line-type"); if (lineType == null) return; switch (lineType) { case "change-deletion": case "change-addition": case "context": case "context-expanded": return lineType; default: return; } } function debugLogIfEnabled(debugLogType = "none", logIfType, ...args) { switch (debugLogType) { case "none": return; case "both": break; case "click": if (logIfType !== "click") return; break; case "move": if (logIfType !== "move") return; break; } console.log(...args); } //#endregion export { InteractionManager, pluckInteractionOptions }; //# sourceMappingURL=InteractionManager.js.map