blob: ba2081622436a299b7c29d70170a10c594270c93 [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 Zeyligeraf2d7e32025-04-23 11:35:03 -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 Zeyligeraf2d7e32025-04-23 11:35:03 -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
Sean McCullough86b56862025-04-18 13:04:03 -0700278 // Add click event handlers to each code line for commenting
279 this.setupDiffLineComments();
Sean McCullough86b56862025-04-18 13:04:03 -0700280 } catch (error) {
281 console.error("Error loading diff2html content:", error);
282 const errorMessage =
283 error instanceof Error ? error.message : "Unknown error";
284 diff2htmlContent.innerHTML = `<span style='color: #dc3545;'>Error loading enhanced diff: ${errorMessage}</span>`;
285 }
286 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700287
Sean McCullough86b56862025-04-18 13:04:03 -0700288 // Handle view format changes
289 private handleViewFormatChange(event: Event) {
290 const input = event.target as HTMLInputElement;
291 if (input.checked) {
292 this.viewFormat = input.value as "side-by-side" | "line-by-line";
293 this.loadDiffContent();
294 }
295 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700296
Sean McCullough86b56862025-04-18 13:04:03 -0700297 /**
298 * Setup handlers for diff code lines to enable commenting
299 */
300 private setupDiffLineComments(): void {
Sean McCullough71941bd2025-04-18 13:31:48 -0700301 const diff2htmlContent =
302 this.shadowRoot?.getElementById("diff2htmlContent");
Sean McCullough86b56862025-04-18 13:04:03 -0700303 if (!diff2htmlContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700304
Sean McCullough86b56862025-04-18 13:04:03 -0700305 // Add plus buttons to each code line
306 this.addCommentButtonsToCodeLines();
Sean McCullough71941bd2025-04-18 13:31:48 -0700307
Sean McCullough86b56862025-04-18 13:04:03 -0700308 // Use event delegation for handling clicks on plus buttons
309 diff2htmlContent.addEventListener("click", (event) => {
310 const target = event.target as HTMLElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700311
Sean McCullough86b56862025-04-18 13:04:03 -0700312 // Only respond to clicks on the plus button
313 if (target.classList.contains("d2h-gutter-comment-button")) {
314 // Find the parent row first
315 const row = target.closest("tr");
316 if (!row) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700317
Sean McCullough86b56862025-04-18 13:04:03 -0700318 // Then find the code line in that row
Sean McCullough71941bd2025-04-18 13:31:48 -0700319 const codeLine =
320 row.querySelector(".d2h-code-side-line") ||
321 row.querySelector(".d2h-code-line");
Sean McCullough86b56862025-04-18 13:04:03 -0700322 if (!codeLine) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700323
Sean McCullough86b56862025-04-18 13:04:03 -0700324 // Get the line text content
325 const lineContent = codeLine.querySelector(".d2h-code-line-ctn");
326 if (!lineContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700327
Sean McCullough86b56862025-04-18 13:04:03 -0700328 const lineText = lineContent.textContent?.trim() || "";
Sean McCullough71941bd2025-04-18 13:31:48 -0700329
Sean McCullough86b56862025-04-18 13:04:03 -0700330 // Get file name to add context
331 const fileHeader = codeLine
332 .closest(".d2h-file-wrapper")
333 ?.querySelector(".d2h-file-name");
334 const fileName = fileHeader
335 ? fileHeader.textContent?.trim()
336 : "Unknown file";
Sean McCullough71941bd2025-04-18 13:31:48 -0700337
Sean McCullough86b56862025-04-18 13:04:03 -0700338 // Get line number if available
339 const lineNumElem = codeLine
340 .closest("tr")
341 ?.querySelector(".d2h-code-side-linenumber");
342 const lineNum = lineNumElem ? lineNumElem.textContent?.trim() : "";
343 const lineInfo = lineNum ? `Line ${lineNum}: ` : "";
Sean McCullough71941bd2025-04-18 13:31:48 -0700344
Sean McCullough86b56862025-04-18 13:04:03 -0700345 // Format the line for the comment box with file context and line number
346 const formattedLine = `${fileName} ${lineInfo}${lineText}`;
Sean McCullough71941bd2025-04-18 13:31:48 -0700347
Sean McCullough86b56862025-04-18 13:04:03 -0700348 console.log("Comment button clicked for line: ", formattedLine);
Sean McCullough71941bd2025-04-18 13:31:48 -0700349
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700350 // Open the comment box with this line and store the clicked element for positioning
351 this.clickedElement = target;
Sean McCullough86b56862025-04-18 13:04:03 -0700352 this.openDiffCommentBox(formattedLine);
Sean McCullough71941bd2025-04-18 13:31:48 -0700353
Sean McCullough86b56862025-04-18 13:04:03 -0700354 // Prevent event from bubbling up
355 event.stopPropagation();
356 }
357 });
358 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700359
Sean McCullough86b56862025-04-18 13:04:03 -0700360 /**
361 * Add plus buttons to each table row in the diff for commenting
362 */
363 private addCommentButtonsToCodeLines(): void {
Sean McCullough71941bd2025-04-18 13:31:48 -0700364 const diff2htmlContent =
365 this.shadowRoot?.getElementById("diff2htmlContent");
Sean McCullough86b56862025-04-18 13:04:03 -0700366 if (!diff2htmlContent) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700367
Sean McCullough86b56862025-04-18 13:04:03 -0700368 // Target code lines first, then find their parent rows
369 const codeLines = diff2htmlContent.querySelectorAll(
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700370 ".d2h-code-side-line, .d2h-code-line"
Sean McCullough86b56862025-04-18 13:04:03 -0700371 );
Sean McCullough71941bd2025-04-18 13:31:48 -0700372
Sean McCullough86b56862025-04-18 13:04:03 -0700373 // Create a Set to store unique rows to avoid duplicates
374 const rowsSet = new Set<HTMLElement>();
Sean McCullough71941bd2025-04-18 13:31:48 -0700375
Sean McCullough86b56862025-04-18 13:04:03 -0700376 // Get all rows that contain code lines
Sean McCullough71941bd2025-04-18 13:31:48 -0700377 codeLines.forEach((line) => {
378 const row = line.closest("tr");
Sean McCullough86b56862025-04-18 13:04:03 -0700379 if (row) rowsSet.add(row as HTMLElement);
380 });
Sean McCullough71941bd2025-04-18 13:31:48 -0700381
Sean McCullough86b56862025-04-18 13:04:03 -0700382 // Convert Set back to array for processing
383 const codeRows = Array.from(rowsSet);
Sean McCullough71941bd2025-04-18 13:31:48 -0700384
Sean McCullough86b56862025-04-18 13:04:03 -0700385 codeRows.forEach((row) => {
386 const rowElem = row as HTMLElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700387
Sean McCullough86b56862025-04-18 13:04:03 -0700388 // Skip info lines without actual code (e.g., "file added")
389 if (rowElem.querySelector(".d2h-info")) {
390 return;
391 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700392
Sean McCullough86b56862025-04-18 13:04:03 -0700393 // Find the code line number element (first TD in the row)
394 const lineNumberCell = rowElem.querySelector(
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700395 ".d2h-code-side-linenumber, .d2h-code-linenumber"
Sean McCullough86b56862025-04-18 13:04:03 -0700396 );
Sean McCullough71941bd2025-04-18 13:31:48 -0700397
Sean McCullough86b56862025-04-18 13:04:03 -0700398 if (!lineNumberCell) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700399
Sean McCullough86b56862025-04-18 13:04:03 -0700400 // Create the plus button
401 const plusButton = document.createElement("span");
402 plusButton.className = "d2h-gutter-comment-button";
403 plusButton.innerHTML = "+";
404 plusButton.title = "Add a comment on this line";
Sean McCullough71941bd2025-04-18 13:31:48 -0700405
Sean McCullough86b56862025-04-18 13:04:03 -0700406 // Add button to the line number cell for proper positioning
407 (lineNumberCell as HTMLElement).style.position = "relative"; // Ensure positioning context
408 lineNumberCell.appendChild(plusButton);
409 });
410 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700411
Sean McCullough86b56862025-04-18 13:04:03 -0700412 /**
413 * Open the comment box for a selected diff line
414 */
415 private openDiffCommentBox(lineText: string): void {
416 // Make sure the comment box div exists
417 const commentBoxId = "diffCommentBox";
418 let commentBox = this.shadowRoot?.getElementById(commentBoxId);
Sean McCullough71941bd2025-04-18 13:31:48 -0700419
Sean McCullough86b56862025-04-18 13:04:03 -0700420 // If it doesn't exist, create it
421 if (!commentBox) {
422 commentBox = document.createElement("div");
423 commentBox.id = commentBoxId;
424 commentBox.className = "diff-comment-box";
Sean McCullough71941bd2025-04-18 13:31:48 -0700425
Sean McCullough86b56862025-04-18 13:04:03 -0700426 // Create the comment box contents
427 commentBox.innerHTML = `
428 <h3>Add a comment</h3>
429 <div class="selected-line">
430 Line:
431 <pre id="selectedLine"></pre>
432 </div>
433 <textarea
434 id="diffCommentInput"
435 placeholder="Enter your comment about this line..."
436 ></textarea>
437 <div class="diff-comment-buttons">
438 <button id="cancelDiffComment">Cancel</button>
439 <button id="submitDiffComment">Add Comment</button>
440 </div>
441 `;
Sean McCullough71941bd2025-04-18 13:31:48 -0700442
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700443 // Append the comment box to the diff container to ensure proper positioning
444 const diffContainer = this.shadowRoot?.querySelector(".diff-container");
445 if (diffContainer) {
446 diffContainer.appendChild(commentBox);
447 } else {
448 this.shadowRoot?.appendChild(commentBox);
449 }
Sean McCullough86b56862025-04-18 13:04:03 -0700450 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700451
Sean McCullough86b56862025-04-18 13:04:03 -0700452 // Store the selected line
453 this.selectedDiffLine = lineText;
Sean McCullough71941bd2025-04-18 13:31:48 -0700454
Sean McCullough86b56862025-04-18 13:04:03 -0700455 // Display the line in the comment box
456 const selectedLine = this.shadowRoot?.getElementById("selectedLine");
457 if (selectedLine) {
458 selectedLine.textContent = lineText;
459 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700460
Sean McCullough86b56862025-04-18 13:04:03 -0700461 // Reset the comment input
462 const commentInput = this.shadowRoot?.getElementById(
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700463 "diffCommentInput"
Sean McCullough86b56862025-04-18 13:04:03 -0700464 ) as HTMLTextAreaElement;
465 if (commentInput) {
466 commentInput.value = "";
467 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700468
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700469 // Show the comment box and position it below the clicked line
470 if (commentBox && this.clickedElement) {
471 // Get the row that contains the clicked button
472 const row = this.clickedElement.closest("tr");
473 if (row) {
474 // Get the position of the row
475 const rowRect = row.getBoundingClientRect();
476 const diffContainerRect = this.shadowRoot?.querySelector(".diff-container")?.getBoundingClientRect();
477
478 if (diffContainerRect) {
479 // Position the comment box below the row
480 const topPosition = rowRect.bottom - diffContainerRect.top + this.shadowRoot!.querySelector(".diff-container")!.scrollTop;
481 const leftPosition = rowRect.left - diffContainerRect.left;
482
483 commentBox.style.top = `${topPosition}px`;
484 commentBox.style.left = `${leftPosition}px`;
485 commentBox.style.display = "block";
486 }
487 } else {
488 // Fallback if we can't find the row
489 commentBox.style.display = "block";
490 }
491 } else if (commentBox) {
492 // Fallback if we don't have clickedElement
Sean McCullough86b56862025-04-18 13:04:03 -0700493 commentBox.style.display = "block";
494 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700495
Sean McCullough86b56862025-04-18 13:04:03 -0700496 // Add event listeners for submit and cancel buttons
497 const submitButton = this.shadowRoot?.getElementById("submitDiffComment");
498 if (submitButton) {
499 submitButton.onclick = () => this.submitDiffComment();
500 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700501
Sean McCullough86b56862025-04-18 13:04:03 -0700502 const cancelButton = this.shadowRoot?.getElementById("cancelDiffComment");
503 if (cancelButton) {
504 cancelButton.onclick = () => this.closeDiffCommentBox();
505 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700506
Sean McCullough86b56862025-04-18 13:04:03 -0700507 // Focus on the comment input
508 if (commentInput) {
509 commentInput.focus();
510 }
511 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700512
Sean McCullough86b56862025-04-18 13:04:03 -0700513 /**
514 * Close the diff comment box without submitting
515 */
516 private closeDiffCommentBox(): void {
517 const commentBox = this.shadowRoot?.getElementById("diffCommentBox");
518 if (commentBox) {
519 commentBox.style.display = "none";
520 }
521 this.selectedDiffLine = null;
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700522 this.clickedElement = null;
Sean McCullough86b56862025-04-18 13:04:03 -0700523 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700524
Sean McCullough86b56862025-04-18 13:04:03 -0700525 /**
526 * Submit a comment on a diff line
527 */
528 private submitDiffComment(): void {
529 const commentInput = this.shadowRoot?.getElementById(
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700530 "diffCommentInput"
Sean McCullough86b56862025-04-18 13:04:03 -0700531 ) as HTMLTextAreaElement;
Sean McCullough71941bd2025-04-18 13:31:48 -0700532
Sean McCullough86b56862025-04-18 13:04:03 -0700533 if (!commentInput) return;
Sean McCullough71941bd2025-04-18 13:31:48 -0700534
Sean McCullough86b56862025-04-18 13:04:03 -0700535 const comment = commentInput.value.trim();
Sean McCullough71941bd2025-04-18 13:31:48 -0700536
Sean McCullough86b56862025-04-18 13:04:03 -0700537 // Validate inputs
538 if (!this.selectedDiffLine || !comment) {
539 alert("Please select a line and enter a comment.");
540 return;
541 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700542
Sean McCullough86b56862025-04-18 13:04:03 -0700543 // Format the comment in a readable way
544 const formattedComment = `\`\`\`\n${this.selectedDiffLine}\n\`\`\`\n\n${comment}`;
Sean McCullough71941bd2025-04-18 13:31:48 -0700545
Sean McCullough86b56862025-04-18 13:04:03 -0700546 // Dispatch a custom event with the formatted comment
Sean McCullough71941bd2025-04-18 13:31:48 -0700547 const event = new CustomEvent("diff-comment", {
Sean McCullough86b56862025-04-18 13:04:03 -0700548 detail: { comment: formattedComment },
549 bubbles: true,
Sean McCullough71941bd2025-04-18 13:31:48 -0700550 composed: true,
Sean McCullough86b56862025-04-18 13:04:03 -0700551 });
552 this.dispatchEvent(event);
Sean McCullough71941bd2025-04-18 13:31:48 -0700553
Sean McCullough86b56862025-04-18 13:04:03 -0700554 // Close only the comment box but keep the diff view open
555 this.closeDiffCommentBox();
556 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700557
Sean McCullough86b56862025-04-18 13:04:03 -0700558 // Clear the current state
559 public clearState(): void {
560 this.commitHash = "";
561 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700562
Sean McCullough86b56862025-04-18 13:04:03 -0700563 // Show diff for a specific commit
564 public showCommitDiff(commitHash: string): void {
565 // Store the commit hash
566 this.commitHash = commitHash;
567 // Load the diff content
568 this.loadDiffContent();
569 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700570
Sean McCullough86b56862025-04-18 13:04:03 -0700571 render() {
572 return html`
573 <div class="diff-view">
574 <div class="diff-container">
575 <div id="diff-view-controls">
576 <div class="diff-view-format">
577 <label>
Sean McCullough71941bd2025-04-18 13:31:48 -0700578 <input
579 type="radio"
580 name="diffViewFormat"
581 value="side-by-side"
Sean McCullough86b56862025-04-18 13:04:03 -0700582 ?checked=${this.viewFormat === "side-by-side"}
583 @change=${this.handleViewFormatChange}
Sean McCullough71941bd2025-04-18 13:31:48 -0700584 />
585 Side-by-side
Sean McCullough86b56862025-04-18 13:04:03 -0700586 </label>
587 <label>
Sean McCullough71941bd2025-04-18 13:31:48 -0700588 <input
589 type="radio"
590 name="diffViewFormat"
Sean McCullough86b56862025-04-18 13:04:03 -0700591 value="line-by-line"
592 ?checked=${this.viewFormat === "line-by-line"}
593 @change=${this.handleViewFormatChange}
Sean McCullough71941bd2025-04-18 13:31:48 -0700594 />
595 Line-by-line
Sean McCullough86b56862025-04-18 13:04:03 -0700596 </label>
597 </div>
598 </div>
599 <div id="diff2htmlContent" class="diff2html-content"></div>
600 </div>
601 </div>
602 `;
603 }
604}
605
606declare global {
607 interface HTMLElementTagNameMap {
608 "sketch-diff-view": SketchDiffView;
609 }
610}