blob: 69763a8ff494a07ce3d56c26e4356dde609ecbb8 [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 Crawshaw255dc432025-07-06 21:58:00 +000050 // Find the parent Monaco editor container (now using Tailwind classes)
Autoformatter9abf8032025-06-14 23:24:08 +000051 const fileDiffEditor = monacoView.closest(
David Crawshaw255dc432025-07-06 21:58:00 +000052 ".flex.flex-col.w-full.min-h-\\[200px\\].flex-1",
Autoformatter9abf8032025-06-14 23:24:08 +000053 ) 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) {
David Crawshaw255dc432025-07-06 21:58:00 +000064 // Update the container height to match monaco's height
David Crawshaw26f3f342025-06-14 19:58:32 +000065 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`
David Crawshaw255dc432025-07-06 21:58:00 +0000304 <div class="px-4 py-2 border-b border-gray-300 bg-gray-100 flex-shrink-0">
305 <div class="flex flex-col gap-3">
306 <div class="w-full flex items-center gap-3">
307 <sketch-diff-range-picker
308 class="flex-1 min-w-[400px]"
309 .gitService="${this.gitService}"
310 @range-change="${this.handleRangeChange}"
311 ></sketch-diff-range-picker>
312 <div class="flex-1"></div>
313 ${this.renderFileSelector()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700314 </div>
315 </div>
David Crawshaw255dc432025-07-06 21:58:00 +0000316 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700317
David Crawshaw255dc432025-07-06 21:58:00 +0000318 <div class="flex-1 overflow-auto flex flex-col min-h-0 relative h-full">
319 ${this.renderDiffContent()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700320 </div>
321 `;
322 }
323
David Crawshaw4cd01292025-06-15 18:59:13 +0000324 renderFileSelector() {
David Crawshaw5c6d8292025-06-15 19:09:19 +0000325 const fileCount = this.files.length;
Autoformatter62554112025-06-15 19:23:33 +0000326
David Crawshaw4cd01292025-06-15 18:59:13 +0000327 return html`
banksean54505842025-07-03 00:18:44 +0000328 <div class="flex items-center gap-2">
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700329 <select
banksean54505842025-07-03 00:18:44 +0000330 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 -0700331 .value="${this.selectedFile}"
332 @change="${this.handleFileSelection}"
333 ?disabled="${fileCount === 0}"
334 >
335 <option value="">All files (${fileCount})</option>
336 ${this.files.map(
337 (file) => html`
338 <option value="${file.path}">
339 ${this.getFileDisplayName(file)}
340 </option>
341 `,
342 )}
343 </select>
344 ${this.selectedFile ? this.renderSingleFileExpandButton() : ""}
345 </div>
David Crawshaw4cd01292025-06-15 18:59:13 +0000346 `;
347 }
348
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700349 renderDiffContent() {
350 if (this.loading) {
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000351 return html`<div class="flex items-center justify-center h-full">
352 Loading diff...
353 </div>`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700354 }
355
356 if (this.error) {
banksean54505842025-07-03 00:18:44 +0000357 return html`<div class="text-red-600 p-4">${this.error}</div>`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700358 }
359
360 if (this.files.length === 0) {
361 return html`<sketch-diff-empty-view></sketch-diff-empty-view>`;
362 }
Autoformatter8c463622025-05-16 21:54:17 +0000363
David Crawshaw4cd01292025-06-15 18:59:13 +0000364 // Render single file view if a specific file is selected
365 if (this.selectedFile && this.viewMode === "single") {
366 return this.renderSingleFileView();
367 }
368
369 // Render multi-file view
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700370 return html`
banksean54505842025-07-03 00:18:44 +0000371 <div class="flex flex-col w-full min-h-full">
David Crawshaw26f3f342025-06-14 19:58:32 +0000372 ${this.files.map((file, index) => this.renderFileDiff(file, index))}
373 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700374 `;
375 }
376
377 /**
378 * Load diff data for the current range
379 */
380 async loadDiffData() {
381 this.loading = true;
382 this.error = null;
383
384 try {
385 // Initialize files as empty array if undefined
386 if (!this.files) {
387 this.files = [];
388 }
389
David Crawshaw216d2fc2025-06-15 18:45:53 +0000390 // Load diff data for the range
391 this.files = await this.gitService.getDiff(
392 this.currentRange.from,
393 this.currentRange.to,
394 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700395
396 // Ensure files is always an array, even when API returns null
397 if (!this.files) {
398 this.files = [];
399 }
Autoformatter8c463622025-05-16 21:54:17 +0000400
David Crawshaw26f3f342025-06-14 19:58:32 +0000401 // Load content for all files
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700402 if (this.files.length > 0) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000403 // Initialize expand states for new files (default to collapsed)
Autoformatter9abf8032025-06-14 23:24:08 +0000404 this.files.forEach((file) => {
David Crawshaw26f3f342025-06-14 19:58:32 +0000405 if (!this.fileExpandStates.has(file.path)) {
406 this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions)
407 }
408 });
409 await this.loadAllFileContents();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700410 } else {
411 // No files to display - reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000412 this.selectedFilePath = "";
David Crawshaw4cd01292025-06-15 18:59:13 +0000413 this.selectedFile = "";
414 this.viewMode = "all";
David Crawshaw26f3f342025-06-14 19:58:32 +0000415 this.fileContents.clear();
416 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700417 }
418 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000419 console.error("Error loading diff data:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700420 this.error = `Error loading diff data: ${error.message}`;
421 // Ensure files is an empty array when an error occurs
422 this.files = [];
423 // Reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000424 this.selectedFilePath = "";
David Crawshaw4cd01292025-06-15 18:59:13 +0000425 this.selectedFile = "";
426 this.viewMode = "all";
David Crawshaw26f3f342025-06-14 19:58:32 +0000427 this.fileContents.clear();
428 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700429 } finally {
430 this.loading = false;
431 }
432 }
433
434 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000435 * Load content for all files in the diff
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700436 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000437 async loadAllFileContents() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700438 this.loading = true;
439 this.error = null;
David Crawshaw26f3f342025-06-14 19:58:32 +0000440 this.fileContents.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700441
442 try {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700443 let isUnstagedChanges = false;
Autoformatter8c463622025-05-16 21:54:17 +0000444
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700445 // Determine the commits to compare based on the current range
philip.zeyliger26bc6592025-06-30 20:15:30 -0700446 const _fromCommit = this.currentRange.from;
447 const toCommit = this.currentRange.to;
David Crawshaw216d2fc2025-06-15 18:45:53 +0000448 // Check if this is an unstaged changes view
449 isUnstagedChanges = toCommit === "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700450
David Crawshaw26f3f342025-06-14 19:58:32 +0000451 // Load content for all files
452 const promises = this.files.map(async (file) => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700453 try {
David Crawshaw26f3f342025-06-14 19:58:32 +0000454 let originalCode = "";
455 let modifiedCode = "";
456 let editable = isUnstagedChanges;
Autoformatter8c463622025-05-16 21:54:17 +0000457
David Crawshaw26f3f342025-06-14 19:58:32 +0000458 // Load the original code based on file status
459 if (file.status !== "A") {
460 // For modified, renamed, or deleted files: load original content
461 originalCode = await this.gitService.getFileContent(
462 file.old_hash || "",
463 );
464 }
465
466 // For modified code, always use working copy when editable
467 if (editable) {
468 try {
469 // Always use working copy when editable, regardless of diff status
470 modifiedCode = await this.gitService.getWorkingCopyContent(
471 file.path,
472 );
473 } catch (error) {
474 if (file.status === "D") {
475 // For deleted files, silently use empty content
476 console.warn(
477 `Could not get working copy for deleted file ${file.path}, using empty content`,
478 );
479 modifiedCode = "";
480 } else {
481 // For any other file status, propagate the error
482 console.error(
483 `Failed to get working copy for ${file.path}:`,
484 error,
485 );
486 throw error;
487 }
488 }
489 } else {
490 // For non-editable view, use git content based on file status
491 if (file.status === "D") {
492 // Deleted file: empty modified
493 modifiedCode = "";
494 } else {
495 // Added/modified/renamed: use the content from git
496 modifiedCode = await this.gitService.getFileContent(
497 file.new_hash || "",
498 );
499 }
500 }
501
502 // Don't make deleted files editable
503 if (file.status === "D") {
504 editable = false;
505 }
506
507 this.fileContents.set(file.path, {
508 original: originalCode,
509 modified: modifiedCode,
510 editable,
511 });
512 } catch (error) {
513 console.error(`Error loading content for file ${file.path}:`, error);
514 // Store empty content for failed files to prevent blocking
515 this.fileContents.set(file.path, {
516 original: "",
517 modified: "",
518 editable: false,
519 });
520 }
521 });
522
523 await Promise.all(promises);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700524 } catch (error) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000525 console.error("Error loading file contents:", error);
526 this.error = `Error loading file contents: ${error.message}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700527 } finally {
528 this.loading = false;
529 }
530 }
531
532 /**
533 * Handle range change event from the range picker
534 */
535 handleRangeChange(event: CustomEvent) {
536 const { range } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000537 console.log("Range changed:", range);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700538 this.currentRange = range;
Autoformatter8c463622025-05-16 21:54:17 +0000539
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700540 // Load diff data for the new range
541 this.loadDiffData();
542 }
543
544 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000545 * Render a single file diff section
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700546 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000547 renderFileDiff(file: GitDiffFile, index: number) {
548 const content = this.fileContents.get(file.path);
549 if (!content) {
550 return html`
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000551 <div
552 class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
553 >
554 <div
555 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"
556 >
557 ${this.renderFileHeader(file)}
558 </div>
559 <div class="flex items-center justify-center h-full">
560 Loading ${file.path}...
561 </div>
David Crawshaw26f3f342025-06-14 19:58:32 +0000562 </div>
563 `;
564 }
565
566 return html`
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000567 <div
568 class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
569 >
570 <div
571 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"
572 >
573 ${this.renderFileHeader(file)}
574 </div>
David Crawshaw255dc432025-07-06 21:58:00 +0000575 <div class="flex flex-col w-full min-h-[200px] flex-1">
David Crawshaw26f3f342025-06-14 19:58:32 +0000576 <sketch-monaco-view
banksean54505842025-07-03 00:18:44 +0000577 class="flex flex-col w-full min-h-[200px] flex-1"
David Crawshaw26f3f342025-06-14 19:58:32 +0000578 .originalCode="${content.original}"
579 .modifiedCode="${content.modified}"
580 .originalFilename="${file.path}"
581 .modifiedFilename="${file.path}"
582 ?readOnly="${!content.editable}"
583 ?editable-right="${content.editable}"
584 @monaco-comment="${this.handleMonacoComment}"
585 @monaco-save="${this.handleMonacoSave}"
586 @monaco-height-changed="${this.handleMonacoHeightChange}"
587 data-file-index="${index}"
588 data-file-path="${file.path}"
589 ></sketch-monaco-view>
590 </div>
591 </div>
592 `;
593 }
594
595 /**
596 * Render file header with status and path info
597 */
598 renderFileHeader(file: GitDiffFile) {
banksean54505842025-07-03 00:18:44 +0000599 const statusClasses = this.getFileStatusTailwindClasses(file.status);
David Crawshaw26f3f342025-06-14 19:58:32 +0000600 const statusText = this.getFileStatusText(file.status);
601 const changesInfo = this.getChangesInfo(file);
602 const pathInfo = this.getPathInfo(file);
603
604 const isExpanded = this.fileExpandStates.get(file.path) ?? false;
Autoformatter9abf8032025-06-14 23:24:08 +0000605
David Crawshaw26f3f342025-06-14 19:58:32 +0000606 return html`
banksean54505842025-07-03 00:18:44 +0000607 <div class="flex items-center gap-2">
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000608 <span
609 class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 ${statusClasses}"
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000610 >
David Crawshaw255dc432025-07-06 21:58:00 +0000611 ${statusText}
612 </span>
banksean54505842025-07-03 00:18:44 +0000613 <span class="font-mono font-normal text-gray-600">${pathInfo}</span>
Autoformatter9abf8032025-06-14 23:24:08 +0000614 ${changesInfo
banksean54505842025-07-03 00:18:44 +0000615 ? html`<span class="ml-2 text-xs text-gray-600">${changesInfo}</span>`
Autoformatter9abf8032025-06-14 23:24:08 +0000616 : ""}
David Crawshaw26f3f342025-06-14 19:58:32 +0000617 </div>
banksean54505842025-07-03 00:18:44 +0000618 <div class="flex items-center">
David Crawshaw26f3f342025-06-14 19:58:32 +0000619 <button
banksean54505842025-07-03 00:18:44 +0000620 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 +0000621 @click="${() => this.toggleFileExpansion(file.path)}"
622 title="${isExpanded
623 ? "Collapse: Hide unchanged regions to focus on changes"
624 : "Expand: Show all lines including unchanged regions"}"
625 >
Autoformatter9abf8032025-06-14 23:24:08 +0000626 ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
David Crawshaw26f3f342025-06-14 19:58:32 +0000627 </button>
628 </div>
629 `;
630 }
631
632 /**
banksean54505842025-07-03 00:18:44 +0000633 * Get Tailwind CSS classes for file status
David Crawshaw26f3f342025-06-14 19:58:32 +0000634 */
banksean54505842025-07-03 00:18:44 +0000635 getFileStatusTailwindClasses(status: string): string {
David Crawshaw26f3f342025-06-14 19:58:32 +0000636 switch (status.toUpperCase()) {
637 case "A":
banksean54505842025-07-03 00:18:44 +0000638 return "bg-green-100 text-green-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000639 case "M":
banksean54505842025-07-03 00:18:44 +0000640 return "bg-yellow-100 text-yellow-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000641 case "D":
banksean54505842025-07-03 00:18:44 +0000642 return "bg-red-100 text-red-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000643 case "R":
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000644 case "C":
David Crawshaw26f3f342025-06-14 19:58:32 +0000645 default:
646 if (status.toUpperCase().startsWith("R")) {
banksean54505842025-07-03 00:18:44 +0000647 return "bg-cyan-100 text-cyan-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000648 }
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000649 if (status.toUpperCase().startsWith("C")) {
banksean54505842025-07-03 00:18:44 +0000650 return "bg-indigo-100 text-indigo-800";
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000651 }
banksean54505842025-07-03 00:18:44 +0000652 return "bg-yellow-100 text-yellow-800";
David Crawshaw26f3f342025-06-14 19:58:32 +0000653 }
654 }
655
656 /**
657 * Get display text for file status
658 */
659 getFileStatusText(status: string): string {
660 switch (status.toUpperCase()) {
661 case "A":
662 return "Added";
663 case "M":
664 return "Modified";
665 case "D":
666 return "Deleted";
667 case "R":
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000668 case "C":
David Crawshaw26f3f342025-06-14 19:58:32 +0000669 default:
670 if (status.toUpperCase().startsWith("R")) {
671 return "Renamed";
672 }
Josh Bleecher Snyderb3aff882025-07-01 02:17:27 +0000673 if (status.toUpperCase().startsWith("C")) {
674 return "Copied";
675 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000676 return "Modified";
677 }
678 }
679
680 /**
681 * Get changes information (+/-) for display
682 */
683 getChangesInfo(file: GitDiffFile): string {
684 const additions = file.additions || 0;
685 const deletions = file.deletions || 0;
686
687 if (additions === 0 && deletions === 0) {
688 return "";
689 }
690
691 const parts = [];
692 if (additions > 0) {
693 parts.push(`+${additions}`);
694 }
695 if (deletions > 0) {
696 parts.push(`-${deletions}`);
697 }
698
699 return `(${parts.join(", ")})`;
700 }
701
702 /**
703 * Get path information for display, handling renames
704 */
705 getPathInfo(file: GitDiffFile): string {
706 if (file.old_path && file.old_path !== "") {
707 // For renames, show old_path → new_path
708 return `${file.old_path} → ${file.path}`;
709 }
710 // For regular files, just show the path
711 return file.path;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700712 }
713
714 /**
Philip Zeyligere89b3082025-05-29 03:16:06 +0000715 * Render expand all icon (dotted line with arrows pointing away)
716 */
717 renderExpandAllIcon() {
718 return html`
719 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
720 <!-- Dotted line in the middle -->
721 <line
722 x1="2"
723 y1="8"
724 x2="14"
725 y2="8"
726 stroke="currentColor"
727 stroke-width="1"
728 stroke-dasharray="2,1"
729 />
730 <!-- Large arrow pointing up -->
731 <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
732 <!-- Large arrow pointing down -->
733 <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
734 </svg>
735 `;
736 }
737
738 /**
739 * Render collapse icon (arrows pointing towards dotted line)
740 */
741 renderCollapseIcon() {
742 return html`
743 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
744 <!-- Dotted line in the middle -->
745 <line
746 x1="2"
747 y1="8"
748 x2="14"
749 y2="8"
750 stroke="currentColor"
751 stroke-width="1"
752 stroke-dasharray="2,1"
753 />
754 <!-- Large arrow pointing down towards line -->
755 <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
756 <!-- Large arrow pointing up towards line -->
757 <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
758 </svg>
759 `;
760 }
761
762 /**
David Crawshaw4cd01292025-06-15 18:59:13 +0000763 * Handle file selection change from the dropdown
764 */
765 handleFileSelection(event: Event) {
766 const selectElement = event.target as HTMLSelectElement;
767 const selectedValue = selectElement.value;
Autoformatter62554112025-06-15 19:23:33 +0000768
David Crawshaw4cd01292025-06-15 18:59:13 +0000769 this.selectedFile = selectedValue;
770 this.viewMode = selectedValue ? "single" : "all";
Autoformatter62554112025-06-15 19:23:33 +0000771
David Crawshaw4cd01292025-06-15 18:59:13 +0000772 // Force re-render
773 this.requestUpdate();
774 }
775
776 /**
777 * Get display name for file in the selector
778 */
779 getFileDisplayName(file: GitDiffFile): string {
780 const status = this.getFileStatusText(file.status);
781 const pathInfo = this.getPathInfo(file);
782 return `${status}: ${pathInfo}`;
783 }
784
785 /**
Philip Zeyliger38499cc2025-06-15 21:17:05 -0700786 * Render expand/collapse button for single file view in header
787 */
788 renderSingleFileExpandButton() {
789 if (!this.selectedFile) return "";
790
791 const isExpanded = this.fileExpandStates.get(this.selectedFile) ?? false;
792
793 return html`
794 <button
banksean54505842025-07-03 00:18:44 +0000795 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 -0700796 @click="${() => this.toggleFileExpansion(this.selectedFile)}"
797 title="${isExpanded
798 ? "Collapse: Hide unchanged regions to focus on changes"
799 : "Expand: Show all lines including unchanged regions"}"
800 >
801 ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
802 </button>
803 `;
804 }
805
806 /**
David Crawshaw4cd01292025-06-15 18:59:13 +0000807 * Render single file view with full-screen Monaco editor
808 */
809 renderSingleFileView() {
Autoformatter62554112025-06-15 19:23:33 +0000810 const selectedFileData = this.files.find(
811 (f) => f.path === this.selectedFile,
812 );
David Crawshaw4cd01292025-06-15 18:59:13 +0000813 if (!selectedFileData) {
banksean54505842025-07-03 00:18:44 +0000814 return html`<div class="text-red-600 p-4">Selected file not found</div>`;
David Crawshaw4cd01292025-06-15 18:59:13 +0000815 }
816
817 const content = this.fileContents.get(this.selectedFile);
818 if (!content) {
Autoformatter9f5cb2e2025-07-03 00:25:35 +0000819 return html`<div class="flex items-center justify-center h-full">
820 Loading ${this.selectedFile}...
821 </div>`;
David Crawshaw4cd01292025-06-15 18:59:13 +0000822 }
823
824 return html`
banksean54505842025-07-03 00:18:44 +0000825 <div class="flex-1 flex flex-col h-full min-h-0">
David Crawshaw4cd01292025-06-15 18:59:13 +0000826 <sketch-monaco-view
banksean54505842025-07-03 00:18:44 +0000827 class="flex-1 w-full h-full min-h-0"
David Crawshaw4cd01292025-06-15 18:59:13 +0000828 .originalCode="${content.original}"
829 .modifiedCode="${content.modified}"
830 .originalFilename="${selectedFileData.path}"
831 .modifiedFilename="${selectedFileData.path}"
832 ?readOnly="${!content.editable}"
833 ?editable-right="${content.editable}"
834 @monaco-comment="${this.handleMonacoComment}"
835 @monaco-save="${this.handleMonacoSave}"
836 data-file-path="${selectedFileData.path}"
837 ></sketch-monaco-view>
838 </div>
839 `;
840 }
841
842 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700843 * Refresh the diff view by reloading commits and diff data
Autoformatter8c463622025-05-16 21:54:17 +0000844 *
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700845 * This is called when the Monaco diff tab is activated to ensure:
846 * 1. Branch information from git/recentlog is current (branches can change frequently)
847 * 2. The diff content is synchronized with the latest repository state
848 * 3. Users always see up-to-date information without manual refresh
849 */
850 refreshDiffView() {
851 // First refresh the range picker to get updated branch information
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700852 const rangePicker = this.querySelector("sketch-diff-range-picker");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700853 if (rangePicker) {
854 (rangePicker as any).loadCommits();
855 }
Autoformatter8c463622025-05-16 21:54:17 +0000856
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700857 if (this.commit) {
David Crawshaw216d2fc2025-06-15 18:45:53 +0000858 // Convert single commit to range (commit^ to commit)
Autoformatter62554112025-06-15 19:23:33 +0000859 this.currentRange = {
860 type: "range",
861 from: `${this.commit}^`,
862 to: this.commit,
863 };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700864 }
Autoformatter8c463622025-05-16 21:54:17 +0000865
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700866 // Then reload diff data based on the current range
867 this.loadDiffData();
868 }
869}
870
871declare global {
872 interface HTMLElementTagNameMap {
873 "sketch-diff2-view": SketchDiff2View;
874 }
875}