blob: ee7a12deb0f59f3be7c66a282857aeb9ff754970 [file] [log] [blame]
Sean McCullough71941bd2025-04-18 13:31:48 -07001import { css, html, LitElement, unsafeCSS } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
Sean McCullough86b56862025-04-18 13:04:03 -07003import * as Diff2Html from "diff2html";
4
Sean McCullough71941bd2025-04-18 13:31:48 -07005@customElement("sketch-diff-view")
Sean McCullough86b56862025-04-18 13:04:03 -07006export class SketchDiffView extends LitElement {
7 // Current commit hash being viewed
Sean McCullough71941bd2025-04-18 13:31:48 -07008 @property({ type: String })
Sean McCullough86b56862025-04-18 13:04:03 -07009 commitHash: string = "";
10
11 // Selected line in the diff for commenting
12 @state()
13 private selectedDiffLine: string | null = null;
14
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -070015 // The clicked button element used for positioning the comment box
16 @state()
17 private clickedElement: HTMLElement | null = null;
18
Sean McCullough86b56862025-04-18 13:04:03 -070019 // View format (side-by-side or line-by-line)
20 @state()
21 private viewFormat: "side-by-side" | "line-by-line" = "side-by-side";
22
23 static styles = css`
24 .diff-view {
25 flex: 1;
26 display: flex;
27 flex-direction: column;
28 overflow: hidden;
29 height: 100%;
30 }
Sean McCullough71941bd2025-04-18 13:31:48 -070031
Sean McCullough86b56862025-04-18 13:04:03 -070032 .diff-container {
33 height: 100%;
34 overflow: auto;
35 flex: 1;
36 padding: 0 1rem;
37 }
Sean McCullough71941bd2025-04-18 13:31:48 -070038
Sean McCullough86b56862025-04-18 13:04:03 -070039 #diff-view-controls {
40 display: flex;
41 justify-content: flex-end;
42 padding: 10px;
43 background: #f8f8f8;
44 border-bottom: 1px solid #eee;
45 }
Sean McCullough71941bd2025-04-18 13:31:48 -070046
Sean McCullough86b56862025-04-18 13:04:03 -070047 .diff-view-format {
48 display: flex;
49 gap: 10px;
50 }
Sean McCullough71941bd2025-04-18 13:31:48 -070051
Sean McCullough86b56862025-04-18 13:04:03 -070052 .diff-view-format label {
53 cursor: pointer;
54 }
Sean McCullough71941bd2025-04-18 13:31:48 -070055
Sean McCullough86b56862025-04-18 13:04:03 -070056 .diff2html-content {
57 font-family: var(--monospace-font);
58 position: relative;
59 }
Sean McCullough71941bd2025-04-18 13:31:48 -070060
Sean McCullough86b56862025-04-18 13:04:03 -070061 /* Comment box styles */
62 .diff-comment-box {
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -070063 position: absolute;
Sean McCullough86b56862025-04-18 13:04:03 -070064 width: 400px;
65 background-color: white;
66 border: 1px solid #ddd;
67 border-radius: 4px;
68 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
69 padding: 16px;
70 z-index: 1000;
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -070071 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -070072 }
Sean McCullough71941bd2025-04-18 13:31:48 -070073
Sean McCullough86b56862025-04-18 13:04:03 -070074 .diff-comment-box h3 {
75 margin-top: 0;
76 margin-bottom: 10px;
77 font-size: 16px;
78 }
Sean McCullough71941bd2025-04-18 13:31:48 -070079
Sean McCullough86b56862025-04-18 13:04:03 -070080 .selected-line {
81 margin-bottom: 10px;
82 font-size: 14px;
83 }
Sean McCullough71941bd2025-04-18 13:31:48 -070084
Sean McCullough86b56862025-04-18 13:04:03 -070085 .selected-line pre {
86 padding: 6px;
87 background: #f5f5f5;
88 border: 1px solid #eee;
89 border-radius: 3px;
90 margin: 5px 0;
91 max-height: 100px;
92 overflow: auto;
93 font-family: var(--monospace-font);
94 font-size: 13px;
95 white-space: pre-wrap;
96 }
Sean McCullough71941bd2025-04-18 13:31:48 -070097
Sean McCullough86b56862025-04-18 13:04:03 -070098 #diffCommentInput {
99 width: 100%;
100 height: 100px;
101 padding: 8px;
102 border: 1px solid #ddd;
103 border-radius: 4px;
104 resize: vertical;
105 font-family: inherit;
106 margin-bottom: 10px;
107 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700108
Sean McCullough86b56862025-04-18 13:04:03 -0700109 .diff-comment-buttons {
110 display: flex;
111 justify-content: flex-end;
112 gap: 8px;
113 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700114
Sean McCullough86b56862025-04-18 13:04:03 -0700115 .diff-comment-buttons button {
116 padding: 6px 12px;
117 border-radius: 4px;
118 border: 1px solid #ddd;
119 background: white;
120 cursor: pointer;
121 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700122
Sean McCullough86b56862025-04-18 13:04:03 -0700123 .diff-comment-buttons button:hover {
124 background: #f5f5f5;
125 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700126
Sean McCullough86b56862025-04-18 13:04:03 -0700127 .diff-comment-buttons button#submitDiffComment {
128 background: #1a73e8;
129 color: white;
130 border-color: #1a73e8;
131 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700132
Sean McCullough86b56862025-04-18 13:04:03 -0700133 .diff-comment-buttons button#submitDiffComment:hover {
134 background: #1967d2;
135 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700136
Sean McCullough86b56862025-04-18 13:04:03 -0700137 /* Styles for the comment button on diff lines */
138 .d2h-gutter-comment-button {
139 position: absolute;
140 right: 0;
141 top: 50%;
142 transform: translateY(-50%);
143 visibility: hidden;
144 background: rgba(255, 255, 255, 0.8);
145 border-radius: 50%;
146 width: 16px;
147 height: 16px;
148 line-height: 13px;
149 text-align: center;
150 font-size: 14px;
151 cursor: pointer;
152 color: #666;
153 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
154 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700155
Sean McCullough86b56862025-04-18 13:04:03 -0700156 tr:hover .d2h-gutter-comment-button {
157 visibility: visible;
158 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700159
Sean McCullough86b56862025-04-18 13:04:03 -0700160 .d2h-gutter-comment-button:hover {
161 background: white;
162 color: #333;
163 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
164 }
165 `;
Sean McCullough71941bd2025-04-18 13:31:48 -0700166
Sean McCullough86b56862025-04-18 13:04:03 -0700167 constructor() {
168 super();
169 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700170
Sean McCullough86b56862025-04-18 13:04:03 -0700171 // See https://lit.dev/docs/components/lifecycle/
172 connectedCallback() {
173 super.connectedCallback();
Sean McCullough71941bd2025-04-18 13:31:48 -0700174
Sean McCullough86b56862025-04-18 13:04:03 -0700175 // Load the diff2html CSS if needed
176 this.loadDiff2HtmlCSS();
177 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700178
Sean McCullough86b56862025-04-18 13:04:03 -0700179 // Load diff2html CSS into the shadow DOM
180 private async loadDiff2HtmlCSS() {
181 try {
182 // Check if diff2html styles are already loaded
Sean McCullough71941bd2025-04-18 13:31:48 -0700183 const styleId = "diff2html-styles";
Sean McCullough86b56862025-04-18 13:04:03 -0700184 if (this.shadowRoot?.getElementById(styleId)) {
185 return; // Already loaded
186 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700187
Sean McCullough86b56862025-04-18 13:04:03 -0700188 // Fetch the diff2html CSS
Sean McCullough71941bd2025-04-18 13:31:48 -0700189 const response = await fetch("static/diff2html.min.css");
190
Sean McCullough86b56862025-04-18 13:04:03 -0700191 if (!response.ok) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700192 console.error(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700193 `Failed to load diff2html CSS: ${response.status} ${response.statusText}`,
Sean McCullough71941bd2025-04-18 13:31:48 -0700194 );
Sean McCullough86b56862025-04-18 13:04:03 -0700195 return;
196 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700197
Sean McCullough86b56862025-04-18 13:04:03 -0700198 const cssText = await response.text();
Sean McCullough71941bd2025-04-18 13:31:48 -0700199
Sean McCullough86b56862025-04-18 13:04:03 -0700200 // Create a style element and append to shadow DOM
Sean McCullough71941bd2025-04-18 13:31:48 -0700201 const style = document.createElement("style");
Sean McCullough86b56862025-04-18 13:04:03 -0700202 style.id = styleId;
203 style.textContent = cssText;
204 this.shadowRoot?.appendChild(style);
Sean McCullough71941bd2025-04-18 13:31:48 -0700205
206 console.log("diff2html CSS loaded into shadow DOM");
Sean McCullough86b56862025-04-18 13:04:03 -0700207 } catch (error) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700208 console.error("Error loading diff2html CSS:", error);
Sean McCullough86b56862025-04-18 13:04:03 -0700209 }
210 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700211
Sean McCullough86b56862025-04-18 13:04:03 -0700212 // See https://lit.dev/docs/components/lifecycle/
213 disconnectedCallback() {
214 super.disconnectedCallback();
215 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700216
Sean McCullough86b56862025-04-18 13:04:03 -0700217 // Method called to load diff content
218 async loadDiffContent() {
219 // Wait for the component to be rendered
220 await this.updateComplete;
Sean McCullough71941bd2025-04-18 13:31:48 -0700221
222 const diff2htmlContent =
223 this.shadowRoot?.getElementById("diff2htmlContent");
Sean McCullough86b56862025-04-18 13:04:03 -0700224 if (!diff2htmlContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700225
Sean McCullough86b56862025-04-18 13:04:03 -0700226 try {
Sean McCullough86b56862025-04-18 13:04:03 -0700227 // Build the diff URL - include commit hash if specified
Sean McCullough71941bd2025-04-18 13:31:48 -0700228 const diffUrl = this.commitHash
229 ? `diff?commit=${this.commitHash}`
230 : "diff";
231
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700232 if (this.commitHash) {
233 diff2htmlContent.innerHTML = `Loading diff for commit <strong>${this.commitHash}</strong>...`;
234 } else {
235 diff2htmlContent.innerHTML = "Loading diff...";
236 }
237
Sean McCullough86b56862025-04-18 13:04:03 -0700238 // Fetch the diff from the server
239 const response = await fetch(diffUrl);
Sean McCullough71941bd2025-04-18 13:31:48 -0700240
Sean McCullough86b56862025-04-18 13:04:03 -0700241 if (!response.ok) {
242 throw new Error(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700243 `Server returned ${response.status}: ${response.statusText}`,
Sean McCullough86b56862025-04-18 13:04:03 -0700244 );
245 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700246
Sean McCullough86b56862025-04-18 13:04:03 -0700247 const diffText = await response.text();
Sean McCullough71941bd2025-04-18 13:31:48 -0700248
Sean McCullough86b56862025-04-18 13:04:03 -0700249 if (!diffText || diffText.trim() === "") {
250 diff2htmlContent.innerHTML =
251 "<span style='color: #666; font-style: italic;'>No changes detected since conversation started.</span>";
252 return;
253 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700254
Sean McCullough86b56862025-04-18 13:04:03 -0700255 // Render the diff using diff2html
256 const diffHtml = Diff2Html.html(diffText, {
257 outputFormat: this.viewFormat,
258 drawFileList: true,
259 matching: "lines",
260 renderNothingWhenEmpty: false,
261 colorScheme: "light" as any, // Force light mode to match the rest of the UI
262 });
Sean McCullough71941bd2025-04-18 13:31:48 -0700263
Sean McCullough86b56862025-04-18 13:04:03 -0700264 // Insert the generated HTML
265 diff2htmlContent.innerHTML = diffHtml;
Sean McCullough71941bd2025-04-18 13:31:48 -0700266
Sean McCullough86b56862025-04-18 13:04:03 -0700267 // Add CSS styles to ensure we don't have double scrollbars
268 const d2hFiles = diff2htmlContent.querySelectorAll(".d2h-file-wrapper");
269 d2hFiles.forEach((file) => {
270 const contentElem = file.querySelector(".d2h-files-diff");
271 if (contentElem) {
272 // Remove internal scrollbar - the outer container will handle scrolling
273 (contentElem as HTMLElement).style.overflow = "visible";
274 (contentElem as HTMLElement).style.maxHeight = "none";
275 }
276 });
Sean McCullough71941bd2025-04-18 13:31:48 -0700277
Philip Zeyliger6bc2eb22025-04-25 03:14:07 +0000278 // Intercept clicks on anchor links within the diff to prevent browser navigation
279 this.interceptAnchorClicks(diff2htmlContent);
280
Sean McCullough86b56862025-04-18 13:04:03 -0700281 // Add click event handlers to each code line for commenting
282 this.setupDiffLineComments();
Sean McCullough86b56862025-04-18 13:04:03 -0700283 } catch (error) {
284 console.error("Error loading diff2html content:", error);
285 const errorMessage =
286 error instanceof Error ? error.message : "Unknown error";
287 diff2htmlContent.innerHTML = `<span style='color: #dc3545;'>Error loading enhanced diff: ${errorMessage}</span>`;
288 }
289 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700290
Sean McCullough86b56862025-04-18 13:04:03 -0700291 // Handle view format changes
292 private handleViewFormatChange(event: Event) {
293 const input = event.target as HTMLInputElement;
294 if (input.checked) {
295 this.viewFormat = input.value as "side-by-side" | "line-by-line";
296 this.loadDiffContent();
297 }
298 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700299
Sean McCullough86b56862025-04-18 13:04:03 -0700300 /**
301 * Setup handlers for diff code lines to enable commenting
302 */
303 private setupDiffLineComments(): void {
Sean McCullough71941bd2025-04-18 13:31:48 -0700304 const diff2htmlContent =
305 this.shadowRoot?.getElementById("diff2htmlContent");
Sean McCullough86b56862025-04-18 13:04:03 -0700306 if (!diff2htmlContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700307
Sean McCullough86b56862025-04-18 13:04:03 -0700308 // Add plus buttons to each code line
309 this.addCommentButtonsToCodeLines();
Sean McCullough71941bd2025-04-18 13:31:48 -0700310
Sean McCullough86b56862025-04-18 13:04:03 -0700311 // Use event delegation for handling clicks on plus buttons
312 diff2htmlContent.addEventListener("click", (event) => {
313 const target = event.target as HTMLElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700314
Sean McCullough86b56862025-04-18 13:04:03 -0700315 // Only respond to clicks on the plus button
316 if (target.classList.contains("d2h-gutter-comment-button")) {
317 // Find the parent row first
318 const row = target.closest("tr");
319 if (!row) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700320
Sean McCullough86b56862025-04-18 13:04:03 -0700321 // Then find the code line in that row
Sean McCullough71941bd2025-04-18 13:31:48 -0700322 const codeLine =
323 row.querySelector(".d2h-code-side-line") ||
324 row.querySelector(".d2h-code-line");
Sean McCullough86b56862025-04-18 13:04:03 -0700325 if (!codeLine) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700326
Sean McCullough86b56862025-04-18 13:04:03 -0700327 // Get the line text content
328 const lineContent = codeLine.querySelector(".d2h-code-line-ctn");
329 if (!lineContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700330
Sean McCullough86b56862025-04-18 13:04:03 -0700331 const lineText = lineContent.textContent?.trim() || "";
Sean McCullough71941bd2025-04-18 13:31:48 -0700332
Sean McCullough86b56862025-04-18 13:04:03 -0700333 // Get file name to add context
334 const fileHeader = codeLine
335 .closest(".d2h-file-wrapper")
336 ?.querySelector(".d2h-file-name");
337 const fileName = fileHeader
338 ? fileHeader.textContent?.trim()
339 : "Unknown file";
Sean McCullough71941bd2025-04-18 13:31:48 -0700340
Sean McCullough86b56862025-04-18 13:04:03 -0700341 // Get line number if available
342 const lineNumElem = codeLine
343 .closest("tr")
344 ?.querySelector(".d2h-code-side-linenumber");
345 const lineNum = lineNumElem ? lineNumElem.textContent?.trim() : "";
346 const lineInfo = lineNum ? `Line ${lineNum}: ` : "";
Sean McCullough71941bd2025-04-18 13:31:48 -0700347
Sean McCullough86b56862025-04-18 13:04:03 -0700348 // Format the line for the comment box with file context and line number
349 const formattedLine = `${fileName} ${lineInfo}${lineText}`;
Sean McCullough71941bd2025-04-18 13:31:48 -0700350
Sean McCullough86b56862025-04-18 13:04:03 -0700351 console.log("Comment button clicked for line: ", formattedLine);
Sean McCullough71941bd2025-04-18 13:31:48 -0700352
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700353 // Open the comment box with this line and store the clicked element for positioning
354 this.clickedElement = target;
Sean McCullough86b56862025-04-18 13:04:03 -0700355 this.openDiffCommentBox(formattedLine);
Sean McCullough71941bd2025-04-18 13:31:48 -0700356
Sean McCullough86b56862025-04-18 13:04:03 -0700357 // Prevent event from bubbling up
358 event.stopPropagation();
359 }
360 });
361 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700362
Sean McCullough86b56862025-04-18 13:04:03 -0700363 /**
364 * Add plus buttons to each table row in the diff for commenting
365 */
366 private addCommentButtonsToCodeLines(): void {
Sean McCullough71941bd2025-04-18 13:31:48 -0700367 const diff2htmlContent =
368 this.shadowRoot?.getElementById("diff2htmlContent");
Sean McCullough86b56862025-04-18 13:04:03 -0700369 if (!diff2htmlContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700370
Sean McCullough86b56862025-04-18 13:04:03 -0700371 // Target code lines first, then find their parent rows
372 const codeLines = diff2htmlContent.querySelectorAll(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700373 ".d2h-code-side-line, .d2h-code-line",
Sean McCullough86b56862025-04-18 13:04:03 -0700374 );
Sean McCullough71941bd2025-04-18 13:31:48 -0700375
Sean McCullough86b56862025-04-18 13:04:03 -0700376 // Create a Set to store unique rows to avoid duplicates
377 const rowsSet = new Set<HTMLElement>();
Sean McCullough71941bd2025-04-18 13:31:48 -0700378
Sean McCullough86b56862025-04-18 13:04:03 -0700379 // Get all rows that contain code lines
Sean McCullough71941bd2025-04-18 13:31:48 -0700380 codeLines.forEach((line) => {
381 const row = line.closest("tr");
Sean McCullough86b56862025-04-18 13:04:03 -0700382 if (row) rowsSet.add(row as HTMLElement);
383 });
Sean McCullough71941bd2025-04-18 13:31:48 -0700384
Sean McCullough86b56862025-04-18 13:04:03 -0700385 // Convert Set back to array for processing
386 const codeRows = Array.from(rowsSet);
Sean McCullough71941bd2025-04-18 13:31:48 -0700387
Sean McCullough86b56862025-04-18 13:04:03 -0700388 codeRows.forEach((row) => {
389 const rowElem = row as HTMLElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700390
Sean McCullough86b56862025-04-18 13:04:03 -0700391 // Skip info lines without actual code (e.g., "file added")
392 if (rowElem.querySelector(".d2h-info")) {
393 return;
394 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700395
Sean McCullough86b56862025-04-18 13:04:03 -0700396 // Find the code line number element (first TD in the row)
397 const lineNumberCell = rowElem.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700398 ".d2h-code-side-linenumber, .d2h-code-linenumber",
Sean McCullough86b56862025-04-18 13:04:03 -0700399 );
Sean McCullough71941bd2025-04-18 13:31:48 -0700400
Sean McCullough86b56862025-04-18 13:04:03 -0700401 if (!lineNumberCell) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700402
Sean McCullough86b56862025-04-18 13:04:03 -0700403 // Create the plus button
404 const plusButton = document.createElement("span");
405 plusButton.className = "d2h-gutter-comment-button";
406 plusButton.innerHTML = "+";
407 plusButton.title = "Add a comment on this line";
Sean McCullough71941bd2025-04-18 13:31:48 -0700408
Sean McCullough86b56862025-04-18 13:04:03 -0700409 // Add button to the line number cell for proper positioning
410 (lineNumberCell as HTMLElement).style.position = "relative"; // Ensure positioning context
411 lineNumberCell.appendChild(plusButton);
412 });
413 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700414
Sean McCullough86b56862025-04-18 13:04:03 -0700415 /**
416 * Open the comment box for a selected diff line
417 */
418 private openDiffCommentBox(lineText: string): void {
419 // Make sure the comment box div exists
420 const commentBoxId = "diffCommentBox";
421 let commentBox = this.shadowRoot?.getElementById(commentBoxId);
Sean McCullough71941bd2025-04-18 13:31:48 -0700422
Sean McCullough86b56862025-04-18 13:04:03 -0700423 // If it doesn't exist, create it
424 if (!commentBox) {
425 commentBox = document.createElement("div");
426 commentBox.id = commentBoxId;
427 commentBox.className = "diff-comment-box";
Sean McCullough71941bd2025-04-18 13:31:48 -0700428
Sean McCullough86b56862025-04-18 13:04:03 -0700429 // Create the comment box contents
430 commentBox.innerHTML = `
431 <h3>Add a comment</h3>
432 <div class="selected-line">
433 Line:
434 <pre id="selectedLine"></pre>
435 </div>
436 <textarea
437 id="diffCommentInput"
438 placeholder="Enter your comment about this line..."
439 ></textarea>
440 <div class="diff-comment-buttons">
441 <button id="cancelDiffComment">Cancel</button>
442 <button id="submitDiffComment">Add Comment</button>
443 </div>
444 `;
Sean McCullough71941bd2025-04-18 13:31:48 -0700445
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700446 // Append the comment box to the diff container to ensure proper positioning
447 const diffContainer = this.shadowRoot?.querySelector(".diff-container");
448 if (diffContainer) {
449 diffContainer.appendChild(commentBox);
450 } else {
451 this.shadowRoot?.appendChild(commentBox);
452 }
Sean McCullough86b56862025-04-18 13:04:03 -0700453 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700454
Sean McCullough86b56862025-04-18 13:04:03 -0700455 // Store the selected line
456 this.selectedDiffLine = lineText;
Sean McCullough71941bd2025-04-18 13:31:48 -0700457
Sean McCullough86b56862025-04-18 13:04:03 -0700458 // Display the line in the comment box
459 const selectedLine = this.shadowRoot?.getElementById("selectedLine");
460 if (selectedLine) {
461 selectedLine.textContent = lineText;
462 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700463
Sean McCullough86b56862025-04-18 13:04:03 -0700464 // Reset the comment input
465 const commentInput = this.shadowRoot?.getElementById(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700466 "diffCommentInput",
Sean McCullough86b56862025-04-18 13:04:03 -0700467 ) as HTMLTextAreaElement;
468 if (commentInput) {
469 commentInput.value = "";
470 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700471
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700472 // Show the comment box and position it below the clicked line
473 if (commentBox && this.clickedElement) {
474 // Get the row that contains the clicked button
475 const row = this.clickedElement.closest("tr");
476 if (row) {
477 // Get the position of the row
478 const rowRect = row.getBoundingClientRect();
Philip Zeyliger72682df2025-04-23 13:09:46 -0700479 const diffContainerRect = this.shadowRoot
480 ?.querySelector(".diff-container")
481 ?.getBoundingClientRect();
482
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700483 if (diffContainerRect) {
484 // Position the comment box below the row
Philip Zeyliger72682df2025-04-23 13:09:46 -0700485 const topPosition =
486 rowRect.bottom -
487 diffContainerRect.top +
488 this.shadowRoot!.querySelector(".diff-container")!.scrollTop;
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700489 const leftPosition = rowRect.left - diffContainerRect.left;
Philip Zeyliger72682df2025-04-23 13:09:46 -0700490
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700491 commentBox.style.top = `${topPosition}px`;
492 commentBox.style.left = `${leftPosition}px`;
493 commentBox.style.display = "block";
494 }
495 } else {
496 // Fallback if we can't find the row
497 commentBox.style.display = "block";
498 }
499 } else if (commentBox) {
500 // Fallback if we don't have clickedElement
Sean McCullough86b56862025-04-18 13:04:03 -0700501 commentBox.style.display = "block";
502 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700503
Sean McCullough86b56862025-04-18 13:04:03 -0700504 // Add event listeners for submit and cancel buttons
505 const submitButton = this.shadowRoot?.getElementById("submitDiffComment");
506 if (submitButton) {
507 submitButton.onclick = () => this.submitDiffComment();
508 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700509
Sean McCullough86b56862025-04-18 13:04:03 -0700510 const cancelButton = this.shadowRoot?.getElementById("cancelDiffComment");
511 if (cancelButton) {
512 cancelButton.onclick = () => this.closeDiffCommentBox();
513 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700514
Sean McCullough86b56862025-04-18 13:04:03 -0700515 // Focus on the comment input
516 if (commentInput) {
517 commentInput.focus();
518 }
519 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700520
Sean McCullough86b56862025-04-18 13:04:03 -0700521 /**
522 * Close the diff comment box without submitting
523 */
524 private closeDiffCommentBox(): void {
525 const commentBox = this.shadowRoot?.getElementById("diffCommentBox");
526 if (commentBox) {
527 commentBox.style.display = "none";
528 }
529 this.selectedDiffLine = null;
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700530 this.clickedElement = null;
Sean McCullough86b56862025-04-18 13:04:03 -0700531 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700532
Sean McCullough86b56862025-04-18 13:04:03 -0700533 /**
534 * Submit a comment on a diff line
535 */
536 private submitDiffComment(): void {
537 const commentInput = this.shadowRoot?.getElementById(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700538 "diffCommentInput",
Sean McCullough86b56862025-04-18 13:04:03 -0700539 ) as HTMLTextAreaElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700540
Sean McCullough86b56862025-04-18 13:04:03 -0700541 if (!commentInput) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700542
Sean McCullough86b56862025-04-18 13:04:03 -0700543 const comment = commentInput.value.trim();
Sean McCullough71941bd2025-04-18 13:31:48 -0700544
Sean McCullough86b56862025-04-18 13:04:03 -0700545 // Validate inputs
546 if (!this.selectedDiffLine || !comment) {
547 alert("Please select a line and enter a comment.");
548 return;
549 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700550
Sean McCullough86b56862025-04-18 13:04:03 -0700551 // Format the comment in a readable way
552 const formattedComment = `\`\`\`\n${this.selectedDiffLine}\n\`\`\`\n\n${comment}`;
Sean McCullough71941bd2025-04-18 13:31:48 -0700553
Sean McCullough86b56862025-04-18 13:04:03 -0700554 // Dispatch a custom event with the formatted comment
Sean McCullough71941bd2025-04-18 13:31:48 -0700555 const event = new CustomEvent("diff-comment", {
Sean McCullough86b56862025-04-18 13:04:03 -0700556 detail: { comment: formattedComment },
557 bubbles: true,
Sean McCullough71941bd2025-04-18 13:31:48 -0700558 composed: true,
Sean McCullough86b56862025-04-18 13:04:03 -0700559 });
560 this.dispatchEvent(event);
Sean McCullough71941bd2025-04-18 13:31:48 -0700561
Sean McCullough86b56862025-04-18 13:04:03 -0700562 // Close only the comment box but keep the diff view open
563 this.closeDiffCommentBox();
564 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700565
Sean McCullough86b56862025-04-18 13:04:03 -0700566 // Clear the current state
567 public clearState(): void {
568 this.commitHash = "";
569 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700570
Sean McCullough86b56862025-04-18 13:04:03 -0700571 // Show diff for a specific commit
572 public showCommitDiff(commitHash: string): void {
573 // Store the commit hash
574 this.commitHash = commitHash;
575 // Load the diff content
576 this.loadDiffContent();
577 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700578
Philip Zeyliger6bc2eb22025-04-25 03:14:07 +0000579 /**
580 * Intercept clicks on anchor links within the diff to prevent default browser navigation
581 * and instead scroll to the target element without changing URL
582 *
583 * @param container The container element containing diff content
584 */
585 private interceptAnchorClicks(container: HTMLElement): void {
586 const anchors = container.querySelectorAll('a[href^="#"]');
587
588 anchors.forEach((anchor) => {
589 anchor.addEventListener("click", (event) => {
590 event.preventDefault();
591
592 // Extract the target ID from the href
593 const href = (anchor as HTMLAnchorElement).getAttribute("href");
594 if (!href || !href.startsWith("#")) return;
595
596 const targetId = href.substring(1);
597 const targetElement = container.querySelector(`[id="${targetId}"]`);
598
599 if (targetElement) {
600 // Scroll the target element into view
601 targetElement.scrollIntoView({ behavior: "smooth" });
602 }
603 });
604 });
605 }
606
Sean McCullough86b56862025-04-18 13:04:03 -0700607 render() {
608 return html`
609 <div class="diff-view">
610 <div class="diff-container">
611 <div id="diff-view-controls">
612 <div class="diff-view-format">
613 <label>
Sean McCullough71941bd2025-04-18 13:31:48 -0700614 <input
615 type="radio"
616 name="diffViewFormat"
617 value="side-by-side"
Sean McCullough86b56862025-04-18 13:04:03 -0700618 ?checked=${this.viewFormat === "side-by-side"}
619 @change=${this.handleViewFormatChange}
Sean McCullough71941bd2025-04-18 13:31:48 -0700620 />
621 Side-by-side
Sean McCullough86b56862025-04-18 13:04:03 -0700622 </label>
623 <label>
Sean McCullough71941bd2025-04-18 13:31:48 -0700624 <input
625 type="radio"
626 name="diffViewFormat"
Sean McCullough86b56862025-04-18 13:04:03 -0700627 value="line-by-line"
628 ?checked=${this.viewFormat === "line-by-line"}
629 @change=${this.handleViewFormatChange}
Sean McCullough71941bd2025-04-18 13:31:48 -0700630 />
631 Line-by-line
Sean McCullough86b56862025-04-18 13:04:03 -0700632 </label>
633 </div>
634 </div>
635 <div id="diff2htmlContent" class="diff2html-content"></div>
636 </div>
637 </div>
638 `;
639 }
640}
641
642declare global {
643 interface HTMLElementTagNameMap {
644 "sketch-diff-view": SketchDiffView;
645 }
646}