Skip to content

Commit 5f395ad

Browse files
feat: Initial commit of Real-Time Diff Analyzer
1 parent d36fd27 commit 5f395ad

3 files changed

Lines changed: 575 additions & 0 deletions

File tree

index.html

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Stable Real-Time Code Diff Viewer</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
11+
<header>
12+
<h1>Code Diff Analyzer 📊 (Stable Input)</h1>
13+
<div class="stats-bar">
14+
<span class="stat-item">Difference: <strong id="percentageDisplay">0%</strong></span>
15+
<span class="stat-item">First Change: Line <strong id="lineDisplay">-</strong>, Col <strong id="colDisplay">-</strong></span>
16+
</div>
17+
<div class="controls">
18+
<button id="prevBtn" title="Previous Change">← Previous</button>
19+
<button id="nextBtn" title="Next Change">Next →</button>
20+
</div>
21+
</header>
22+
23+
<main class="editor-container">
24+
25+
<div class="editor-panel">
26+
<h2>Version A (Original)</h2>
27+
<button class="clear-btn" id="clearA">Clear A</button>
28+
29+
<div class="input-wrapper" id="inputWrapperA">
30+
<div class="line-numbers input-numbers" id="inputLineNumbersA"></div>
31+
<textarea id="versionAInput" class="paste-input"
32+
placeholder="Paste code for Version A here..."
33+
onscroll="syncScrollInput('versionAInput', 'inputLineNumbersA')"></textarea>
34+
</div>
35+
36+
<div class="code-wrapper">
37+
<div class="line-numbers" id="lineNumbersA"></div>
38+
<div class="code-editor" id="codeEditorA"
39+
onscroll="syncScrollDisplay('codeEditorA', 'codeEditorB')"></div>
40+
</div>
41+
</div>
42+
43+
<div class="editor-panel">
44+
<h2>Version B (Modified)</h2>
45+
<button class="clear-btn" id="clearB">Clear B</button>
46+
47+
<div class="input-wrapper" id="inputWrapperB">
48+
<div class="line-numbers input-numbers" id="inputLineNumbersB"></div>
49+
<textarea id="versionBInput" class="paste-input"
50+
placeholder="Paste code for Version B here..."
51+
onscroll="syncScrollInput('versionBInput', 'inputLineNumbersB')"></textarea>
52+
</div>
53+
54+
<div class="code-wrapper">
55+
<div class="line-numbers" id="lineNumbersB"></div>
56+
<div class="code-editor" id="codeEditorB"
57+
onscroll="syncScrollDisplay('codeEditorB', 'codeEditorA')"></div>
58+
</div>
59+
</div>
60+
</main>
61+
62+
<script src="https://cdn.jsdelivr.net/npm/diff-match-patch/index.min.js"></script>
63+
<script src="script.js"></script>
64+
</body>
65+
</html>

script.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
const dmp = new diff_match_patch();
2+
let diffResults = [];
3+
let currentDiffIndex = 0;
4+
let versionAContent = '';
5+
let versionBContent = '';
6+
7+
// Store the full diff list for accurate navigation and rendering
8+
let fullDiffsForNavigation = [];
9+
10+
// --- Debounce Helper Function ---
11+
function debounce(func, delay) {
12+
let timeoutId;
13+
return function(...args) {
14+
clearTimeout(timeoutId);
15+
timeoutId = setTimeout(() => {
16+
func.apply(this, args);
17+
}, delay);
18+
};
19+
}
20+
21+
// --- Line Number and Scroll Helpers ---
22+
23+
function calculateLineAndCol(text, index) {
24+
// Splits the text up to the index to find the line breaks
25+
const lines = text.substring(0, index).split('\n');
26+
const line = lines.length; // The line number is the count of lines + 1
27+
const col = lines[lines.length - 1].length + 1; // Column is characters on the last line segment
28+
return { line, col };
29+
}
30+
31+
function updateLineNumbers(lineNumbersDivId, content) {
32+
const lineNumbersDiv = document.getElementById(lineNumbersDivId);
33+
const lines = content.split('\n');
34+
let numbers = '';
35+
36+
const lineCount = (lines.length === 1 && lines[0].length === 0) ? 1 : lines.length;
37+
38+
for (let i = 1; i <= lineCount; i++) {
39+
numbers += i + '<br>';
40+
}
41+
lineNumbersDiv.innerHTML = numbers;
42+
}
43+
44+
// Line Numbers for Input Textareas
45+
function updateInputLineNumbers() {
46+
const contentA = document.getElementById('versionAInput').value;
47+
const contentB = document.getElementById('versionBInput').value;
48+
49+
updateLineNumbers('inputLineNumbersA', contentA);
50+
updateLineNumbers('inputLineNumbersB', contentB);
51+
}
52+
53+
// Sync scrolling for the display editors (Code Editors)
54+
function syncScrollDisplay(sourceId, targetId) {
55+
const source = document.getElementById(sourceId);
56+
const target = document.getElementById(targetId);
57+
58+
target.scrollTop = source.scrollTop;
59+
60+
document.getElementById('lineNumbersA').scrollTop = source.scrollTop;
61+
document.getElementById('lineNumbersB').scrollTop = source.scrollTop;
62+
}
63+
64+
// Sync scrolling for the input textareas and their line number panels
65+
function syncScrollInput(sourceInputId, targetNumbersId) {
66+
const source = document.getElementById(sourceInputId);
67+
const target = document.getElementById(targetNumbersId);
68+
69+
target.scrollTop = source.scrollTop;
70+
}
71+
72+
// --- Rendering and Diff Functions ---
73+
74+
function clearActiveDiffHighlight() {
75+
document.querySelectorAll('.active-diff').forEach(el => {
76+
el.classList.remove('active-diff');
77+
});
78+
}
79+
80+
function renderDiff(editorId, diffs, isVersionA) {
81+
const editorDiv = document.getElementById(editorId);
82+
let html = '';
83+
let contentForLineNumbers = '';
84+
85+
for (const diff of diffs) {
86+
const op = diff[0];
87+
const text = diff[1];
88+
89+
let spanClass = '';
90+
let showText = true;
91+
92+
if (op === 1) {
93+
spanClass = 'added';
94+
if (isVersionA) showText = false;
95+
} else if (op === -1) {
96+
spanClass = 'removed';
97+
if (!isVersionA) showText = false;
98+
}
99+
100+
// Replace spaces with non-breaking spaces and escape HTML
101+
const formattedText = text.replace(/ /g, '\u00a0').replace(/</g, '&lt;').replace(/>/g, '&gt;');
102+
103+
// Wrap text in an inline-block span for correct highlight application
104+
html += `<span class="${spanClass}">${showText ? formattedText : ''}</span>`;
105+
if (showText) {
106+
contentForLineNumbers += text;
107+
}
108+
}
109+
110+
editorDiv.innerHTML = html;
111+
112+
return contentForLineNumbers;
113+
}
114+
115+
let lastDiffCount = 0;
116+
117+
function runDiff(event) {
118+
versionAContent = document.getElementById('versionAInput').value;
119+
versionBContent = document.getElementById('versionBInput').value;
120+
121+
if (!versionAContent && !versionBContent) {
122+
document.getElementById('percentageDisplay').textContent = `0.00%`;
123+
document.getElementById('lineDisplay').textContent = '-';
124+
document.getElementById('colDisplay').textContent = '-';
125+
document.getElementById('codeEditorA').innerHTML = '';
126+
document.getElementById('codeEditorB').innerHTML = '';
127+
updateLineNumbers('lineNumbersA', '');
128+
updateLineNumbers('lineNumbersB', '');
129+
lastDiffCount = 0;
130+
clearActiveDiffHighlight();
131+
return;
132+
}
133+
134+
const diffs = dmp.diff_main(versionAContent, versionBContent);
135+
dmp.diff_cleanupSemantic(diffs);
136+
137+
fullDiffsForNavigation = diffs;
138+
139+
const newDiffResults = diffs.filter(d => d[0] !== 0);
140+
141+
const diffsChanged = newDiffResults.length !== lastDiffCount;
142+
lastDiffCount = newDiffResults.length;
143+
144+
diffResults = newDiffResults;
145+
146+
const contentA = renderDiff('codeEditorA', fullDiffsForNavigation, true);
147+
const contentB = renderDiff('codeEditorB', fullDiffsForNavigation, false);
148+
149+
updateLineNumbers('lineNumbersA', contentA);
150+
updateLineNumbers('lineNumbersB', contentB);
151+
152+
updateStats(fullDiffsForNavigation, versionAContent.length);
153+
154+
if (diffResults.length > 0) {
155+
if (diffsChanged) {
156+
currentDiffIndex = 0;
157+
}
158+
// Always call navigateDiff to re-apply the highlight and scroll
159+
navigateDiff(0, true);
160+
} else {
161+
clearActiveDiffHighlight();
162+
}
163+
}
164+
165+
166+
function updateStats(diffs, totalLengthA) {
167+
let equalChars = 0;
168+
let firstChangeIndexA = -1; // Character index in Version A where the first change occurs
169+
let currentCharIndexA = 0;
170+
171+
for (const diff of diffs) {
172+
const op = diff[0];
173+
const text = diff[1];
174+
175+
if (op === 0) {
176+
equalChars += text.length;
177+
currentCharIndexA += text.length;
178+
} else if (op === -1) {
179+
if (firstChangeIndexA === -1) {
180+
firstChangeIndexA = currentCharIndexA;
181+
}
182+
currentCharIndexA += text.length;
183+
} else if (op === 1) {
184+
if (firstChangeIndexA === -1) {
185+
firstChangeIndexA = currentCharIndexA;
186+
}
187+
}
188+
}
189+
190+
const totalChars = versionAContent.length + versionBContent.length;
191+
const totalDiffLength = totalChars - (2 * equalChars);
192+
const percentage = totalChars > 0 ? (totalDiffLength / totalChars) * 100 : 0;
193+
194+
document.getElementById('percentageDisplay').textContent = `${percentage.toFixed(2)}%`;
195+
196+
if (firstChangeIndexA !== -1) {
197+
// Correctly calculate line and column based on the full content string
198+
const { line, col } = calculateLineAndCol(versionAContent, firstChangeIndexA);
199+
document.getElementById('lineDisplay').textContent = line;
200+
document.getElementById('colDisplay').textContent = col;
201+
} else {
202+
document.getElementById('lineDisplay').textContent = '-';
203+
document.getElementById('colDisplay').textContent = '-';
204+
}
205+
}
206+
207+
208+
// FUNCTIONAL NAVIGATION: Scrolls the display panels to the change and applies purple highlight.
209+
function navigateDiff(indexOffset) {
210+
if (diffResults.length === 0) return;
211+
212+
// Update index based on offset, with loop-around logic
213+
currentDiffIndex += indexOffset;
214+
if (currentDiffIndex < 0) {
215+
currentDiffIndex = diffResults.length - 1;
216+
} else if (currentDiffIndex >= diffResults.length) {
217+
currentDiffIndex = 0;
218+
}
219+
220+
const targetChange = diffResults[currentDiffIndex];
221+
222+
let charIndexA = 0;
223+
224+
// Find the character index corresponding to the start of the current change
225+
for (const diff of fullDiffsForNavigation) {
226+
if (diff === targetChange) {
227+
break;
228+
}
229+
230+
if (diff[0] === 0 || diff[0] === -1) {
231+
charIndexA += diff[1].length;
232+
}
233+
}
234+
235+
// --- Scrolling ---
236+
const { line } = calculateLineAndCol(versionAContent, charIndexA);
237+
const editorA = document.getElementById('codeEditorA');
238+
const editorB = document.getElementById('codeEditorB');
239+
const lineHeight = 18;
240+
const scrollPosition = (line - 1) * lineHeight;
241+
242+
editorA.scrollTop = scrollPosition;
243+
editorB.scrollTop = scrollPosition;
244+
document.getElementById('lineNumbersA').scrollTop = scrollPosition;
245+
document.getElementById('lineNumbersB').scrollTop = scrollPosition;
246+
247+
// --- Highlighting ---
248+
clearActiveDiffHighlight();
249+
250+
const spansA = editorA.querySelectorAll('span');
251+
const spansB = editorB.querySelectorAll('span');
252+
253+
let currentSpanIndex = 0;
254+
255+
for (let i = 0; i < fullDiffsForNavigation.length; i++) {
256+
const diff = fullDiffsForNavigation[i];
257+
258+
if (diff === targetChange) {
259+
// Apply the active highlight (purple) to the span in the respective editor
260+
// Check span in version A (removed diffs)
261+
if (diff[0] === -1 || diff[0] === 0) {
262+
if (spansA[currentSpanIndex]) spansA[currentSpanIndex].classList.add('active-diff');
263+
}
264+
// Check span in version B (added diffs)
265+
if (diff[0] === 1 || diff[0] === 0) {
266+
if (spansB[currentSpanIndex]) spansB[currentSpanIndex].classList.add('active-diff');
267+
}
268+
break;
269+
}
270+
271+
// The span index always increments for every diff block
272+
currentSpanIndex++;
273+
}
274+
275+
// Update status bar for navigation
276+
document.getElementById('lineDisplay').textContent = line;
277+
document.getElementById('colDisplay').textContent = calculateLineAndCol(versionAContent, charIndexA).col;
278+
}
279+
280+
281+
function clearEditor(version) {
282+
const inputId = (version === 'A') ? 'versionAInput' : 'versionBInput';
283+
284+
document.getElementById(inputId).value = '';
285+
286+
runDiff(null);
287+
}
288+
289+
// Apply the debounce wrapper to runDiff
290+
const debouncedRunDiff = debounce(runDiff, 300);
291+
292+
// --- Event Listeners ---
293+
document.addEventListener('DOMContentLoaded', () => {
294+
295+
document.getElementById('nextBtn').addEventListener('click', () => navigateDiff(1));
296+
document.getElementById('prevBtn').addEventListener('click', () => navigateDiff(-1));
297+
298+
// Input listeners: Update line numbers immediately, debounce the heavy diff calculation
299+
document.getElementById('versionAInput').addEventListener('input', () => {
300+
updateInputLineNumbers();
301+
debouncedRunDiff();
302+
});
303+
document.getElementById('versionBInput').addEventListener('input', () => {
304+
updateInputLineNumbers();
305+
debouncedRunDiff();
306+
});
307+
308+
// Listen to scroll events on the resizable textareas to sync line numbers
309+
document.getElementById('versionAInput').addEventListener('scroll', () => syncScrollInput('versionAInput', 'inputLineNumbersA'));
310+
document.getElementById('versionBInput').addEventListener('scroll', () => syncScrollInput('versionBInput', 'inputLineNumbersB'));
311+
312+
document.getElementById('clearA').addEventListener('click', () => clearEditor('A'));
313+
document.getElementById('clearB').addEventListener('click', () => clearEditor('B'));
314+
315+
// Initial run on load
316+
runDiff(null);
317+
});

0 commit comments

Comments
 (0)