@pierre/diffs
Version:
1,126 lines (1,124 loc) • 44.4 kB
JavaScript
import { areSelectionsEqual } from "../utils/areSelectionsEqual.js";
import { areSelectionPointsEqual } from "../utils/areSelectionPointsEqual.js";
import { createGutterUtilityElement } from "../utils/createGutterUtilityElement.js";
import { toHtml } from "hast-util-to-html";
//#region src/managers/InteractionManager.ts
var InteractionManager = class {
hoveredLine;
hoveredToken;
pre;
gutterUtilityLine;
gutterUtilityContainer;
gutterUtilityButton;
gutterUtilitySlot;
interactiveLinesAttr = false;
interactiveLineNumbersAttr = false;
hasPointerListeners = false;
hasDocumentPointerListeners = false;
selectedRange = null;
proposedSelectedRange;
renderedSelectionRange;
selectionAnchor;
queuedSelectionRender;
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();
if (this.queuedSelectionRender != null) {
cancelAnimationFrame(this.queuedSelectionRender);
this.queuedSelectionRender = void 0;
}
this.interactiveLinesAttr = false;
this.interactiveLineNumbersAttr = false;
this.hasPointerListeners = false;
}
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.renderedSelectionRange = void 0;
}
isSelectionDirty() {
return this.renderedSelectionRange === null;
}
setSelection(range, options) {
const isRangeChange = !(range === this.selectedRange || areSelectionsEqual(range ?? void 0, this.selectedRange ?? void 0));
if (!this.isSelectionDirty() && !isRangeChange) return;
this.proposedSelectedRange = void 0;
this.selectedRange = range;
this.renderSelection();
this.placeUtility();
if (isRangeChange && options?.notify !== false) this.notifySelectionCommitted();
}
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;
event.preventDefault();
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 { enableLineSelection = false, 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
};
if (enableLineSelection) {
this.selectionAnchor = {
lineNumber: anchor.lineNumber,
side: anchor.side
};
this.updateSelection(point.lineNumber, point.side, false);
this.notifySelectionStart(this.getCurrentSelectionRange());
}
this.attachDocumentPointerListeners();
}
handleDocumentPointerMove = (event) => {
const { enableLineSelection = false } = this.options;
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;
if (enableLineSelection === true) 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 { enableLineSelection = false, onGutterUtilityClick } = this.options;
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) {
this.pointerSession.current = point;
if (enableLineSelection) this.updateSelection(point.lineNumber, point.side);
}
onGutterUtilityClick?.(this.buildSelectedLineRange(this.pointerSession.anchor, this.pointerSession.current));
this.selectionAnchor = void 0;
if (enableLineSelection) {
this.notifySelectionEnd(this.getCurrentSelectionRange());
this.notifySelectionCommitted();
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.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.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 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;
if (this.options.controlledSelection === true) this.proposedSelectedRange = nextRange;
else {
this.selectedRange = nextRange;
this.queuedSelectionRender ??= requestAnimationFrame(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;
}
renderSelection = () => {
if (this.queuedSelectionRender != null) {
cancelAnimationFrame(this.queuedSelectionRender);
this.queuedSelectionRender = void 0;
}
if (this.pre == null || this.renderedSelectionRange === this.selectedRange) return;
const allSelected = this.pre.querySelectorAll("[data-selected-line]");
for (const element of allSelected) element.removeAttribute("data-selected-line");
this.renderedSelectionRange = this.selectedRange;
if (this.selectedRange == null) return;
const { children: codeElements } = this.pre;
if (codeElements.length === 0) return;
if (codeElements.length > 2) {
console.error(codeElements);
throw new Error("InteractionManager.renderSelection: Somehow there are more than 2 code elements...");
}
const split = this.pre.getAttribute("data-diff-type") === "split";
const rowRange = this.getIndexesFromSelection(this.selectedRange, split);
if (rowRange == null) {
console.error({
rowRange,
selectedRange: this.selectedRange
});
throw new Error("InteractionManager.renderSelection: No valid rowRange");
}
const isSingle = rowRange.start === rowRange.end;
const first = Math.min(rowRange.start, rowRange.end);
const last = Math.max(rowRange.start, rowRange.end);
for (const code of codeElements) {
const [gutter, content] = code.children;
const len = content.children.length;
if (len !== gutter.children.length) throw new Error("InteractionManager.renderSelection: gutter and content children dont match, something is wrong");
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) > last) break;
if (lineIndex == null || lineIndex < first) continue;
let attributeValue = isSingle ? "single" : lineIndex === first ? "first" : lineIndex === last ? "last" : "";
contentElement.setAttribute("data-selected-line", attributeValue);
gutterElement.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 (isSingle) {
attributeValue = "last";
contentElement.setAttribute("data-selected-line", "first");
} else if (lineIndex === first) attributeValue = "";
else if (lineIndex === last) contentElement.setAttribute("data-selected-line", "");
contentElement.nextSibling.setAttribute("data-selected-line", attributeValue);
gutterElement.nextSibling.setAttribute("data-selected-line", attributeValue);
}
}
}
};
notifySelectionCommitted() {
this.options.onLineSelected?.(this.getCurrentSelectionRange() ?? null);
}
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 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 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 isGutterUtilityPath(path) {
for (const element of path) {
if (!(element instanceof HTMLElement)) continue;
if (element.hasAttribute("data-utility-button") || element.hasAttribute("data-gutter-utility-slot") || element.getAttribute("slot") === "gutter-utility-slot" || element.getAttribute("name") === "gutter-utility-slot") return true;
}
return false;
}
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