|
| 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> |
0 commit comments