2026-04-16 19:44:23 +02:00
|
|
|
---
|
2026-05-03 15:36:14 +02:00
|
|
|
import { runHighlighterWithAstro } from '../../node_modules/@astrojs/prism/dist/highlighter.js';
|
|
|
|
|
|
2026-04-16 19:44:23 +02:00
|
|
|
interface Props {
|
|
|
|
|
code: string;
|
|
|
|
|
hints: string[];
|
2026-05-03 15:36:14 +02:00
|
|
|
file?: string;
|
2026-04-16 19:44:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
const { code, hints, file } = Astro.props;
|
2026-04-17 13:09:06 +02:00
|
|
|
const id = `dv-${Math.random().toString(36).substring(2, 9)}`;
|
2026-04-16 19:44:23 +02:00
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
const originalLines = code.split('\n');
|
|
|
|
|
const fileExtension = (file?.split('/').pop() || 'file').split('.').pop() || '';
|
|
|
|
|
|
|
|
|
|
const langMap: Record<string, string> = {
|
|
|
|
|
js: 'javascript', jsx: 'jsx', ts: 'typescript', tsx: 'tsx',
|
|
|
|
|
html: 'html', css: 'css', json: 'json', py: 'python',
|
|
|
|
|
rb: 'ruby', java: 'java', c: 'c', cpp: 'cpp', cs: 'csharp',
|
|
|
|
|
go: 'go', rs: 'rust', php: 'php', swift: 'swift', kt: 'kotlin',
|
|
|
|
|
sh: 'bash', yaml: 'yaml', yml: 'yaml', md: 'markdown', sql: 'sql',
|
|
|
|
|
xml: 'xml', dockerfile: 'dockerfile', gitignore: 'bash', astro: 'astro',
|
|
|
|
|
};
|
|
|
|
|
const language = langMap[fileExtension] || undefined;
|
|
|
|
|
|
|
|
|
|
let highlightedLines: string[] = [];
|
|
|
|
|
if (language) {
|
|
|
|
|
try {
|
|
|
|
|
const { html } = runHighlighterWithAstro(language, code);
|
|
|
|
|
const parsed = html.split('\n');
|
|
|
|
|
if (parsed.length > originalLines.length && parsed[parsed.length - 1] === '') {
|
|
|
|
|
parsed.pop();
|
|
|
|
|
}
|
|
|
|
|
highlightedLines = parsed.length === originalLines.length ? parsed : [];
|
|
|
|
|
} catch {
|
|
|
|
|
// Fallback: no highlighting
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const highlightedMap = new Map<string, string>();
|
|
|
|
|
highlightedLines.forEach((hl, i) => {
|
|
|
|
|
highlightedMap.set(String(i + 1), hl);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const lineContentMap = new Map<string, string>();
|
|
|
|
|
originalLines.forEach((line, i) => {
|
|
|
|
|
const lineNum = String(i + 1);
|
|
|
|
|
lineContentMap.set(lineNum, highlightedMap.get(lineNum) || line);
|
|
|
|
|
});
|
2026-04-16 19:44:23 +02:00
|
|
|
---
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
<div id={id} class="diff-viewer-container" data-dv-id={id} data-original-code={code} data-file-extension={fileExtension}>
|
2026-04-17 13:09:06 +02:00
|
|
|
<div class="bg-[#0d1117] border border-[#30363d] rounded-md overflow-hidden">
|
2026-05-03 15:36:14 +02:00
|
|
|
<!-- Toolbar -->
|
|
|
|
|
<div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d] flex items-center justify-between">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<span class="text-sm text-[#8b949e]">{file?.split('/').pop() || 'file'}.{fileExtension}</span>
|
|
|
|
|
<span class="text-xs text-[#484f58]">|</span>
|
|
|
|
|
<span class="text-xs text-[#8b949e]">
|
|
|
|
|
<span id="left-lines-count">{originalLines.length}</span> lines
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<button id="reveal-hints-btn" class="text-xs px-2 py-1 rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#30363d] transition-colors focus:outline-none focus:ring-0">
|
|
|
|
|
Reveal Hints
|
|
|
|
|
</button>
|
|
|
|
|
<button id="reset-btn" class="text-xs px-2 py-1 rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] hover:bg-[#30363d] transition-colors focus:outline-none focus:ring-0">
|
|
|
|
|
Reset
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-04-16 19:44:23 +02:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
<!-- Side-by-side viewer -->
|
|
|
|
|
<div class="flex flex-col md:flex-row min-h-[400px]">
|
|
|
|
|
<!-- Left panel: Original code -->
|
|
|
|
|
<div class="flex-1 flex flex-col border-b md:border-b-0 md:border-r border-[#30363d]">
|
|
|
|
|
<div class="bg-[#161b22] px-4 py-2 border-b border-[#30363d] flex items-center justify-between">
|
|
|
|
|
<span class="text-xs font-semibold text-[#f85149] uppercase tracking-wider">Original (Buggy)</span>
|
|
|
|
|
<span class="text-xs text-[#484f58]">Read-only</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 overflow-auto relative">
|
|
|
|
|
<div class="flex min-h-full">
|
|
|
|
|
<!-- Line numbers -->
|
|
|
|
|
<div class="left-gutter select-none bg-[#0d1117] border-r border-[#30363d] py-2 sticky left-0 z-10">
|
|
|
|
|
{originalLines.map((_, index) => {
|
|
|
|
|
const lineNum = index + 1;
|
|
|
|
|
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-line={lineNum}
|
|
|
|
|
data-side="left"
|
|
|
|
|
class={`line-gutter-row font-mono text-right pr-3 pl-3 text-[#484f58] text-sm cursor-pointer hover:text-[#8b949e] transition-colors leading-[1.6] ${hasHint ? 'has-hint' : ''}`}
|
|
|
|
|
title={`Mark line ${lineNum} as buggy`}
|
|
|
|
|
>
|
2026-04-17 13:09:06 +02:00
|
|
|
{lineNum}
|
2026-05-03 15:36:14 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Code content -->
|
|
|
|
|
<div class="left-code-area flex-1 py-2 overflow-hidden">
|
|
|
|
|
{originalLines.map((line, index) => {
|
|
|
|
|
const lineNum = index + 1;
|
|
|
|
|
const hasHint = hints.some(h => h.includes(`Line ${lineNum}`));
|
|
|
|
|
const hintText = hints.find(h => h.includes(`Line ${lineNum}`));
|
|
|
|
|
const content = highlightedMap.get(String(lineNum)) || line || ' ';
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-line={lineNum}
|
|
|
|
|
data-side="left"
|
|
|
|
|
class={`code-row flex items-start gap-0 px-4 text-sm leading-[1.6] transition-colors ${hasHint ? 'has-hint' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<span class={`text-[#484f58] w-0 select-none ${hasHint ? 'has-hint-num' : ''}`}></span>
|
|
|
|
|
<span class={`code-text font-mono text-[#e6edf3] whitespace-pre ${hasHint ? 'has-hint-bg' : ''}`} set:html={content}></span>
|
|
|
|
|
{hasHint && (
|
|
|
|
|
<span class="hint-text font-mono text-[#79c0ff] text-xs ml-2 hidden">
|
|
|
|
|
{hintText?.replace(`Line ${lineNum}: `, '')}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-17 13:09:06 +02:00
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
<!-- Right panel: Editable code -->
|
|
|
|
|
<div class="flex-1 flex flex-col">
|
|
|
|
|
<div class="bg-[#161b22] px-4 py-2 border-b border-[#30363d] flex items-center justify-between">
|
|
|
|
|
<span class="text-xs font-semibold text-[#3fb950] uppercase tracking-wider">Your Fix</span>
|
|
|
|
|
<span class="text-xs text-[#484f58]">Editable</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 overflow-auto relative">
|
|
|
|
|
<div class="flex min-h-full">
|
|
|
|
|
<!-- Line numbers -->
|
|
|
|
|
<div class="right-gutter select-none bg-[#0d1117] border-r border-[#30363d] py-2 sticky left-0 z-10">
|
|
|
|
|
{originalLines.map((_, index) => (
|
|
|
|
|
<div
|
|
|
|
|
data-line={index + 1}
|
|
|
|
|
data-side="right"
|
|
|
|
|
class="right-gutter-row font-mono text-right pr-3 pl-3 text-[#484f58] text-sm leading-[1.6] bg-transparent"
|
|
|
|
|
>
|
|
|
|
|
{index + 1}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Editable content with diff overlay -->
|
|
|
|
|
<div class="right-editor-area flex-1 relative py-0">
|
|
|
|
|
<!-- Diff highlight overlay -->
|
|
|
|
|
<div id="diff-highlight" class="absolute inset-0 py-2 pointer-events-none">
|
|
|
|
|
{originalLines.map((_, index) => {
|
|
|
|
|
const lineNum = index + 1;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-diff-line={lineNum}
|
|
|
|
|
class="diff-row h-[1.6em] px-4 transition-colors"
|
|
|
|
|
></div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Textarea for editing -->
|
|
|
|
|
<textarea
|
|
|
|
|
id="edit-textarea"
|
|
|
|
|
class="editor-textarea absolute inset-0 w-full h-full bg-transparent text-[#e6edf3] font-mono text-sm leading-[1.6] resize-none focus:outline-none pl-4 pr-4 py-2"
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
>{code}</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-28 15:59:47 +02:00
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
<!-- Status bar -->
|
|
|
|
|
<div class="bg-[#161b22] px-4 py-2 border-t border-[#30363d] flex items-center justify-between text-xs">
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
<span class="text-[#484f58]">
|
|
|
|
|
<span id="diff-added" class="text-[#3fb950]">0</span> added
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-[#484f58]">
|
|
|
|
|
<span id="diff-removed" class="text-[#f85149]">0</span> removed
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-[#484f58]">
|
|
|
|
|
<span id="diff-changed" class="text-[#d29922]">0</span> changed
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-[#484f58]">
|
|
|
|
|
Click lines on the left to mark as buggy
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-17 13:09:06 +02:00
|
|
|
</div>
|
2026-04-16 19:44:23 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-05-03 15:36:14 +02:00
|
|
|
.left-gutter-row,
|
|
|
|
|
.right-gutter-row {
|
|
|
|
|
height: 1.6em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.code-row {
|
|
|
|
|
height: 1.6em;
|
|
|
|
|
min-height: 1.6em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-textarea {
|
2026-04-16 19:44:23 +02:00
|
|
|
line-height: 1.6;
|
2026-05-03 15:36:14 +02:00
|
|
|
tab-size: 2;
|
|
|
|
|
-moz-tab-size: 2;
|
2026-04-16 19:44:23 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.diff-row {
|
|
|
|
|
height: 1.6em;
|
|
|
|
|
min-height: 1.6em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.diff-row.added {
|
|
|
|
|
background-color: #3fb95018;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.diff-row.removed {
|
|
|
|
|
background-color: #f8514918;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.code-row.diff-removed {
|
|
|
|
|
background-color: #f8514918;
|
2026-04-16 19:44:23 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.code-row.diff-added {
|
|
|
|
|
background-color: #3fb95018;
|
2026-04-16 19:44:23 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.diff-viewer-container:not(.revealed) .has-hint .code-text {
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.diff-viewer-container.revealed .has-hint .hint-text {
|
|
|
|
|
display: inline;
|
2026-04-17 13:09:06 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.has-hint .code-text {
|
|
|
|
|
background-color: #f8514918;
|
2026-04-17 13:09:06 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.has-hint.has-hint-bg .code-text {
|
2026-04-17 13:09:06 +02:00
|
|
|
background-color: #f8514918;
|
|
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.has-hint .has-hint-num {
|
2026-04-28 15:59:47 +02:00
|
|
|
color: #f85149 !important;
|
|
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.has-hint:not(.revealed) .has-hint-num {
|
|
|
|
|
color: #484f58 !important;
|
2026-04-28 15:59:47 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
.diff-viewer-container.revealed .has-hint .has-hint-num {
|
|
|
|
|
color: #f85149 !important;
|
2026-04-28 15:59:47 +02:00
|
|
|
}
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
/* Scrollbar styling */
|
|
|
|
|
.overflow-auto::-webkit-scrollbar {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overflow-auto::-webkit-scrollbar-track {
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overflow-auto::-webkit-scrollbar-thumb {
|
|
|
|
|
background: #30363d;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overflow-auto::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: #484f58;
|
2026-04-28 15:59:47 +02:00
|
|
|
}
|
2026-04-16 19:44:23 +02:00
|
|
|
</style>
|
2026-04-17 13:09:06 +02:00
|
|
|
|
|
|
|
|
<script client:load>
|
|
|
|
|
(() => {
|
2026-05-03 15:36:14 +02:00
|
|
|
const container = document.querySelector('[data-dv-id]');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
const originalCode = container.getAttribute('data-original-code') || '';
|
|
|
|
|
const originalLines = originalCode.split('\n');
|
|
|
|
|
|
|
|
|
|
const leftGutter = container.querySelector('.left-gutter');
|
|
|
|
|
const leftCodeArea = container.querySelector('.left-code-area');
|
|
|
|
|
const rightGutter = container.querySelector('.right-gutter');
|
|
|
|
|
const rightEditorArea = container.querySelector('.right-editor-area');
|
|
|
|
|
const textarea = document.getElementById('edit-textarea');
|
|
|
|
|
const diffHighlight = document.getElementById('diff-highlight');
|
|
|
|
|
const revealBtn = document.getElementById('reveal-hints-btn');
|
|
|
|
|
const resetBtn = document.getElementById('reset-btn');
|
|
|
|
|
const addedEl = document.getElementById('diff-added');
|
|
|
|
|
const removedEl = document.getElementById('diff-removed');
|
|
|
|
|
const changedEl = document.getElementById('diff-changed');
|
2026-04-17 13:09:06 +02:00
|
|
|
|
|
|
|
|
const buggedLines = new Set();
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
// Synchronized scrolling
|
|
|
|
|
function syncScroll(source, targetGutter, targetCode) {
|
|
|
|
|
if (targetCode) {
|
|
|
|
|
targetCode.scrollTop = source.scrollTop;
|
|
|
|
|
targetCode.scrollLeft = source.scrollLeft;
|
|
|
|
|
}
|
|
|
|
|
if (targetGutter) {
|
|
|
|
|
targetGutter.scrollTop = source.scrollTop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Left side scroll sync (also scrolls right side)
|
|
|
|
|
if (leftCodeArea) {
|
|
|
|
|
leftCodeArea.addEventListener('scroll', () => {
|
|
|
|
|
syncScroll(leftCodeArea, rightGutter, rightEditorArea);
|
2026-04-17 13:09:06 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
// Right side scroll sync (also scrolls left side)
|
|
|
|
|
if (rightEditorArea) {
|
|
|
|
|
rightEditorArea.addEventListener('scroll', () => {
|
|
|
|
|
syncScroll(rightEditorArea, leftGutter, leftCodeArea);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-17 13:09:06 +02:00
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
// Diff computation using LCS-based line diff
|
|
|
|
|
function computeDiff(oldText, newText) {
|
|
|
|
|
const oldLines = oldText.split('\n');
|
|
|
|
|
const newLines = newText.split('\n');
|
|
|
|
|
|
|
|
|
|
// LCS to compute diff
|
|
|
|
|
const m = oldLines.length;
|
|
|
|
|
const n = newLines.length;
|
|
|
|
|
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i <= m; i++) {
|
|
|
|
|
for (let j = 1; j <= n; j++) {
|
|
|
|
|
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
|
|
|
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
|
|
|
} else {
|
|
|
|
|
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Backtrack to get diff ops
|
|
|
|
|
const ops = [];
|
|
|
|
|
let i = m, j = n;
|
|
|
|
|
while (i > 0 || j > 0) {
|
|
|
|
|
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
|
|
|
ops.push({ type: 'equal', oldLine: i, newLine: j });
|
|
|
|
|
i--; j--;
|
|
|
|
|
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
|
|
|
ops.push({ type: 'added', newLine: j });
|
|
|
|
|
j--;
|
|
|
|
|
} else {
|
|
|
|
|
ops.push({ type: 'removed', oldLine: i });
|
|
|
|
|
i--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ops.reverse();
|
|
|
|
|
return ops;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateDiffHighlight() {
|
|
|
|
|
if (!textarea || !diffHighlight) return;
|
|
|
|
|
|
|
|
|
|
const newText = textarea.value;
|
|
|
|
|
const allOld = originalLines;
|
|
|
|
|
const allNew = newText.split('\n');
|
|
|
|
|
const totalLines = Math.max(allOld.length, allNew.length);
|
|
|
|
|
|
|
|
|
|
let added = 0, removed = 0, changed = 0;
|
|
|
|
|
|
|
|
|
|
// Rebuild diff rows to match textarea line count
|
|
|
|
|
diffHighlight.innerHTML = '';
|
|
|
|
|
for (let i = 0; i < totalLines; i++) {
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
row.setAttribute('data-diff-line', String(i + 1));
|
|
|
|
|
row.className = 'diff-row h-[1.6em] px-4 transition-colors';
|
|
|
|
|
diffHighlight.appendChild(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare line by line using LCS-based approach
|
|
|
|
|
let oi = 0, ni = 0;
|
|
|
|
|
const statusMap = new Map(); // old line index -> 'equal' | 'removed' | 'changed'
|
|
|
|
|
|
|
|
|
|
while (oi < allOld.length || ni < allNew.length) {
|
|
|
|
|
if (oi < allOld.length && ni < allNew.length && allOld[oi] === allNew[ni]) {
|
|
|
|
|
statusMap.set(oi, 'equal');
|
|
|
|
|
oi++;
|
|
|
|
|
ni++;
|
|
|
|
|
} else if (oi < allOld.length && ni < allNew.length) {
|
|
|
|
|
statusMap.set(oi, 'changed');
|
|
|
|
|
added++;
|
|
|
|
|
changed++;
|
|
|
|
|
oi++;
|
|
|
|
|
ni++;
|
|
|
|
|
} else if (oi < allOld.length) {
|
|
|
|
|
statusMap.set(oi, 'removed');
|
|
|
|
|
removed++;
|
|
|
|
|
oi++;
|
|
|
|
|
} else {
|
|
|
|
|
added++;
|
|
|
|
|
ni++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply to right side diff overlay
|
|
|
|
|
statusMap.forEach((status, idx) => {
|
|
|
|
|
const lineNum = idx + 1;
|
|
|
|
|
const row = diffHighlight.querySelector(`[data-diff-line="${lineNum}"]`);
|
|
|
|
|
if (row) {
|
|
|
|
|
if (status === 'removed') {
|
|
|
|
|
row.classList.add('removed');
|
|
|
|
|
} else if (status === 'changed') {
|
|
|
|
|
row.classList.add('added');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Apply to left panel code rows (only removed = red, no yellow)
|
|
|
|
|
statusMap.forEach((status, idx) => {
|
|
|
|
|
const lineNum = idx + 1;
|
|
|
|
|
const row = leftCodeArea?.querySelector(`.code-row[data-line="${lineNum}"]`);
|
|
|
|
|
if (row) {
|
|
|
|
|
row.classList.remove('diff-removed', 'diff-added');
|
|
|
|
|
if (status === 'removed') {
|
|
|
|
|
row.classList.add('diff-removed');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Highlight added lines beyond original length
|
|
|
|
|
for (let i = allOld.length; i < allNew.length; i++) {
|
|
|
|
|
const row = diffHighlight.querySelector(`[data-diff-line="${i + 1}"]`);
|
|
|
|
|
if (row) {
|
|
|
|
|
row.classList.add('added');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addedEl.textContent = added;
|
|
|
|
|
removedEl.textContent = removed;
|
|
|
|
|
changedEl.textContent = changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Left panel: click to mark bugged lines (gutter + code area)
|
|
|
|
|
function toggleBuggedLine(lineNum) {
|
|
|
|
|
if (isNaN(lineNum) || lineNum === 0) return;
|
|
|
|
|
|
|
|
|
|
const gutterRow = leftGutter?.querySelector(`[data-line="${lineNum}"][data-side="left"]`);
|
|
|
|
|
const codeRow = leftCodeArea?.querySelector(`.code-row[data-line="${lineNum}"]`);
|
|
|
|
|
|
|
|
|
|
const isBugged = buggedLines.has(lineNum);
|
|
|
|
|
|
|
|
|
|
if (isBugged) {
|
2026-04-17 13:09:06 +02:00
|
|
|
buggedLines.delete(lineNum);
|
2026-05-03 15:36:14 +02:00
|
|
|
if (gutterRow) { gutterRow.style.backgroundColor = ''; gutterRow.style.borderLeft = ''; }
|
|
|
|
|
if (codeRow) { codeRow.style.backgroundColor = ''; }
|
2026-04-17 13:09:06 +02:00
|
|
|
} else {
|
|
|
|
|
buggedLines.add(lineNum);
|
2026-05-03 15:36:14 +02:00
|
|
|
if (gutterRow) { gutterRow.style.backgroundColor = '#f8514920'; gutterRow.style.borderLeft = '3px solid #f85149'; }
|
|
|
|
|
if (codeRow) { codeRow.style.backgroundColor = '#f8514920'; }
|
2026-04-17 13:09:06 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-03 15:36:14 +02:00
|
|
|
container.dispatchEvent(new CustomEvent('bugged-line-toggle', {
|
2026-04-17 13:09:06 +02:00
|
|
|
bubbles: true,
|
|
|
|
|
detail: { line: lineNum, bugged: buggedLines.has(lineNum), allBugged: [...buggedLines] }
|
|
|
|
|
}));
|
2026-05-03 15:36:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leftGutter?.addEventListener('click', (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
if (!(target instanceof HTMLElement)) return;
|
|
|
|
|
const row = target.closest('[data-line][data-side="left"]');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
|
|
|
|
|
toggleBuggedLine(lineNum);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
leftCodeArea?.addEventListener('click', (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
if (!(target instanceof HTMLElement)) return;
|
|
|
|
|
const row = target.closest('.code-row');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
|
|
|
|
|
toggleBuggedLine(lineNum);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reveal hints
|
|
|
|
|
revealBtn?.addEventListener('click', () => {
|
|
|
|
|
const isRevealed = container.classList.toggle('revealed');
|
|
|
|
|
revealBtn.textContent = isRevealed ? 'Hide Hints' : 'Reveal Hints';
|
|
|
|
|
revealBtn.classList.toggle('text-[#79c0ff]', isRevealed);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reset
|
|
|
|
|
resetBtn?.addEventListener('click', () => {
|
|
|
|
|
textarea.value = originalLines.join('\n');
|
|
|
|
|
buggedLines.clear();
|
|
|
|
|
container.classList.remove('revealed');
|
|
|
|
|
revealBtn.textContent = 'Reveal Hints';
|
|
|
|
|
revealBtn.classList.remove('text-[#79c0ff]');
|
|
|
|
|
|
|
|
|
|
const gutterRows = leftGutter?.querySelectorAll('[data-side="left"]');
|
|
|
|
|
gutterRows?.forEach(row => {
|
|
|
|
|
row.style.backgroundColor = '';
|
|
|
|
|
row.style.borderLeft = '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const codeRows = leftCodeArea?.querySelectorAll('.code-row');
|
|
|
|
|
codeRows?.forEach(row => {
|
|
|
|
|
row.style.backgroundColor = '';
|
|
|
|
|
row.classList.remove('diff-removed', 'diff-added');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
updateDiffHighlight();
|
|
|
|
|
|
|
|
|
|
container.dispatchEvent(new CustomEvent('bugged-line-reset', {
|
|
|
|
|
bubbles: true,
|
|
|
|
|
detail: { allBugged: [] }
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Textarea input -> update diff
|
|
|
|
|
textarea?.addEventListener('input', () => {
|
|
|
|
|
updateDiffHighlight();
|
2026-04-17 13:09:06 +02:00
|
|
|
});
|
2026-05-03 15:36:14 +02:00
|
|
|
|
|
|
|
|
// Tab key support in textarea
|
|
|
|
|
textarea?.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Tab') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const end = textarea.selectionEnd;
|
|
|
|
|
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
|
|
|
|
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
|
|
|
updateDiffHighlight();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Initial diff
|
|
|
|
|
updateDiffHighlight();
|
2026-04-17 13:09:06 +02:00
|
|
|
})();
|
|
|
|
|
</script>
|