import { diffWordsWithSpace } from "diff"; import type { ReviewHighlightedToken } from "./shikiReviewHighlighter"; interface ReviewDiffOperation { readonly value: string; readonly added?: true; readonly removed?: true; } interface ReviewDiffHighlightRange { readonly start: number; readonly end: number; } const REVIEW_MAX_WORD_ALT_LINE_LENGTH = 1_000; function cleanLineEnding(value: string): string { return value.endsWith("\n") ? value.slice(0, -1) : value; } function pushOrJoinSpan(input: { readonly operation: ReviewDiffOperation; readonly spans: Array<[0 | 1, string]>; readonly enableJoin: boolean; readonly isNeutral?: boolean; readonly isLastOperation?: boolean; }): void { const { operation, spans, enableJoin, isNeutral = false, isLastOperation = false } = input; const lastSpan = spans.at(-1); if (!lastSpan || isLastOperation || !enableJoin) { spans.push([isNeutral ? 0 : 1, operation.value]); return; } const lastSpanIsNeutral = lastSpan[0] === 0; if ( isNeutral === lastSpanIsNeutral || (isNeutral && operation.value.length === 1 && !lastSpanIsNeutral) ) { lastSpan[1] += operation.value; return; } spans.push([isNeutral ? 0 : 1, operation.value]); } function spansToRanges( spans: ReadonlyArray, ): ReadonlyArray { const ranges: ReviewDiffHighlightRange[] = []; let offset = 0; spans.forEach(([kind, value]) => { const nextOffset = offset + value.length; if (kind === 1 && value.length > 0) { ranges.push({ start: offset, end: nextOffset }); } offset = nextOffset; }); return ranges; } function mergeNearbyRanges( ranges: ReadonlyArray, ): ReadonlyArray { if (ranges.length <= 1) { return ranges; } const merged: ReviewDiffHighlightRange[] = []; ranges.forEach((range) => { const previous = merged.at(-1); if (previous && range.start - previous.end <= 1) { merged[merged.length - 1] = { start: previous.start, end: range.end }; return; } merged.push({ ...range }); }); return merged; } function appendTokenSegment( target: ReviewHighlightedToken[], source: ReviewHighlightedToken, content: string, diffHighlight: boolean, ): void { if (content.length === 0) { return; } const previous = target.at(-1); if ( previous && previous.color === source.color && previous.fontStyle === source.fontStyle && previous.diffHighlight === diffHighlight ) { previous.content += content; return; } target.push({ content, color: source.color, fontStyle: source.fontStyle, diffHighlight, }); } export function computeWordAltDiffRanges(input: { readonly deletionLine: string; readonly additionLine: string; }): { readonly deletion: ReadonlyArray; readonly addition: ReadonlyArray; } { const deletionLine = cleanLineEnding(input.deletionLine); const additionLine = cleanLineEnding(input.additionLine); if (deletionLine.length === 0 && additionLine.length === 0) { return { deletion: [], addition: [] }; } if ( deletionLine.length > REVIEW_MAX_WORD_ALT_LINE_LENGTH || additionLine.length > REVIEW_MAX_WORD_ALT_LINE_LENGTH ) { return { deletion: [], addition: [] }; } const operations = diffWordsWithSpace( deletionLine, additionLine, ) as ReadonlyArray; const deletionSpans: Array<[0 | 1, string]> = []; const additionSpans: Array<[0 | 1, string]> = []; const lastOperation = operations.at(-1); operations.forEach((operation) => { const isLastOperation = operation === lastOperation; if (!operation.added && !operation.removed) { pushOrJoinSpan({ operation, spans: deletionSpans, enableJoin: true, isNeutral: true, isLastOperation, }); pushOrJoinSpan({ operation, spans: additionSpans, enableJoin: true, isNeutral: true, isLastOperation, }); return; } if (operation.removed) { pushOrJoinSpan({ operation, spans: deletionSpans, enableJoin: true, isLastOperation, }); return; } pushOrJoinSpan({ operation, spans: additionSpans, enableJoin: true, isLastOperation, }); }); return { deletion: mergeNearbyRanges(spansToRanges(deletionSpans)), addition: mergeNearbyRanges(spansToRanges(additionSpans)), }; } export function applyDiffRangesToTokens( tokens: ReadonlyArray, ranges: ReadonlyArray, ): ReadonlyArray { if (tokens.length === 0 || ranges.length === 0) { return tokens; } const nextTokens: ReviewHighlightedToken[] = []; let tokenOffset = 0; let rangeIndex = 0; tokens.forEach((token) => { const tokenStart = tokenOffset; const tokenEnd = tokenStart + token.content.length; tokenOffset = tokenEnd; if (token.content.length === 0) { nextTokens.push(token); return; } while (rangeIndex < ranges.length && ranges[rangeIndex]!.end <= tokenStart) { rangeIndex += 1; } if ((ranges[rangeIndex]?.start ?? Number.POSITIVE_INFINITY) >= tokenEnd) { nextTokens.push(token.diffHighlight ? { ...token, diffHighlight: false } : token); return; } let cursor = tokenStart; let localRangeIndex = rangeIndex; while ((ranges[localRangeIndex]?.start ?? Number.POSITIVE_INFINITY) < tokenEnd) { const range = ranges[localRangeIndex]; if (!range) { break; } if (range.start > cursor) { appendTokenSegment( nextTokens, token, token.content.slice(cursor - tokenStart, range.start - tokenStart), false, ); } const highlightedStart = Math.max(cursor, range.start); const highlightedEnd = Math.min(tokenEnd, range.end); if (highlightedEnd > highlightedStart) { appendTokenSegment( nextTokens, token, token.content.slice(highlightedStart - tokenStart, highlightedEnd - tokenStart), true, ); } cursor = highlightedEnd; if (range.end <= tokenEnd) { localRangeIndex += 1; } else { break; } } if (cursor < tokenEnd) { appendTokenSegment(nextTokens, token, token.content.slice(cursor - tokenStart), false); } rangeIndex = localRangeIndex; }); return nextTokens; }