UNPKG

@pierre/diffs

Version:

407 lines (406 loc) 17.1 kB
import { DEFAULT_THEMES } from "../constants.js"; import { pluckInteractionOptions } from "../managers/InteractionManager.js"; import { createAnnotationWrapperNode } from "../utils/createAnnotationWrapperNode.js"; import { shouldUseTokenTransformer } from "../utils/shouldUseTokenTransformer.js"; import { areFilesEqual } from "../utils/areFilesEqual.js"; import { splitFileContents } from "../utils/splitFileContents.js"; import { FileDiff } from "./FileDiff.js"; import { getMergeConflictActionSlotName } from "../utils/getMergeConflictActionSlotName.js"; import { buildMergeConflictMarkerRows, getMergeConflictActionAnchor, parseMergeConflictDiffFromFile } from "../utils/parseMergeConflictDiffFromFile.js"; import { UnresolvedFileHunksRenderer } from "../renderers/UnresolvedFileHunksRenderer.js"; import { areMergeConflictActionsEqual } from "../utils/areMergeConflictActionsEqual.js"; import { resolveConflict } from "../utils/resolveConflict.js"; //#region src/components/UnresolvedFile.ts let instanceId = -1; var UnresolvedFile = class extends FileDiff { options; __id = `unresolved-file:${++instanceId}`; type = "unresolved-file"; computedCache = { file: void 0, fileDiff: void 0, actions: void 0, markerRows: void 0 }; conflictActions = []; markerRows = []; conflictActionCache = /* @__PURE__ */ new Map(); constructor(options = { theme: DEFAULT_THEMES }, workerManager, isContainerManaged = false) { super(void 0, workerManager, isContainerManaged); this.options = options; this.setOptions(options); } setOptions(options) { if (options == null) return; if (options.onMergeConflictAction != null && options.onMergeConflictResolve != null) throw new Error("UnresolvedFile: onMergeConflictAction and onMergeConflictResolve are mutually exclusive. Use only one callback."); this.options = options; this.hunksRenderer.setOptions(this.getHunksRendererOptions(options)); this.syncInteractionOptions(); } syncInteractionOptions() { this.interactionManager.setOptions(pluckInteractionOptions(this.options, typeof this.options.hunkSeparators === "function" || (this.options.hunkSeparators ?? "line-info") === "line-info" || this.options.hunkSeparators === "line-info-basic" ? this.handleExpandHunk : void 0, this.getLineIndex, this.handleMergeConflictActionClick)); } createHunksRenderer(options) { return new UnresolvedFileHunksRenderer(this.getHunksRendererOptions(options), this.getAnnotationSlotName, this.handleHighlightRender, this.workerManager); } getHunksRendererOptions(options) { return getUnresolvedDiffHunksRendererOptions(options, this.options); } applyPreNodeAttributes(pre, result) { super.applyPreNodeAttributes(pre, result, { "data-has-merge-conflict": "" }); } cleanUp() { this.emitPostRender(true); this.clearMergeConflictActionCache(); this.computedCache = { file: void 0, fileDiff: void 0, actions: void 0, markerRows: void 0 }; this.conflictActions = []; super.cleanUp(); } getOrComputeDiff({ file, fileDiff, actions, markerRows }) { const { maxContextLines, onMergeConflictAction } = this.options; wrapper: if (onMergeConflictAction != null) { const hasFileDiff = fileDiff != null; if (hasFileDiff !== (actions != null) || hasFileDiff !== (markerRows != null)) throw new Error("UnresolvedFile.getOrComputeDiff: fileDiff, actions, and markerRows must be passed together"); if (fileDiff != null && actions != null && markerRows != null) { this.computedCache = { file: file ?? this.computedCache.file, fileDiff, actions, markerRows }; break wrapper; } else if (file != null || this.computedCache.file != null) { if (file != null && this.computedCache.file != null && !areFilesEqual(file, this.computedCache.file) && this.computedCache.fileDiff != null && this.computedCache.actions != null) throw new Error("UnresolvedFile.getOrComputeDiff: file can only be used to initialize unresolved state once. Pass fileDiff and actions for subsequent updates."); file ??= this.computedCache.file; if (file == null) throw new Error("UnresolvedFile.getOrComputeDiff: file is null, should be impossible"); if (!areFilesEqual(file, this.computedCache.file) || this.computedCache.fileDiff == null || this.computedCache.actions == null) { const computed = parseMergeConflictDiffFromFile(file, maxContextLines); this.computedCache = { file, fileDiff: computed.fileDiff, actions: computed.actions, markerRows: computed.markerRows }; } fileDiff = this.computedCache.fileDiff; actions = this.computedCache.actions; markerRows = this.computedCache.markerRows; break wrapper; } else { fileDiff = this.computedCache.fileDiff; actions = this.computedCache.actions; markerRows = this.computedCache.markerRows; break wrapper; } } else { if (fileDiff != null || actions != null || markerRows != null) throw new Error("UnresolvedFile.getOrComputeDiff: fileDiff, actions, and markerRows are only usable in controlled mode, you must pass in `onMergeConflictAction`"); if (file != null && this.computedCache.file != null && !areFilesEqual(file, this.computedCache.file)) throw new Error("UnresolvedFile.getOrComputeDiff: uncontrolled unresolved files parse the file only once. Later updates must come from the cached diff state."); this.computedCache.file ??= file; if (this.computedCache.fileDiff == null && this.computedCache.file != null) { const computed = parseMergeConflictDiffFromFile(this.computedCache.file, maxContextLines); this.computedCache.fileDiff = computed.fileDiff; this.computedCache.actions = computed.actions; this.computedCache.markerRows = computed.markerRows; } fileDiff = this.computedCache.fileDiff; actions = this.computedCache.actions; markerRows = this.computedCache.markerRows; break wrapper; } if (fileDiff == null || actions == null || markerRows == null) return; return { fileDiff, actions, markerRows }; } hydrate(props) { const { file, fileDiff, actions, markerRows, lineAnnotations, fileContainer, prerenderedHTML, preventEmit = false } = props; const source = this.getOrComputeDiff({ file, fileDiff, actions, markerRows }); if (source == null) return; this.hydrateElements(fileContainer, prerenderedHTML); this.setActiveMergeConflictState(source.actions, source.markerRows); if (shouldRenderCode(this.pre, source.fileDiff, this.options.collapsed) || shouldRenderHeader(this.headerElement, source.fileDiff, this.options.disableFileHeader)) this.render({ ...props, preventEmit: true }); else { this.hydrationSetup({ fileDiff: source.fileDiff, lineAnnotations }); if (this.pre != null) this.renderMergeConflictActionSlots(); } if (!preventEmit) this.emitPostRender(); } rerender() { if (!this.enabled || this.fileDiff == null) return; this.render({ forceRender: true, renderRange: this.renderRange }); } render(props = {}) { const { file, fileDiff, actions, markerRows, lineAnnotations, preventEmit = false, ...rest } = props; const source = this.getOrComputeDiff({ file, fileDiff, actions, markerRows }); if (source == null) return false; this.setActiveMergeConflictState(source.actions, source.markerRows); const didRender = super.render({ ...rest, fileDiff: source.fileDiff, lineAnnotations, preventEmit: true }); if (didRender) { this.renderMergeConflictActionSlots(); if (!preventEmit) this.emitPostRender(); } return didRender; } resolveConflict(conflictIndex, resolution, fileDiff = this.computedCache.fileDiff) { const action = this.conflictActions[conflictIndex]; if (fileDiff == null || action == null) return; if (action.conflictIndex !== conflictIndex) { console.error({ conflictIndex, action }); throw new Error("UnresolvedFile.resolveConflict: conflictIndex and conflictAction don't match"); } const newFileDiff = resolveConflict(fileDiff, action, resolution); const previousFile = this.computedCache.file; const { file, actions, markerRows } = rebuildFileAndActions({ fileDiff: newFileDiff, previousActions: this.conflictActions, resolvedConflictIndex: conflictIndex, previousFile, resolution }); return { file, fileDiff: newFileDiff, actions, markerRows }; } resolveConflictAndRender(conflictIndex, resolution) { const action = this.conflictActions[conflictIndex]; if (action == null) return; if (action.conflictIndex !== conflictIndex) { console.error({ conflictIndex, action }); throw new Error("UnresolvedFile.resolveConflictAndRender: conflictIndex and conflictAction don't match"); } const payload = { resolution, conflict: action.conflict }; const { file, fileDiff, actions, markerRows } = this.resolveConflict(conflictIndex, resolution) ?? {}; if (file == null || fileDiff == null || actions == null || markerRows == null) return; this.computedCache = { file, fileDiff, actions, markerRows }; this.setActiveMergeConflictState(actions, markerRows); if (this.workerManager != null) this.hunksRenderer.renderDiff(fileDiff); else this.render({ forceRender: true }); this.options.onMergeConflictResolve?.(file, payload); } setActiveMergeConflictState(actions = this.conflictActions, markerRows = this.markerRows) { this.conflictActions = actions; this.markerRows = markerRows; if (this.computedCache.fileDiff != null && this.hunksRenderer instanceof UnresolvedFileHunksRenderer) this.hunksRenderer.setConflictState(this.options.mergeConflictActionsType === "none" ? [] : actions, markerRows, this.computedCache.fileDiff); } handleMergeConflictActionClick = (target) => { const action = this.conflictActions[target.conflictIndex]; if (action == null) return; if (action.conflictIndex !== target.conflictIndex) { console.error({ conflictIndex: target.conflictIndex, action }); throw new Error("UnresolvedFile.handleMergeConflictActionClick: conflictIndex and conflictAction don't match"); } const payload = { resolution: target.resolution, conflict: action.conflict }; if (this.options.onMergeConflictAction != null) { this.options.onMergeConflictAction(payload, this); return; } this.resolveConflictAndRender(target.conflictIndex, target.resolution); }; renderMergeConflictActionSlots() { const { fileDiff } = this.computedCache; if (this.isContainerManaged || this.fileContainer == null || typeof this.options.mergeConflictActionsType !== "function" || this.conflictActions.length === 0 || fileDiff == null) { this.clearMergeConflictActionCache(); return; } const staleActions = new Map(this.conflictActionCache); for (let actionIndex = 0; actionIndex < this.conflictActions.length; actionIndex++) { const action = this.conflictActions[actionIndex]; if (action == null) continue; if (action.conflictIndex !== actionIndex) { console.error({ conflictIndex: actionIndex, action }); throw new Error("UnresolvedFile.renderMergeConflictActionSlots: conflictIndex and conflictAction don't match"); } const anchor = getMergeConflictActionAnchor(action, fileDiff); if (anchor == null) continue; const conflictIndex = action.conflictIndex; const slotName = getMergeConflictActionSlotName({ hunkIndex: anchor.hunkIndex, lineIndex: anchor.lineIndex, conflictIndex }); const id = `${actionIndex}-${slotName}`; let cache = this.conflictActionCache.get(id); if (cache == null || !areMergeConflictActionsEqual(cache.action, action)) { cache?.element.remove(); const rendered = this.renderMergeConflictAction(action); if (rendered == null) continue; const element = createAnnotationWrapperNode(slotName); element.appendChild(rendered); this.fileContainer.appendChild(element); cache = { element, action }; this.conflictActionCache.set(id, cache); } staleActions.delete(id); } for (const [id, { element }] of staleActions.entries()) { this.conflictActionCache.delete(id); element.remove(); } } renderMergeConflictAction(action) { if (typeof this.options.mergeConflictActionsType !== "function") return; const rendered = this.options.mergeConflictActionsType(action, this); if (rendered == null) return; if (rendered instanceof HTMLElement) return rendered; if (typeof DocumentFragment !== "undefined" && rendered instanceof DocumentFragment) { const wrapper = document.createElement("div"); wrapper.style.display = "contents"; wrapper.appendChild(rendered); return wrapper; } } clearMergeConflictActionCache() { for (const { element } of this.conflictActionCache.values()) element.remove(); this.conflictActionCache.clear(); } }; function rebuildFileAndActions({ fileDiff, previousActions, resolvedConflictIndex, previousFile, resolution }) { const resolvedAction = previousActions[resolvedConflictIndex]; if (resolvedAction == null) throw new Error("rebuildFileAndActions: missing resolved action for unresolved file rebuild"); const actions = updateConflictActionsAfterResolution(previousActions, resolvedConflictIndex, resolvedAction, resolution); const markerRows = buildMergeConflictMarkerRows(fileDiff, actions); return { file: rebuildUnresolvedFile({ fileDiff, resolvedAction, resolvedConflictIndex, previousFile, resolution }), actions, markerRows }; } function rebuildUnresolvedFile({ resolvedAction, resolvedConflictIndex, previousFile, fileDiff, resolution }) { const lines = splitFileContents(previousFile?.contents ?? ""); const { conflict } = resolvedAction; const replacementLines = getResolvedConflictReplacementLines(lines, conflict, resolution); const contents = [ ...lines.slice(0, conflict.startLineIndex), ...replacementLines, ...lines.slice(conflict.endLineIndex + 1) ].join(""); return { name: previousFile?.name ?? fileDiff.name, contents, cacheKey: previousFile?.cacheKey != null ? `${previousFile.cacheKey}:mc-${resolvedConflictIndex}-${resolution}` : void 0 }; } function getResolvedConflictReplacementLines(lines, conflict, resolution) { const currentLines = lines.slice(conflict.startLineIndex + 1, conflict.baseMarkerLineIndex ?? conflict.separatorLineIndex); const incomingLines = lines.slice(conflict.separatorLineIndex + 1, conflict.endLineIndex); if (resolution === "current") return currentLines; if (resolution === "incoming") return incomingLines; return [...currentLines, ...incomingLines]; } function updateConflictActionsAfterResolution(previousActions, resolvedConflictIndex, resolvedAction, resolution) { const lineDelta = getResolvedConflictLineDelta(resolvedAction.conflict, resolution); return previousActions.map((action, index) => { if (index === resolvedConflictIndex) return; if (action == null) return; if (action.conflict.startLineIndex > resolvedAction.conflict.endLineIndex) return { ...action, conflict: shiftMergeConflictRegion(action.conflict, lineDelta) }; return action; }); } function getResolvedConflictLineDelta(conflict, resolution) { const currentLineCount = (conflict.baseMarkerLineIndex ?? conflict.separatorLineIndex) - conflict.startLineIndex - 1; const incomingLineCount = conflict.endLineIndex - conflict.separatorLineIndex - 1; return (resolution === "current" ? currentLineCount : resolution === "incoming" ? incomingLineCount : currentLineCount + incomingLineCount) - (conflict.endLineIndex - conflict.startLineIndex + 1); } function shiftMergeConflictRegion(conflict, lineDelta) { return { ...conflict, startLineIndex: conflict.startLineIndex + lineDelta, startLineNumber: conflict.startLineNumber + lineDelta, separatorLineIndex: conflict.separatorLineIndex + lineDelta, separatorLineNumber: conflict.separatorLineNumber + lineDelta, endLineIndex: conflict.endLineIndex + lineDelta, endLineNumber: conflict.endLineNumber + lineDelta, baseMarkerLineIndex: conflict.baseMarkerLineIndex != null ? conflict.baseMarkerLineIndex + lineDelta : void 0, baseMarkerLineNumber: conflict.baseMarkerLineNumber != null ? conflict.baseMarkerLineNumber + lineDelta : void 0 }; } function shouldRenderCode(pre, fileDiff, collapsed = false) { return !collapsed && pre == null && fileDiff != null; } function shouldRenderHeader(headerElement, fileDiff, disableFileHeader = false) { return headerElement == null && fileDiff != null && !disableFileHeader; } function getUnresolvedDiffHunksRendererOptions(options, baseOptions) { const merged = { ...baseOptions, ...options }; return { ...merged, useTokenTransformer: shouldUseTokenTransformer(merged), hunkSeparators: typeof options?.hunkSeparators === "function" ? "custom" : options?.hunkSeparators, mergeConflictActionsType: typeof options?.mergeConflictActionsType === "function" ? "custom" : options?.mergeConflictActionsType }; } //#endregion export { UnresolvedFile, getUnresolvedDiffHunksRendererOptions }; //# sourceMappingURL=UnresolvedFile.js.map