Add proper diff viewer with syntax highlighting
This commit is contained in:
parent
d9c27b4353
commit
60eaae5371
23 changed files with 2358 additions and 870 deletions
|
|
@ -1,129 +1,546 @@
|
|||
---
|
||||
import { runHighlighterWithAstro } from '../../node_modules/@astrojs/prism/dist/highlighter.js';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
hints: string[];
|
||||
file?: string;
|
||||
}
|
||||
|
||||
const { code, hints } = Astro.props;
|
||||
const { code, hints, file } = Astro.props;
|
||||
const id = `dv-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
const lines = code.split('\n');
|
||||
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);
|
||||
});
|
||||
---
|
||||
|
||||
<div id={id} class="diff-viewer-container" data-dv-id={id}>
|
||||
<div id={id} class="diff-viewer-container" data-dv-id={id} data-original-code={code} data-file-extension={fileExtension}>
|
||||
<div class="bg-[#0d1117] border border-[#30363d] rounded-md overflow-hidden">
|
||||
<div class="bg-[#161b22] px-4 py-3 border-b border-[#30363d]">
|
||||
<span class="text-sm text-[#8b949e]">{Astro.url.pathname.split('/').pop() || 'file.js'}</span>
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full font-mono text-sm">
|
||||
<tbody>
|
||||
{lines.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}`));
|
||||
|
||||
return (
|
||||
<tr
|
||||
data-line={lineNum}
|
||||
class="group hover:bg-[#161b22] cursor-pointer transition-colors"
|
||||
title={`Mark line ${lineNum} as buggy`}
|
||||
>
|
||||
<td class="w-16 text-right pr-4 select-none">
|
||||
<span class={`text-[#8b949e] line-number ${hasHint ? 'has-hint-color' : ''}`}>
|
||||
<!-- 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`}
|
||||
>
|
||||
{lineNum}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-0.5 whitespace-pre">
|
||||
<span class={`text-[#e6edf3] ${hasHint ? 'has-hint-bg bg-[#ff7b7233] px-2 -mx-2' : ''}`}>
|
||||
{line || ' '}
|
||||
</span>
|
||||
{hasHint && (
|
||||
<span class="ml-2 hint text-[#79c0ff] text-xs hidden">
|
||||
• {hintText?.replace(`Line ${lineNum}: `, '')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
tr {
|
||||
.left-gutter-row,
|
||||
.right-gutter-row {
|
||||
height: 1.6em;
|
||||
}
|
||||
|
||||
.code-row {
|
||||
height: 1.6em;
|
||||
min-height: 1.6em;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
line-height: 1.6;
|
||||
tab-size: 2;
|
||||
-moz-tab-size: 2;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #161b22;
|
||||
|
||||
.diff-row {
|
||||
height: 1.6em;
|
||||
min-height: 1.6em;
|
||||
}
|
||||
td:first-child {
|
||||
border-right: 1px solid #30363d;
|
||||
|
||||
.diff-row.added {
|
||||
background-color: #3fb95018;
|
||||
}
|
||||
tr.bugged td:first-child {
|
||||
border-left: 3px solid #f85149;
|
||||
}
|
||||
tr.bugged .line-number {
|
||||
color: #f85149;
|
||||
}
|
||||
tr.bugged {
|
||||
|
||||
.diff-row.removed {
|
||||
background-color: #f8514918;
|
||||
}
|
||||
.diff-viewer-container:not(.revealed) tr.bugged .line-number.has-hint-color {
|
||||
color: #f85149 !important;
|
||||
|
||||
.code-row.diff-removed {
|
||||
background-color: #f8514918;
|
||||
}
|
||||
.diff-viewer-container:not(.revealed) tr:not(.bugged) span.line-number.has-hint-color {
|
||||
color: #8b949e !important;
|
||||
font-weight: normal !important;
|
||||
|
||||
.code-row.diff-added {
|
||||
background-color: #3fb95018;
|
||||
}
|
||||
.diff-viewer-container.revealed tr.bugged .hint {
|
||||
|
||||
.diff-viewer-container:not(.revealed) .has-hint .code-text {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.diff-viewer-container.revealed .has-hint .hint-text {
|
||||
display: inline;
|
||||
}
|
||||
.diff-viewer-container:not(.revealed) td > span.has-hint-bg {
|
||||
background-color: transparent !important;
|
||||
|
||||
.has-hint .code-text {
|
||||
background-color: #f8514918;
|
||||
}
|
||||
|
||||
.has-hint.has-hint-bg .code-text {
|
||||
background-color: #f8514918;
|
||||
}
|
||||
|
||||
.has-hint .has-hint-num {
|
||||
color: #f85149 !important;
|
||||
}
|
||||
|
||||
.has-hint:not(.revealed) .has-hint-num {
|
||||
color: #484f58 !important;
|
||||
}
|
||||
|
||||
.diff-viewer-container.revealed .has-hint .has-hint-num {
|
||||
color: #f85149 !important;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script client:load>
|
||||
(() => {
|
||||
const el = document.querySelector('[data-dv-id]');
|
||||
if (!el) return;
|
||||
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');
|
||||
|
||||
const buggedLines = new Set();
|
||||
|
||||
function updateBuggedState() {
|
||||
const rows = el.querySelectorAll('tr[data-line]');
|
||||
rows.forEach(row => {
|
||||
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
|
||||
row.classList.toggle('bugged', buggedLines.has(lineNum));
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', function(e) {
|
||||
const target = e.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
const row = target.closest('tr[data-line]');
|
||||
if (!row) return;
|
||||
// Right side scroll sync (also scrolls left side)
|
||||
if (rightEditorArea) {
|
||||
rightEditorArea.addEventListener('scroll', () => {
|
||||
syncScroll(rightEditorArea, leftGutter, leftCodeArea);
|
||||
});
|
||||
}
|
||||
|
||||
const lineNum = parseInt(row.getAttribute('data-line') || '0', 10);
|
||||
if (buggedLines.has(lineNum)) {
|
||||
buggedLines.delete(lineNum);
|
||||
} else {
|
||||
buggedLines.add(lineNum);
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBuggedState();
|
||||
// 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;
|
||||
}
|
||||
|
||||
el.dispatchEvent(new CustomEvent('bugged-line-toggle', {
|
||||
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) {
|
||||
buggedLines.delete(lineNum);
|
||||
if (gutterRow) { gutterRow.style.backgroundColor = ''; gutterRow.style.borderLeft = ''; }
|
||||
if (codeRow) { codeRow.style.backgroundColor = ''; }
|
||||
} else {
|
||||
buggedLines.add(lineNum);
|
||||
if (gutterRow) { gutterRow.style.backgroundColor = '#f8514920'; gutterRow.style.borderLeft = '3px solid #f85149'; }
|
||||
if (codeRow) { codeRow.style.backgroundColor = '#f8514920'; }
|
||||
}
|
||||
|
||||
container.dispatchEvent(new CustomEvent('bugged-line-toggle', {
|
||||
bubbles: true,
|
||||
detail: { line: lineNum, bugged: buggedLines.has(lineNum), allBugged: [...buggedLines] }
|
||||
}));
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
// 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();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue