Skip to content

Commit cc72dea

Browse files
committed
can comment on diff ranges
1 parent 9b927ad commit cc72dea

File tree

6 files changed

+605
-55
lines changed

6 files changed

+605
-55
lines changed

apps/web/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@sentry/sveltekit": "^8.9.2",
2727
"highlight.js": "^11.10.0",
2828
"marked": "^10.0.0",
29-
"moment": "^2.30.1"
29+
"moment": "^2.30.1",
30+
"svelte-gravatar": "^1.0.3"
3031
}
3132
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
<script lang="ts">
2+
// Define types for the parsed lines and hunk information
3+
type DiffLine = {
4+
type: 'added' | 'removed' | 'context' | 'header' | 'hunk';
5+
content: string;
6+
lineNumber: number;
7+
leftLineNumber: number | null; // Use null for lines without line numbers
8+
rightLineNumber: number | null; // Use null for lines without line numbers
9+
};
10+
11+
// Props for the component (with type annotations)
12+
export let diff: string = '';
13+
export let diffPath: string = '';
14+
export let diffSha: string = '';
15+
16+
// eslint-disable-next-line func-style
17+
export let onRangeSelect: (range: string, diff_path: string, diff_sha: string) => void = () => {};
18+
19+
let selectedRange: { startLine: number | null; endLine: number | null } = {
20+
startLine: null,
21+
endLine: null
22+
};
23+
24+
$: selectedRange.startLine === null; // just to trigger reactivity
25+
26+
// Handle click event on the gutter line number
27+
function handleLineNumberClick(line: DiffLine, event: MouseEvent) {
28+
if (line === null) return;
29+
if (line.type === 'header' || line.type === 'hunk') return;
30+
/* dont highlight text when clicking on line number */
31+
document.getSelection()?.removeAllRanges();
32+
33+
// Check if Shift key is held to extend selection range
34+
if (event.shiftKey && selectedRange.startLine !== null) {
35+
if (line.lineNumber < selectedRange.startLine) {
36+
selectedRange.endLine = selectedRange.startLine;
37+
selectedRange.startLine = line.lineNumber;
38+
} else {
39+
selectedRange.endLine = line.lineNumber;
40+
}
41+
let range = rangeToString(selectedRange);
42+
onRangeSelect(range, diffPath, diffSha);
43+
} else {
44+
// Start a new selection range
45+
if (selectedRange.startLine === line.lineNumber) {
46+
selectedRange = { startLine: null, endLine: null };
47+
onRangeSelect('', '', '');
48+
} else {
49+
selectedRange = { startLine: line.lineNumber, endLine: null };
50+
let range = rangeToString(selectedRange);
51+
onRangeSelect(range, diffPath, diffSha);
52+
}
53+
}
54+
}
55+
56+
function rangeToString(range: { startLine: number | null; endLine: number | null }): string {
57+
let rangeString = '';
58+
parsedLines.forEach((line) => {
59+
if (line.lineNumber === range.startLine) {
60+
if (line.leftLineNumber !== null) {
61+
rangeString = `L${line.leftLineNumber}`;
62+
}
63+
if (line.rightLineNumber !== null) {
64+
rangeString = `R${line.rightLineNumber}`;
65+
} else {
66+
rangeString = ''; // selected a header or something
67+
}
68+
}
69+
});
70+
if (range.endLine !== null) {
71+
parsedLines.forEach((line) => {
72+
if (line.lineNumber === range.endLine) {
73+
if (line.leftLineNumber !== null) {
74+
rangeString += `-L${line.leftLineNumber}`;
75+
} else {
76+
rangeString += `-R${line.rightLineNumber}`;
77+
}
78+
}
79+
});
80+
}
81+
return rangeString;
82+
}
83+
84+
// Function to parse the diff string and extract meaningful lines and line numbers
85+
function parseDiff(diff: string): DiffLine[] {
86+
const lines = diff.split('\n');
87+
const parsedLines: DiffLine[] = [];
88+
let lineNumber: number = 0;
89+
let leftLineNumber: number = 0;
90+
let rightLineNumber: number = 0;
91+
92+
for (const line of lines) {
93+
lineNumber++;
94+
// Skip the diff header lines
95+
if (
96+
line.startsWith('diff ') ||
97+
line.startsWith('index ') ||
98+
line.startsWith('---') ||
99+
line.startsWith('+++')
100+
) {
101+
parsedLines.push({
102+
type: 'header',
103+
content: line,
104+
lineNumber,
105+
leftLineNumber: null,
106+
rightLineNumber: null
107+
});
108+
continue;
109+
}
110+
111+
// If the line starts with '@@', it's a hunk header; extract the starting line number
112+
if (line.startsWith('@@')) {
113+
const match = line.match(/@@ -(\d+)(,\d+)? \+(\d+)(,\d+)? @@/);
114+
if (match) {
115+
console.log(match);
116+
leftLineNumber = parseInt(match[1], 10);
117+
rightLineNumber = parseInt(match[3], 10);
118+
}
119+
parsedLines.push({
120+
type: 'hunk',
121+
content: line,
122+
lineNumber,
123+
leftLineNumber: null,
124+
rightLineNumber: null
125+
}); // Display hunk header with no line number
126+
continue;
127+
}
128+
129+
// Determine the type of each line and assign line numbers accordingly
130+
let type: 'added' | 'removed' | 'context' = 'context';
131+
let showLeftLineNumber: number | null = null;
132+
let showRightLineNumber: number | null = null;
133+
if (line.startsWith('+') && !line.startsWith('+++')) {
134+
type = 'added';
135+
rightLineNumber++;
136+
showRightLineNumber = rightLineNumber - 1;
137+
} else if (line.startsWith('-') && !line.startsWith('---')) {
138+
type = 'removed';
139+
leftLineNumber++;
140+
showLeftLineNumber = leftLineNumber - 1;
141+
} else {
142+
type = 'context';
143+
rightLineNumber++;
144+
leftLineNumber++;
145+
showLeftLineNumber = leftLineNumber - 1;
146+
showRightLineNumber = rightLineNumber - 1;
147+
}
148+
149+
parsedLines.push({
150+
type,
151+
content: line,
152+
lineNumber,
153+
leftLineNumber: showLeftLineNumber,
154+
rightLineNumber: showRightLineNumber
155+
});
156+
}
157+
158+
return parsedLines;
159+
}
160+
161+
function inRangeClass(lineNumber: number): string {
162+
if (selectedRange.startLine === null) {
163+
return '';
164+
}
165+
if (selectedRange.endLine === null) {
166+
if (lineNumber === selectedRange.startLine) {
167+
return 'inRange startRange endRange';
168+
}
169+
return '';
170+
}
171+
if (lineNumber >= selectedRange.startLine && lineNumber <= selectedRange.endLine) {
172+
let rangeClasses = 'inRange';
173+
if (lineNumber === selectedRange.startLine) {
174+
rangeClasses += ' startRange';
175+
}
176+
if (lineNumber === selectedRange.endLine) {
177+
rangeClasses += ' endRange';
178+
}
179+
return rangeClasses;
180+
}
181+
return '';
182+
}
183+
184+
// Store parsed lines in a reactive variable
185+
let parsedLines: DiffLine[] = parseDiff(diff);
186+
</script>
187+
188+
<div class="diff-container">
189+
<!-- Gutter with line numbers -->
190+
<div class="gutter">
191+
{#each parsedLines as line}
192+
{#if line.type !== 'header'}
193+
<!-- svelte-ignore a11y_click_events_have_key_events -->
194+
<!-- svelte-ignore a11y_no_static_element_interactions -->
195+
<div
196+
class={`gutterEntry ${line.type}`}
197+
on:click={(event) => handleLineNumberClick(line, event)}
198+
>
199+
{line.leftLineNumber !== null ? line.leftLineNumber : ' '}
200+
</div>
201+
{/if}
202+
{/each}
203+
</div>
204+
205+
<div class="gutter">
206+
{#each parsedLines as line}
207+
{#if line.type !== 'header'}
208+
<!-- svelte-ignore a11y_click_events_have_key_events -->
209+
<!-- svelte-ignore a11y_no_static_element_interactions -->
210+
<div
211+
class={`gutterEntry ${line.type}`}
212+
on:click={(event) => handleLineNumberClick(line, event)}
213+
>
214+
{line.rightLineNumber !== null ? line.rightLineNumber : ' '}
215+
</div>
216+
{/if}
217+
{/each}
218+
</div>
219+
220+
<!-- Content of the diff -->
221+
<div class="content">
222+
{#each parsedLines as line}
223+
{#if line.type !== 'header'}
224+
<div class={`line ${line.type} ${inRangeClass(line.lineNumber)}`}>
225+
͏{line.content}
226+
</div>
227+
{/if}
228+
{/each}
229+
</div>
230+
</div>
231+
232+
<style>
233+
.diff-container {
234+
display: flex;
235+
font-family: monospace;
236+
}
237+
238+
.gutter {
239+
width: 50px;
240+
background-color: #f7f7f7;
241+
padding: 0 10px;
242+
text-align: right;
243+
color: #999;
244+
border-right: 1px solid #ddd;
245+
cursor: pointer;
246+
}
247+
248+
.content {
249+
width: 100%;
250+
overflow-x: auto;
251+
}
252+
253+
.line {
254+
display: flex;
255+
white-space: pre; /* Preserve whitespace */
256+
padding: 4px;
257+
}
258+
259+
.line.added {
260+
background-color: #e6ffed;
261+
}
262+
263+
.line.removed {
264+
background-color: #ffeef0;
265+
}
266+
267+
.line.header {
268+
color: #999;
269+
}
270+
271+
.line.hunk {
272+
color: #556;
273+
background-color: #cef;
274+
}
275+
276+
.gutterEntry {
277+
padding: 4px;
278+
}
279+
280+
.startRange {
281+
border-top: 2px solid #2076e7;
282+
}
283+
284+
.endRange {
285+
border-bottom: 2px solid #2076e7;
286+
}
287+
288+
.inRange {
289+
border-left: 2px solid #2076e7;
290+
border-right: 2px solid #2076e7;
291+
background-color: #e4e4e4;
292+
color: #000000;
293+
}
294+
295+
.inRange.line.added {
296+
background-color: #9be19b;
297+
color: #1e4505;
298+
}
299+
300+
.inRange.line.removed {
301+
background-color: #f0bcc2;
302+
color: rgb(79, 5, 5);
303+
}
304+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script lang="ts">
2+
// Props for the component (with type annotations)
3+
export let diffArray: string[] = [];
4+
console.log(diffArray);
5+
</script>
6+
7+
<div class="diff">
8+
<div class="gutter">
9+
{#each diffArray as line}
10+
<div class="line">{line.right}</div>
11+
{/each}
12+
</div>
13+
<div class="lines">
14+
{#each diffArray as line}
15+
<div class="line {line.type}">{line.line}</div>
16+
{/each}
17+
</div>
18+
</div>
19+
20+
<style>
21+
.diff {
22+
display: flex;
23+
font-family: monospace;
24+
white-space: pre;
25+
background-color: #fff;
26+
padding: 8px;
27+
border: 1px solid #ccc;
28+
border-radius: 10px;
29+
width: 100%;
30+
}
31+
.gutter {
32+
padding: 0 8px;
33+
}
34+
.lines {
35+
width: 100%;
36+
}
37+
.line {
38+
padding: 2px;
39+
}
40+
.diff .added {
41+
background-color: #dfd;
42+
}
43+
.diff .removed {
44+
background-color: #fdd;
45+
}
46+
</style>

0 commit comments

Comments
 (0)