blob: d2a8fbb58d7e39edac45f81c8adc7ecabb098425 [file] [log] [blame]
philip.zeyliger26bc6592025-06-30 20:15:30 -07001/* eslint-disable @typescript-eslint/no-explicit-any */
banksean54505842025-07-03 00:18:44 +00002import { html } from "lit";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07003import { customElement, property, state } from "lit/decorators.js";
banksean54505842025-07-03 00:18:44 +00004import { SketchTailwindElement } from "./sketch-tailwind-element.js";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07005import "./sketch-monaco-view";
6import "./sketch-diff-range-picker";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07007import "./sketch-diff-empty-view";
philip.zeyliger26bc6592025-06-30 20:15:30 -07008import { GitDiffFile, GitDataService } from "./git-data-service";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07009import { DiffRange } from "./sketch-diff-range-picker";
10
11/**
12 * A component that displays diffs using Monaco editor with range and file pickers
13 */
14@customElement("sketch-diff2-view")
banksean54505842025-07-03 00:18:44 +000015export class SketchDiff2View extends SketchTailwindElement {
Philip Zeyliger272a90e2025-05-16 14:49:51 -070016 /**
17 * Handles comment events from the Monaco editor and forwards them to the chat input
18 * using the same event format as the original diff view for consistency.
19 */
20 private handleMonacoComment(event: CustomEvent) {
21 try {
22 // Validate incoming data
23 if (!event.detail || !event.detail.formattedComment) {
Autoformatter8c463622025-05-16 21:54:17 +000024 console.error("Invalid comment data received");
Philip Zeyliger272a90e2025-05-16 14:49:51 -070025 return;
26 }
Autoformatter8c463622025-05-16 21:54:17 +000027
Philip Zeyliger272a90e2025-05-16 14:49:51 -070028 // Create and dispatch event using the standardized format
Autoformatter8c463622025-05-16 21:54:17 +000029 const commentEvent = new CustomEvent("diff-comment", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -070030 detail: { comment: event.detail.formattedComment },
31 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +000032 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -070033 });
Autoformatter8c463622025-05-16 21:54:17 +000034
Philip Zeyliger272a90e2025-05-16 14:49:51 -070035 this.dispatchEvent(commentEvent);
36 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +000037 console.error("Error handling Monaco comment:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -070038 }
39 }
Autoformatter8c463622025-05-16 21:54:17 +000040
Philip Zeyliger272a90e2025-05-16 14:49:51 -070041 /**
David Crawshaw26f3f342025-06-14 19:58:32 +000042 * Handle height change events from the Monaco editor
43 */
44 private handleMonacoHeightChange(event: CustomEvent) {
45 try {
46 // Get the monaco view that emitted the event
47 const monacoView = event.target as HTMLElement;
48 if (!monacoView) return;
Autoformatter9abf8032025-06-14 23:24:08 +000049
David Crawshaw26f3f342025-06-14 19:58:32 +000050 // Find the parent file-diff-editor container
Autoformatter9abf8032025-06-14 23:24:08 +000051 const fileDiffEditor = monacoView.closest(
52 ".file-diff-editor",
53 ) as HTMLElement;
David Crawshaw26f3f342025-06-14 19:58:32 +000054 if (!fileDiffEditor) return;
Autoformatter9abf8032025-06-14 23:24:08 +000055
David Crawshaw26f3f342025-06-14 19:58:32 +000056 // Get the new height from the event
57 const newHeight = event.detail.height;
Autoformatter9abf8032025-06-14 23:24:08 +000058
David Crawshaw26f3f342025-06-14 19:58:32 +000059 // Only update if the height actually changed to avoid unnecessary layout
60 const currentHeight = fileDiffEditor.style.height;
61 const newHeightStr = `${newHeight}px`;
Autoformatter9abf8032025-06-14 23:24:08 +000062
David Crawshaw26f3f342025-06-14 19:58:32 +000063 if (currentHeight !== newHeightStr) {
64 // Update the file-diff-editor height to match monaco's height
65 fileDiffEditor.style.height = newHeightStr;
Autoformatter9abf8032025-06-14 23:24:08 +000066
David Crawshaw26f3f342025-06-14 19:58:32 +000067 // Remove any previous min-height constraint that might interfere
Autoformatter9abf8032025-06-14 23:24:08 +000068 fileDiffEditor.style.minHeight = "auto";
69
David Crawshaw26f3f342025-06-14 19:58:32 +000070 // IMPORTANT: Tell Monaco to relayout after its container size changed
71 // Monaco has automaticLayout: false, so it won't detect container changes
72 setTimeout(() => {
73 const monacoComponent = monacoView as any;
74 if (monacoComponent && monacoComponent.editor) {
75 // Force layout with explicit dimensions to ensure Monaco fills the space
76 const editorWidth = fileDiffEditor.offsetWidth;
77 monacoComponent.editor.layout({
78 width: editorWidth,
Autoformatter9abf8032025-06-14 23:24:08 +000079 height: newHeight,
David Crawshaw26f3f342025-06-14 19:58:32 +000080 });
81 }
82 }, 0);
83 }
David Crawshaw26f3f342025-06-14 19:58:32 +000084 } catch (error) {
Autoformatter9abf8032025-06-14 23:24:08 +000085 console.error("Error handling Monaco height change:", error);
David Crawshaw26f3f342025-06-14 19:58:32 +000086 }
87 }
88
89 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -070090 * Handle save events from the Monaco editor
91 */
92 private async handleMonacoSave(event: CustomEvent) {
93 try {
94 // Validate incoming data
Autoformatter8c463622025-05-16 21:54:17 +000095 if (
96 !event.detail ||
97 !event.detail.path ||
98 event.detail.content === undefined
99 ) {
100 console.error("Invalid save data received");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700101 return;
102 }
Autoformatter8c463622025-05-16 21:54:17 +0000103
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700104 const { path, content } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000105
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700106 // Get Monaco view component
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700107 const monacoView = this.querySelector("sketch-monaco-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700108 if (!monacoView) {
Autoformatter8c463622025-05-16 21:54:17 +0000109 console.error("Monaco view not found");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700110 return;
111 }
Autoformatter8c463622025-05-16 21:54:17 +0000112
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700113 try {
114 await this.gitService?.saveFileContent(path, content);
115 console.log(`File saved: ${path}`);
116 (monacoView as any).notifySaveComplete(true);
117 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000118 console.error(
119 `Error saving file: ${error instanceof Error ? error.message : String(error)}`,
120 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700121 (monacoView as any).notifySaveComplete(false);
122 }
123 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000124 console.error("Error handling save:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700125 }
126 }
127 @property({ type: String })
128 initialCommit: string = "";
Autoformatter8c463622025-05-16 21:54:17 +0000129
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700130 // The commit to show - used when showing a specific commit from timeline
131 @property({ type: String })
132 commit: string = "";
133
134 @property({ type: String })
135 selectedFilePath: string = "";
136
137 @state()
138 private files: GitDiffFile[] = [];
Autoformatter8c463622025-05-16 21:54:17 +0000139
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700140 @state()
Autoformatter8c463622025-05-16 21:54:17 +0000141 private currentRange: DiffRange = { type: "range", from: "", to: "HEAD" };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700142
143 @state()
Autoformatter9abf8032025-06-14 23:24:08 +0000144 private fileContents: Map<
145 string,
146 { original: string; modified: string; editable: boolean }
147 > = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700148
149 @state()
David Crawshaw26f3f342025-06-14 19:58:32 +0000150 private fileExpandStates: Map<string, boolean> = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700151
152 @state()
153 private loading: boolean = false;
154
155 @state()
156 private error: string | null = null;
157
David Crawshaw4cd01292025-06-15 18:59:13 +0000158 @state()
159 private selectedFile: string = ""; // Empty string means "All files"
160
161 @state()
162 private viewMode: "all" | "single" = "all";
163
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700164 @property({ attribute: false, type: Object })
165 gitService!: GitDataService;
Autoformatter8c463622025-05-16 21:54:17 +0000166
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700167 // The gitService must be passed from parent to ensure proper dependency injection
168
169 constructor() {
170 super();
Autoformatter8c463622025-05-16 21:54:17 +0000171 console.log("SketchDiff2View initialized");
172
David Crawshawe2954ce2025-06-15 00:06:34 +0000173 // Fix for monaco-aria-container positioning and hide scrollbars globally
174 // Add a global style to ensure proper positioning of aria containers and hide scrollbars
Autoformatter8c463622025-05-16 21:54:17 +0000175 const styleElement = document.createElement("style");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700176 styleElement.textContent = `
177 .monaco-aria-container {
178 position: absolute !important;
179 top: 0 !important;
180 left: 0 !important;
181 width: 1px !important;
182 height: 1px !important;
183 overflow: hidden !important;
184 clip: rect(1px, 1px, 1px, 1px) !important;
185 white-space: nowrap !important;
186 margin: 0 !important;
187 padding: 0 !important;
188 border: 0 !important;
189 z-index: -1 !important;
190 }
Sean McCulloughdf234032025-07-02 20:45:29 -0700191
David Crawshawe2954ce2025-06-15 00:06:34 +0000192 /* Aggressively hide all Monaco scrollbar elements */
193 .monaco-editor .scrollbar,
194 .monaco-editor .scroll-decoration,
195 .monaco-editor .invisible.scrollbar,
196 .monaco-editor .slider,
197 .monaco-editor .vertical.scrollbar,
198 .monaco-editor .horizontal.scrollbar,
199 .monaco-diff-editor .scrollbar,
200 .monaco-diff-editor .scroll-decoration,
201 .monaco-diff-editor .invisible.scrollbar,
202 .monaco-diff-editor .slider,
203 .monaco-diff-editor .vertical.scrollbar,
204 .monaco-diff-editor .horizontal.scrollbar {
205 display: none !important;
206 visibility: hidden !important;
207 width: 0 !important;
208 height: 0 !important;
209 opacity: 0 !important;
210 }
Sean McCulloughdf234032025-07-02 20:45:29 -0700211
David Crawshawe2954ce2025-06-15 00:06:34 +0000212 /* Target the specific scrollbar classes that Monaco uses */
213 .monaco-scrollable-element > .scrollbar,
214 .monaco-scrollable-element > .scroll-decoration,
215 .monaco-scrollable-element .slider {
216 display: none !important;
217 visibility: hidden !important;
218 width: 0 !important;
219 height: 0 !important;
220 }
Sean McCulloughdf234032025-07-02 20:45:29 -0700221
David Crawshawe2954ce2025-06-15 00:06:34 +0000222 /* Remove scrollbar space/padding from content area */
223 .monaco-editor .monaco-scrollable-element,
224 .monaco-diff-editor .monaco-scrollable-element {
225 padding-right: 0 !important;
226 padding-bottom: 0 !important;
227 margin-right: 0 !important;
228 margin-bottom: 0 !important;
229 }
Sean McCulloughdf234032025-07-02 20:45:29 -0700230
David Crawshawe2954ce2025-06-15 00:06:34 +0000231 /* Ensure the diff content takes full width without scrollbar space */
232 .monaco-diff-editor .editor.modified,
233 .monaco-diff-editor .editor.original {
234 margin-right: 0 !important;
235 padding-right: 0 !important;
236 }
Sean McCulloughdf234032025-07-02 20:45:29 -0700237 `;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700238 document.head.appendChild(styleElement);
239 }
240
Sean McCulloughdf234032025-07-02 20:45:29 -0700241 // Override createRenderRoot to apply host styles for proper sizing while still using light DOM
242 createRenderRoot() {
243 // Use light DOM like SketchTailwindElement but still apply host styles
244 const style = document.createElement("style");
245 style.textContent = `
246 sketch-diff2-view {
247 height: -webkit-fill-available;
248 }
249 `;
250
251 // Add the style to the document head if not already present
252 if (!document.head.querySelector("style[data-sketch-diff2-view]")) {
253 style.setAttribute("data-sketch-diff2-view", "");
254 document.head.appendChild(style);
255 }
256
257 return this;
258 }
259
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700260 connectedCallback() {
261 super.connectedCallback();
262 // Initialize with default range and load data
263 // Get base commit if not set
Autoformatter8c463622025-05-16 21:54:17 +0000264 if (
265 this.currentRange.type === "range" &&
266 !("from" in this.currentRange && this.currentRange.from)
267 ) {
268 this.gitService
269 .getBaseCommitRef()
270 .then((baseRef) => {
271 this.currentRange = { type: "range", from: baseRef, to: "HEAD" };
272 this.loadDiffData();
273 })
274 .catch((error) => {
275 console.error("Error getting base commit ref:", error);
276 // Use default range
277 this.loadDiffData();
278 });
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700279 } else {
280 this.loadDiffData();
281 }
282 }
283
David Crawshaw26f3f342025-06-14 19:58:32 +0000284 // Toggle hideUnchangedRegions setting for a specific file
285 private toggleFileExpansion(filePath: string) {
286 const currentState = this.fileExpandStates.get(filePath) ?? false;
287 const newState = !currentState;
288 this.fileExpandStates.set(filePath, newState);
Autoformatter9abf8032025-06-14 23:24:08 +0000289
David Crawshaw26f3f342025-06-14 19:58:32 +0000290 // Apply to the specific Monaco view component for this file
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700291 const monacoView = this.querySelector(
Autoformatter9abf8032025-06-14 23:24:08 +0000292 `sketch-monaco-view[data-file-path="${filePath}"]`,
293 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700294 if (monacoView) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000295 (monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged"
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700296 }
Autoformatter9abf8032025-06-14 23:24:08 +0000297
David Crawshaw26f3f342025-06-14 19:58:32 +0000298 // Force a re-render to update the button state
299 this.requestUpdate();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700300 }
Autoformatter8c463622025-05-16 21:54:17 +0000301
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700302 render() {
303 return html`
banksean54505842025-07-03 00:18:44 +0000304 <div class="flex h-full flex-1 flex-col min-h-0 overflow-hidden relative">
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000305 <div
306 class="px-4 py-2 border-b border-gray-300 bg-gray-100 flex-shrink-0"
307 >
banksean54505842025-07-03 00:18:44 +0000308 <div class="flex flex-col gap-3">
309 <div class="w-full flex items-center gap-3">
310 <sketch-diff-range-picker
311 class="flex-1 min-w-[400px]"
312 .gitService="${this.gitService}"
313 @range-change="${this.handleRangeChange}"
314 ></sketch-diff-range-picker>
315 <div class="flex-1"></div>
316 ${this.renderFileSelector()}
317 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700318 </div>
319 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700320
banksean54505842025-07-03 00:18:44 +0000321 <div class="flex-1 overflow-auto flex flex-col min-h-0 relative h-full">
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000322 <div
323 class="flex-1 overflow-auto min-h-0 flex flex-col relative h-full"
324 >
325 ${this.renderDiffContent()}
326 </div>
banksean54505842025-07-03 00:18:44 +0000327 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700328 </div>
329 `;
330 }
331
David Crawshaw4cd01292025-06-15 18:59:13 +0000332 renderFileSelector() {
David Crawshaw5c6d8292025-06-15 19:09:19 +0000333 const fileCount = this.files.length;
Autoformatter62554112025-06-15 19:23:33 +0000334
David Crawshaw4cd01292025-06-15 18:59:13 +0000335 return html`
banksean54505842025-07-03 00:18:44 +0000336 <div class="flex items-center gap-2">
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700337 <select
banksean54505842025-07-03 00:18:44 +0000338 class="min-w-[200px] px-3 py-2 border border-gray-400 rounded bg-white text-sm cursor-pointer focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed"
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700339 .value="${this.selectedFile}"
340 @change="${this.handleFileSelection}"
341 ?disabled="${fileCount === 0}"
342 >
343 <option value="">All files (${fileCount})</option>
344 ${this.files.map(
345 (file) => html`
346 <option value="${file.path}">
347 ${this.getFileDisplayName(file)}
348 </option>
349 `,
350 )}
351 </select>
352 ${this.selectedFile ? this.renderSingleFileExpandButton() : ""}
353 </div>
David Crawshaw4cd01292025-06-15 18:59:13 +0000354 `;
355 }
356
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700357 renderDiffContent() {
358 if (this.loading) {
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000359 return html`<div class="flex items-center justify-center h-full">
360 Loading diff...
361 </div>`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700362 }
363
364 if (this.error) {
banksean54505842025-07-03 00:18:44 +0000365 return html`<div class="text-red-600 p-4">${this.error}</div>`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700366 }
367
368 if (this.files.length === 0) {
369 return html`<sketch-diff-empty-view></sketch-diff-empty-view>`;
370 }
Autoformatter8c463622025-05-16 21:54:17 +0000371
David Crawshaw4cd01292025-06-15 18:59:13 +0000372 // Render single file view if a specific file is selected
373 if (this.selectedFile && this.viewMode === "single") {
374 return this.renderSingleFileView();
375 }
376
377 // Render multi-file view
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700378 return html`
banksean54505842025-07-03 00:18:44 +0000379 <div class="flex flex-col w-full min-h-full">
David Crawshaw26f3f342025-06-14 19:58:32 +0000380 ${this.files.map((file, index) => this.renderFileDiff(file, index))}
381 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700382 `;
383 }
384
385 /**
386 * Load diff data for the current range
387 */
388 async loadDiffData() {
389 this.loading = true;
390 this.error = null;
391
392 try {
393 // Initialize files as empty array if undefined
394 if (!this.files) {
395 this.files = [];
396 }
397
David Crawshaw216d2fc2025-06-15 18:45:53 +0000398 // Load diff data for the range
399 this.files = await this.gitService.getDiff(
400 this.currentRange.from,
401 this.currentRange.to,
402 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700403
404 // Ensure files is always an array, even when API returns null
405 if (!this.files) {
406 this.files = [];
407 }
Autoformatter8c463622025-05-16 21:54:17 +0000408
David Crawshaw26f3f342025-06-14 19:58:32 +0000409 // Load content for all files
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700410 if (this.files.length > 0) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000411 // Initialize expand states for new files (default to collapsed)
Autoformatter9abf8032025-06-14 23:24:08 +0000412 this.files.forEach((file) => {
David Crawshaw26f3f342025-06-14 19:58:32 +0000413 if (!this.fileExpandStates.has(file.path)) {
414 this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions)
415 }
416 });
417 await this.loadAllFileContents();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700418 } else {
419 // No files to display - reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000420 this.selectedFilePath = "";
David Crawshaw4cd01292025-06-15 18:59:13 +0000421 this.selectedFile = "";
422 this.viewMode = "all";
David Crawshaw26f3f342025-06-14 19:58:32 +0000423 this.fileContents.clear();
424 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700425 }
426 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000427 console.error("Error loading diff data:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700428 this.error = `Error loading diff data: ${error.message}`;
429 // Ensure files is an empty array when an error occurs
430 this.files = [];
431 // Reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000432 this.selectedFilePath = "";
David Crawshaw4cd01292025-06-15 18:59:13 +0000433 this.selectedFile = "";
434 this.viewMode = "all";
David Crawshaw26f3f342025-06-14 19:58:32 +0000435 this.fileContents.clear();
436 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700437 } finally {
438 this.loading = false;
439 }
440 }
441
442 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000443 * Load content for all files in the diff
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700444 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000445 async loadAllFileContents() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700446 this.loading = true;
447 this.error = null;
David Crawshaw26f3f342025-06-14 19:58:32 +0000448 this.fileContents.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700449
450 try {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700451 let isUnstagedChanges = false;
Autoformatter8c463622025-05-16 21:54:17 +0000452
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700453 // Determine the commits to compare based on the current range
philip.zeyliger26bc6592025-06-30 20:15:30 -0700454 const _fromCommit = this.currentRange.from;
455 const toCommit = this.currentRange.to;
David Crawshaw216d2fc2025-06-15 18:45:53 +0000456 // Check if this is an unstaged changes view
457 isUnstagedChanges = toCommit === "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700458
David Crawshaw26f3f342025-06-14 19:58:32 +0000459 // Load content for all files
460 const promises = this.files.map(async (file) => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700461 try {
David Crawshaw26f3f342025-06-14 19:58:32 +0000462 let originalCode = "";
463 let modifiedCode = "";
464 let editable = isUnstagedChanges;
Autoformatter8c463622025-05-16 21:54:17 +0000465
David Crawshaw26f3f342025-06-14 19:58:32 +0000466 // Load the original code based on file status
467 if (file.status !== "A") {
468 // For modified, renamed, or deleted files: load original content
469 originalCode = await this.gitService.getFileContent(
470 file.old_hash || "",
471 );
472 }
473
474 // For modified code, always use working copy when editable
475 if (editable) {
476 try {
477 // Always use working copy when editable, regardless of diff status
478 modifiedCode = await this.gitService.getWorkingCopyContent(
479 file.path,
480 );
481 } catch (error) {
482 if (file.status === "D") {
483 // For deleted files, silently use empty content
484 console.warn(
485 `Could not get working copy for deleted file ${file.path}, using empty content`,
486 );
487 modifiedCode = "";
488 } else {
489 // For any other file status, propagate the error
490 console.error(
491 `Failed to get working copy for ${file.path}:`,
492 error,
493 );
494 throw error;
495 }
496 }
497 } else {
498 // For non-editable view, use git content based on file status
499 if (file.status === "D") {
500 // Deleted file: empty modified
501 modifiedCode = "";
502 } else {
503 // Added/modified/renamed: use the content from git
504 modifiedCode = await this.gitService.getFileContent(
505 file.new_hash || "",
506 );
507 }
508 }
509
510 // Don't make deleted files editable
511 if (file.status === "D") {
512 editable = false;
513 }
514
515 this.fileContents.set(file.path, {
516 original: originalCode,
517 modified: modifiedCode,
518 editable,
519 });
520 } catch (error) {
521 console.error(`Error loading content for file ${file.path}:`, error);
522 // Store empty content for failed files to prevent blocking
523 this.fileContents.set(file.path, {
524 original: "",
525 modified: "",
526 editable: false,
527 });
528 }
529 });
530
531 await Promise.all(promises);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700532 } catch (error) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000533 console.error("Error loading file contents:", error);
534 this.error = `Error loading file contents: ${error.message}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700535 } finally {
536 this.loading = false;
537 }
538 }
539
540 /**
541 * Handle range change event from the range picker
542 */
543 handleRangeChange(event: CustomEvent) {
544 const { range } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000545 console.log("Range changed:", range);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700546 this.currentRange = range;
Autoformatter8c463622025-05-16 21:54:17 +0000547
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700548 // Load diff data for the new range
549 this.loadDiffData();
550 }
551
552 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000553 * Render a single file diff section
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700554 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000555 renderFileDiff(file: GitDiffFile, index: number) {
556 const content = this.fileContents.get(file.path);
557 if (!content) {
558 return html`
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000559 <div
560 class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
561 >
562 <div
563 class="bg-gray-100 border-b border-gray-300 px-4 py-2 font-medium text-sm text-gray-800 sticky top-0 z-10 shadow-sm flex justify-between items-center"
564 >
565 ${this.renderFileHeader(file)}
566 </div>
567 <div class="flex items-center justify-center h-full">
568 Loading ${file.path}...
569 </div>
David Crawshaw26f3f342025-06-14 19:58:32 +0000570 </div>
571 `;
572 }
573
574 return html`
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000575 <div
576 class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
577 >
578 <div
579 class="bg-gray-100 border-b border-gray-300 px-4 py-2 font-medium text-sm text-gray-800 sticky top-0 z-10 shadow-sm flex justify-between items-center"
580 >
581 ${this.renderFileHeader(file)}
582 </div>
banksean54505842025-07-03 00:18:44 +0000583 <div class="flex flex-col min-h-[200px] overflow-visible">
David Crawshaw26f3f342025-06-14 19:58:32 +0000584 <sketch-monaco-view
banksean54505842025-07-03 00:18:44 +0000585 class="flex flex-col w-full min-h-[200px] flex-1"
David Crawshaw26f3f342025-06-14 19:58:32 +0000586 .originalCode="${content.original}"
587 .modifiedCode="${content.modified}"
588 .originalFilename="${file.path}"
589 .modifiedFilename="${file.path}"
590 ?readOnly="${!content.editable}"
591 ?editable-right="${content.editable}"
592 @monaco-comment="${this.handleMonacoComment}"
593 @monaco-save="${this.handleMonacoSave}"
594 @monaco-height-changed="${this.handleMonacoHeightChange}"
595 data-file-index="${index}"
596 data-file-path="${file.path}"
597 ></sketch-monaco-view>
598 </div>
599 </div>
600 `;
601 }
602
603 /**
604 * Render file header with status and path info
605 */
606 renderFileHeader(file: GitDiffFile) {
banksean54505842025-07-03 00:18:44 +0000607 const statusClasses = this.getFileStatusTailwindClasses(file.status);
David Crawshaw26f3f342025-06-14 19:58:32 +0000608 const statusText = this.getFileStatusText(file.status);
609 const changesInfo = this.getChangesInfo(file);
610 const pathInfo = this.getPathInfo(file);
611
612 const isExpanded = this.fileExpandStates.get(file.path) ?? false;
Autoformatter9abf8032025-06-14 23:24:08 +0000613
David Crawshaw26f3f342025-06-14 19:58:32 +0000614 return html`
banksean54505842025-07-03 00:18:44 +0000615 <div class="flex items-center gap-2">
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000616 <span
617 class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 ${statusClasses}"
618 >${statusText}</span
619 >
banksean54505842025-07-03 00:18:44 +0000620 <span class="font-mono font-normal text-gray-600">${pathInfo}</span>
Autoformatter9abf8032025-06-14 23:24:08 +0000621 ${changesInfo
banksean54505842025-07-03 00:18:44 +0000622 ? html`<span class="ml-2 text-xs text-gray-600">${changesInfo}</span>`
Autoformatter9abf8032025-06-14 23:24:08 +0000623 : ""}
David Crawshaw26f3f342025-06-14 19:58:32 +0000624 </div>
banksean54505842025-07-03 00:18:44 +0000625 <div class="flex items-center">
David Crawshaw26f3f342025-06-14 19:58:32 +0000626 <button
banksean54505842025-07-03 00:18:44 +0000627 class="bg-transparent border border-gray-300 rounded px-2 py-1 text-sm cursor-pointer whitespace-nowrap transition-colors duration-200 flex items-center justify-center min-w-8 min-h-8 hover:bg-gray-200"
David Crawshaw26f3f342025-06-14 19:58:32 +0000628 @click="${() => this.toggleFileExpansion(file.path)}"
629 title="${isExpanded
630 ? "Collapse: Hide unchanged regions to focus on changes"
631 : "Expand: Show all lines including unchanged regions"}"
632 >
Autoformatter9abf8032025-06-14 23:24:08 +0000633 ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
David Crawshaw26f3f342025-06-14 19:58:32 +0000634 </button>
635 </div>
636 `;
637 }
638
639 /**
banksean54505842025-07-03 00:18:44 +0000640 * Get Tailwind CSS classes for file status
David Crawshaw26f3f342025-06-14 19:58:32 +0000641 */
banksean54505842025-07-03 00:18:44 +0000642 getFileStatusTailwindClasses(status: string): string {
David Crawshaw26f3f342025-06-14 19:58:32 +0000643 switch (status.toUpperCase()) {
644 case "A":
banksean54505842025-07-03 00:18:44 +0000645 return "bg-green-100 text-green-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000646 case "M":
banksean54505842025-07-03 00:18:44 +0000647 return "bg-yellow-100 text-yellow-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000648 case "D":
banksean54505842025-07-03 00:18:44 +0000649 return "bg-red-100 text-red-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000650 case "R":
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000651 case "C":
David Crawshaw26f3f342025-06-14 19:58:32 +0000652 default:
653 if (status.toUpperCase().startsWith("R")) {
banksean54505842025-07-03 00:18:44 +0000654 return "bg-cyan-100 text-cyan-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000655 }
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000656 if (status.toUpperCase().startsWith("C")) {
banksean54505842025-07-03 00:18:44 +0000657 return "bg-indigo-100 text-indigo-800";
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000658 }
banksean54505842025-07-03 00:18:44 +0000659 return "bg-yellow-100 text-yellow-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000660 }
661 }
662
663 /**
664 * Get display text for file status
665 */
666 getFileStatusText(status: string): string {
667 switch (status.toUpperCase()) {
668 case "A":
669 return "Added";
670 case "M":
671 return "Modified";
672 case "D":
673 return "Deleted";
674 case "R":
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000675 case "C":
David Crawshaw26f3f342025-06-14 19:58:32 +0000676 default:
677 if (status.toUpperCase().startsWith("R")) {
678 return "Renamed";
679 }
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000680 if (status.toUpperCase().startsWith("C")) {
681 return "Copied";
682 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000683 return "Modified";
684 }
685 }
686
687 /**
688 * Get changes information (+/-) for display
689 */
690 getChangesInfo(file: GitDiffFile): string {
691 const additions = file.additions || 0;
692 const deletions = file.deletions || 0;
693
694 if (additions === 0 && deletions === 0) {
695 return "";
696 }
697
698 const parts = [];
699 if (additions > 0) {
700 parts.push(`+${additions}`);
701 }
702 if (deletions > 0) {
703 parts.push(`-${deletions}`);
704 }
705
706 return `(${parts.join(", ")})`;
707 }
708
709 /**
710 * Get path information for display, handling renames
711 */
712 getPathInfo(file: GitDiffFile): string {
713 if (file.old_path && file.old_path !== "") {
714 // For renames, show old_path → new_path
715 return `${file.old_path} → ${file.path}`;
716 }
717 // For regular files, just show the path
718 return file.path;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700719 }
720
721 /**
Philip Zeyligere89b3082025-05-29 03:16:06 +0000722 * Render expand all icon (dotted line with arrows pointing away)
723 */
724 renderExpandAllIcon() {
725 return html`
726 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
727 <!-- Dotted line in the middle -->
728 <line
729 x1="2"
730 y1="8"
731 x2="14"
732 y2="8"
733 stroke="currentColor"
734 stroke-width="1"
735 stroke-dasharray="2,1"
736 />
737 <!-- Large arrow pointing up -->
738 <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
739 <!-- Large arrow pointing down -->
740 <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
741 </svg>
742 `;
743 }
744
745 /**
746 * Render collapse icon (arrows pointing towards dotted line)
747 */
748 renderCollapseIcon() {
749 return html`
750 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
751 <!-- Dotted line in the middle -->
752 <line
753 x1="2"
754 y1="8"
755 x2="14"
756 y2="8"
757 stroke="currentColor"
758 stroke-width="1"
759 stroke-dasharray="2,1"
760 />
761 <!-- Large arrow pointing down towards line -->
762 <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
763 <!-- Large arrow pointing up towards line -->
764 <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
765 </svg>
766 `;
767 }
768
769 /**
David Crawshaw4cd01292025-06-15 18:59:13 +0000770 * Handle file selection change from the dropdown
771 */
772 handleFileSelection(event: Event) {
773 const selectElement = event.target as HTMLSelectElement;
774 const selectedValue = selectElement.value;
Autoformatter62554112025-06-15 19:23:33 +0000775
David Crawshaw4cd01292025-06-15 18:59:13 +0000776 this.selectedFile = selectedValue;
777 this.viewMode = selectedValue ? "single" : "all";
Autoformatter62554112025-06-15 19:23:33 +0000778
David Crawshaw4cd01292025-06-15 18:59:13 +0000779 // Force re-render
780 this.requestUpdate();
781 }
782
783 /**
784 * Get display name for file in the selector
785 */
786 getFileDisplayName(file: GitDiffFile): string {
787 const status = this.getFileStatusText(file.status);
788 const pathInfo = this.getPathInfo(file);
789 return `${status}: ${pathInfo}`;
790 }
791
792 /**
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700793 * Render expand/collapse button for single file view in header
794 */
795 renderSingleFileExpandButton() {
796 if (!this.selectedFile) return "";
797
798 const isExpanded = this.fileExpandStates.get(this.selectedFile) ?? false;
799
800 return html`
801 <button
banksean54505842025-07-03 00:18:44 +0000802 class="bg-transparent border border-gray-300 rounded px-1.5 py-1.5 text-sm cursor-pointer whitespace-nowrap transition-colors duration-200 flex items-center justify-center min-w-8 min-h-8 hover:bg-gray-200"
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700803 @click="${() => this.toggleFileExpansion(this.selectedFile)}"
804 title="${isExpanded
805 ? "Collapse: Hide unchanged regions to focus on changes"
806 : "Expand: Show all lines including unchanged regions"}"
807 >
808 ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
809 </button>
810 `;
811 }
812
813 /**
David Crawshaw4cd01292025-06-15 18:59:13 +0000814 * Render single file view with full-screen Monaco editor
815 */
816 renderSingleFileView() {
Autoformatter62554112025-06-15 19:23:33 +0000817 const selectedFileData = this.files.find(
818 (f) => f.path === this.selectedFile,
819 );
David Crawshaw4cd01292025-06-15 18:59:13 +0000820 if (!selectedFileData) {
banksean54505842025-07-03 00:18:44 +0000821 return html`<div class="text-red-600 p-4">Selected file not found</div>`;
David Crawshaw4cd01292025-06-15 18:59:13 +0000822 }
823
824 const content = this.fileContents.get(this.selectedFile);
825 if (!content) {
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000826 return html`<div class="flex items-center justify-center h-full">
827 Loading ${this.selectedFile}...
828 </div>`;
David Crawshaw4cd01292025-06-15 18:59:13 +0000829 }
830
831 return html`
banksean54505842025-07-03 00:18:44 +0000832 <div class="flex-1 flex flex-col h-full min-h-0">
David Crawshaw4cd01292025-06-15 18:59:13 +0000833 <sketch-monaco-view
banksean54505842025-07-03 00:18:44 +0000834 class="flex-1 w-full h-full min-h-0"
David Crawshaw4cd01292025-06-15 18:59:13 +0000835 .originalCode="${content.original}"
836 .modifiedCode="${content.modified}"
837 .originalFilename="${selectedFileData.path}"
838 .modifiedFilename="${selectedFileData.path}"
839 ?readOnly="${!content.editable}"
840 ?editable-right="${content.editable}"
841 @monaco-comment="${this.handleMonacoComment}"
842 @monaco-save="${this.handleMonacoSave}"
843 data-file-path="${selectedFileData.path}"
844 ></sketch-monaco-view>
845 </div>
846 `;
847 }
848
849 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700850 * Refresh the diff view by reloading commits and diff data
Autoformatter8c463622025-05-16 21:54:17 +0000851 *
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700852 * This is called when the Monaco diff tab is activated to ensure:
853 * 1. Branch information from git/recentlog is current (branches can change frequently)
854 * 2. The diff content is synchronized with the latest repository state
855 * 3. Users always see up-to-date information without manual refresh
856 */
857 refreshDiffView() {
858 // First refresh the range picker to get updated branch information
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700859 const rangePicker = this.querySelector("sketch-diff-range-picker");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700860 if (rangePicker) {
861 (rangePicker as any).loadCommits();
862 }
Autoformatter8c463622025-05-16 21:54:17 +0000863
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700864 if (this.commit) {
David Crawshaw216d2fc2025-06-15 18:45:53 +0000865 // Convert single commit to range (commit^ to commit)
Autoformatter62554112025-06-15 19:23:33 +0000866 this.currentRange = {
867 type: "range",
868 from: `${this.commit}^`,
869 to: this.commit,
870 };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700871 }
Autoformatter8c463622025-05-16 21:54:17 +0000872
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700873 // Then reload diff data based on the current range
874 this.loadDiffData();
875 }
876}
877
878declare global {
879 interface HTMLElementTagNameMap {
880 "sketch-diff2-view": SketchDiff2View;
881 }
882}