blob: f65593378b54a265ff92b736b87eac09cf5287d6 [file] [log] [blame]
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001import { css, html, LitElement } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
3import "./sketch-monaco-view";
4import "./sketch-diff-range-picker";
David Crawshaw26f3f342025-06-14 19:58:32 +00005// import "./sketch-diff-file-picker"; // No longer needed for multi-file view
Philip Zeyliger272a90e2025-05-16 14:49:51 -07006import "./sketch-diff-empty-view";
Autoformatter8c463622025-05-16 21:54:17 +00007import {
8 GitDiffFile,
9 GitDataService,
10 DefaultGitDataService,
11} from "./git-data-service";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070012import { DiffRange } from "./sketch-diff-range-picker";
13
14/**
15 * A component that displays diffs using Monaco editor with range and file pickers
16 */
17@customElement("sketch-diff2-view")
18export class SketchDiff2View extends LitElement {
19 /**
20 * Handles comment events from the Monaco editor and forwards them to the chat input
21 * using the same event format as the original diff view for consistency.
22 */
23 private handleMonacoComment(event: CustomEvent) {
24 try {
25 // Validate incoming data
26 if (!event.detail || !event.detail.formattedComment) {
Autoformatter8c463622025-05-16 21:54:17 +000027 console.error("Invalid comment data received");
Philip Zeyliger272a90e2025-05-16 14:49:51 -070028 return;
29 }
Autoformatter8c463622025-05-16 21:54:17 +000030
Philip Zeyliger272a90e2025-05-16 14:49:51 -070031 // Create and dispatch event using the standardized format
Autoformatter8c463622025-05-16 21:54:17 +000032 const commentEvent = new CustomEvent("diff-comment", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -070033 detail: { comment: event.detail.formattedComment },
34 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +000035 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -070036 });
Autoformatter8c463622025-05-16 21:54:17 +000037
Philip Zeyliger272a90e2025-05-16 14:49:51 -070038 this.dispatchEvent(commentEvent);
39 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +000040 console.error("Error handling Monaco comment:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -070041 }
42 }
Autoformatter8c463622025-05-16 21:54:17 +000043
Philip Zeyliger272a90e2025-05-16 14:49:51 -070044 /**
David Crawshaw26f3f342025-06-14 19:58:32 +000045 * Handle height change events from the Monaco editor
46 */
47 private handleMonacoHeightChange(event: CustomEvent) {
48 try {
49 // Get the monaco view that emitted the event
50 const monacoView = event.target as HTMLElement;
51 if (!monacoView) return;
Autoformatter9abf8032025-06-14 23:24:08 +000052
David Crawshaw26f3f342025-06-14 19:58:32 +000053 // Find the parent file-diff-editor container
Autoformatter9abf8032025-06-14 23:24:08 +000054 const fileDiffEditor = monacoView.closest(
55 ".file-diff-editor",
56 ) as HTMLElement;
David Crawshaw26f3f342025-06-14 19:58:32 +000057 if (!fileDiffEditor) return;
Autoformatter9abf8032025-06-14 23:24:08 +000058
David Crawshaw26f3f342025-06-14 19:58:32 +000059 // Get the new height from the event
60 const newHeight = event.detail.height;
Autoformatter9abf8032025-06-14 23:24:08 +000061
David Crawshaw26f3f342025-06-14 19:58:32 +000062 // Only update if the height actually changed to avoid unnecessary layout
63 const currentHeight = fileDiffEditor.style.height;
64 const newHeightStr = `${newHeight}px`;
Autoformatter9abf8032025-06-14 23:24:08 +000065
David Crawshaw26f3f342025-06-14 19:58:32 +000066 if (currentHeight !== newHeightStr) {
67 // Update the file-diff-editor height to match monaco's height
68 fileDiffEditor.style.height = newHeightStr;
Autoformatter9abf8032025-06-14 23:24:08 +000069
David Crawshaw26f3f342025-06-14 19:58:32 +000070 // Remove any previous min-height constraint that might interfere
Autoformatter9abf8032025-06-14 23:24:08 +000071 fileDiffEditor.style.minHeight = "auto";
72
David Crawshaw26f3f342025-06-14 19:58:32 +000073 // IMPORTANT: Tell Monaco to relayout after its container size changed
74 // Monaco has automaticLayout: false, so it won't detect container changes
75 setTimeout(() => {
76 const monacoComponent = monacoView as any;
77 if (monacoComponent && monacoComponent.editor) {
78 // Force layout with explicit dimensions to ensure Monaco fills the space
79 const editorWidth = fileDiffEditor.offsetWidth;
80 monacoComponent.editor.layout({
81 width: editorWidth,
Autoformatter9abf8032025-06-14 23:24:08 +000082 height: newHeight,
David Crawshaw26f3f342025-06-14 19:58:32 +000083 });
84 }
85 }, 0);
86 }
David Crawshaw26f3f342025-06-14 19:58:32 +000087 } catch (error) {
Autoformatter9abf8032025-06-14 23:24:08 +000088 console.error("Error handling Monaco height change:", error);
David Crawshaw26f3f342025-06-14 19:58:32 +000089 }
90 }
91
92 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -070093 * Handle save events from the Monaco editor
94 */
95 private async handleMonacoSave(event: CustomEvent) {
96 try {
97 // Validate incoming data
Autoformatter8c463622025-05-16 21:54:17 +000098 if (
99 !event.detail ||
100 !event.detail.path ||
101 event.detail.content === undefined
102 ) {
103 console.error("Invalid save data received");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700104 return;
105 }
Autoformatter8c463622025-05-16 21:54:17 +0000106
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700107 const { path, content } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000108
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700109 // Get Monaco view component
Autoformatter8c463622025-05-16 21:54:17 +0000110 const monacoView = this.shadowRoot?.querySelector("sketch-monaco-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700111 if (!monacoView) {
Autoformatter8c463622025-05-16 21:54:17 +0000112 console.error("Monaco view not found");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700113 return;
114 }
Autoformatter8c463622025-05-16 21:54:17 +0000115
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700116 try {
117 await this.gitService?.saveFileContent(path, content);
118 console.log(`File saved: ${path}`);
119 (monacoView as any).notifySaveComplete(true);
120 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000121 console.error(
122 `Error saving file: ${error instanceof Error ? error.message : String(error)}`,
123 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700124 (monacoView as any).notifySaveComplete(false);
125 }
126 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000127 console.error("Error handling save:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700128 }
129 }
130 @property({ type: String })
131 initialCommit: string = "";
Autoformatter8c463622025-05-16 21:54:17 +0000132
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700133 // The commit to show - used when showing a specific commit from timeline
134 @property({ type: String })
135 commit: string = "";
136
137 @property({ type: String })
138 selectedFilePath: string = "";
139
140 @state()
141 private files: GitDiffFile[] = [];
Autoformatter8c463622025-05-16 21:54:17 +0000142
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700143 @state()
Autoformatter8c463622025-05-16 21:54:17 +0000144 private currentRange: DiffRange = { type: "range", from: "", to: "HEAD" };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700145
146 @state()
Autoformatter9abf8032025-06-14 23:24:08 +0000147 private fileContents: Map<
148 string,
149 { original: string; modified: string; editable: boolean }
150 > = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700151
152 @state()
David Crawshaw26f3f342025-06-14 19:58:32 +0000153 private fileExpandStates: Map<string, boolean> = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700154
155 @state()
156 private loading: boolean = false;
157
158 @state()
159 private error: string | null = null;
160
161 static styles = css`
162 :host {
163 display: flex;
164 height: 100%;
165 flex: 1;
166 flex-direction: column;
167 min-height: 0; /* Critical for flex child behavior */
168 overflow: hidden;
169 position: relative; /* Establish positioning context */
170 }
171
172 .controls {
173 padding: 8px 16px;
174 border-bottom: 1px solid var(--border-color, #e0e0e0);
175 background-color: var(--background-light, #f8f8f8);
176 flex-shrink: 0; /* Prevent controls from shrinking */
177 }
Autoformatter8c463622025-05-16 21:54:17 +0000178
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700179 .controls-container {
180 display: flex;
181 flex-direction: column;
182 gap: 12px;
183 }
Autoformatter8c463622025-05-16 21:54:17 +0000184
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700185 .range-row {
186 width: 100%;
187 display: flex;
188 }
Autoformatter8c463622025-05-16 21:54:17 +0000189
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700190 .file-row {
191 width: 100%;
192 display: flex;
193 justify-content: space-between;
194 align-items: center;
195 gap: 10px;
196 }
Autoformatter8c463622025-05-16 21:54:17 +0000197
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700198 sketch-diff-range-picker {
199 width: 100%;
200 }
Autoformatter8c463622025-05-16 21:54:17 +0000201
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700202 sketch-diff-file-picker {
203 flex: 1;
204 }
Autoformatter8c463622025-05-16 21:54:17 +0000205
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700206 .view-toggle-button {
207 background-color: #f0f0f0;
208 border: 1px solid #ccc;
209 border-radius: 4px;
Philip Zeyligere89b3082025-05-29 03:16:06 +0000210 padding: 8px;
211 font-size: 16px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700212 cursor: pointer;
213 white-space: nowrap;
214 transition: background-color 0.2s;
Philip Zeyligere89b3082025-05-29 03:16:06 +0000215 display: flex;
216 align-items: center;
217 justify-content: center;
218 min-width: 36px;
219 min-height: 36px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700220 }
Autoformatter8c463622025-05-16 21:54:17 +0000221
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700222 .view-toggle-button:hover {
223 background-color: #e0e0e0;
224 }
225
226 .diff-container {
227 flex: 1;
David Crawshaw26f3f342025-06-14 19:58:32 +0000228 overflow: auto;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700229 display: flex;
230 flex-direction: column;
David Crawshaw26f3f342025-06-14 19:58:32 +0000231 min-height: 0;
232 position: relative;
233 height: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700234 }
235
236 .diff-content {
237 flex: 1;
David Crawshaw26f3f342025-06-14 19:58:32 +0000238 overflow: auto;
239 min-height: 0;
240 display: flex;
241 flex-direction: column;
242 position: relative;
243 height: 100%;
244 }
245
246 .multi-file-diff-container {
247 display: flex;
248 flex-direction: column;
249 width: 100%;
250 min-height: 100%;
251 }
252
253 .file-diff-section {
254 display: flex;
255 flex-direction: column;
256 border-bottom: 3px solid var(--border-color, #e0e0e0);
257 margin-bottom: 0;
258 }
259
260 .file-diff-section:last-child {
261 border-bottom: none;
262 }
263
264 .file-header {
265 background-color: var(--background-light, #f8f8f8);
266 border-bottom: 1px solid var(--border-color, #e0e0e0);
267 padding: 12px 16px;
268 font-family: var(--font-family, system-ui, sans-serif);
269 font-weight: 500;
270 font-size: 14px;
271 color: var(--text-primary-color, #333);
272 position: sticky;
273 top: 0;
274 z-index: 10;
275 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
276 display: flex;
277 justify-content: space-between;
278 align-items: center;
279 }
280
281 .file-header-left {
282 display: flex;
283 align-items: center;
284 gap: 8px;
285 }
286
287 .file-header-right {
288 display: flex;
289 align-items: center;
290 }
291
292 .file-expand-button {
293 background-color: transparent;
294 border: 1px solid var(--border-color, #e0e0e0);
295 border-radius: 4px;
296 padding: 4px 8px;
297 font-size: 14px;
298 cursor: pointer;
299 transition: background-color 0.2s;
300 display: flex;
301 align-items: center;
302 justify-content: center;
303 min-width: 32px;
304 min-height: 32px;
305 }
306
307 .file-expand-button:hover {
308 background-color: var(--background-hover, #e8e8e8);
309 }
310
311 .file-path {
312 font-family: monospace;
313 font-weight: normal;
314 color: var(--text-secondary-color, #666);
315 }
316
317 .file-status {
318 display: inline-block;
319 padding: 2px 6px;
320 border-radius: 3px;
321 font-size: 12px;
322 font-weight: bold;
323 margin-right: 8px;
324 }
325
326 .file-status.added {
327 background-color: #d4edda;
328 color: #155724;
329 }
330
331 .file-status.modified {
332 background-color: #fff3cd;
333 color: #856404;
334 }
335
336 .file-status.deleted {
337 background-color: #f8d7da;
338 color: #721c24;
339 }
340
341 .file-status.renamed {
342 background-color: #d1ecf1;
343 color: #0c5460;
344 }
345
346 .file-changes {
347 margin-left: 8px;
348 font-size: 12px;
349 color: var(--text-secondary-color, #666);
350 }
351
352 .file-diff-editor {
353 display: flex;
354 flex-direction: column;
355 min-height: 200px;
356 /* Height will be set dynamically by monaco editor */
357 overflow: visible; /* Ensure content is not clipped */
358 }
359
360 .file-count {
361 font-size: 14px;
362 color: var(--text-secondary-color, #666);
363 font-weight: 500;
364 padding: 8px 12px;
365 background-color: var(--background-light, #f8f8f8);
366 border-radius: 4px;
367 border: 1px solid var(--border-color, #e0e0e0);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700368 }
369
Autoformatter8c463622025-05-16 21:54:17 +0000370 .loading,
371 .empty-diff {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700372 display: flex;
373 align-items: center;
374 justify-content: center;
375 height: 100%;
376 font-family: var(--font-family, system-ui, sans-serif);
377 }
Autoformatter8c463622025-05-16 21:54:17 +0000378
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700379 .empty-diff {
380 color: var(--text-secondary-color, #666);
381 font-size: 16px;
382 text-align: center;
383 }
384
385 .error {
386 color: var(--error-color, #dc3545);
387 padding: 16px;
388 font-family: var(--font-family, system-ui, sans-serif);
389 }
390
391 sketch-monaco-view {
392 --editor-width: 100%;
393 --editor-height: 100%;
David Crawshaw26f3f342025-06-14 19:58:32 +0000394 display: flex;
395 flex-direction: column;
396 width: 100%;
397 min-height: 200px;
398 /* Ensure Monaco view takes full container space */
399 flex: 1;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700400 }
401 `;
402
403 @property({ attribute: false, type: Object })
404 gitService!: GitDataService;
Autoformatter8c463622025-05-16 21:54:17 +0000405
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700406 // The gitService must be passed from parent to ensure proper dependency injection
407
408 constructor() {
409 super();
Autoformatter8c463622025-05-16 21:54:17 +0000410 console.log("SketchDiff2View initialized");
411
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700412 // Fix for monaco-aria-container positioning
413 // Add a global style to ensure proper positioning of aria containers
Autoformatter8c463622025-05-16 21:54:17 +0000414 const styleElement = document.createElement("style");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700415 styleElement.textContent = `
416 .monaco-aria-container {
417 position: absolute !important;
418 top: 0 !important;
419 left: 0 !important;
420 width: 1px !important;
421 height: 1px !important;
422 overflow: hidden !important;
423 clip: rect(1px, 1px, 1px, 1px) !important;
424 white-space: nowrap !important;
425 margin: 0 !important;
426 padding: 0 !important;
427 border: 0 !important;
428 z-index: -1 !important;
429 }
430 `;
431 document.head.appendChild(styleElement);
432 }
433
434 connectedCallback() {
435 super.connectedCallback();
436 // Initialize with default range and load data
437 // Get base commit if not set
Autoformatter8c463622025-05-16 21:54:17 +0000438 if (
439 this.currentRange.type === "range" &&
440 !("from" in this.currentRange && this.currentRange.from)
441 ) {
442 this.gitService
443 .getBaseCommitRef()
444 .then((baseRef) => {
445 this.currentRange = { type: "range", from: baseRef, to: "HEAD" };
446 this.loadDiffData();
447 })
448 .catch((error) => {
449 console.error("Error getting base commit ref:", error);
450 // Use default range
451 this.loadDiffData();
452 });
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700453 } else {
454 this.loadDiffData();
455 }
456 }
457
David Crawshaw26f3f342025-06-14 19:58:32 +0000458 // Toggle hideUnchangedRegions setting for a specific file
459 private toggleFileExpansion(filePath: string) {
460 const currentState = this.fileExpandStates.get(filePath) ?? false;
461 const newState = !currentState;
462 this.fileExpandStates.set(filePath, newState);
Autoformatter9abf8032025-06-14 23:24:08 +0000463
David Crawshaw26f3f342025-06-14 19:58:32 +0000464 // Apply to the specific Monaco view component for this file
Autoformatter9abf8032025-06-14 23:24:08 +0000465 const monacoView = this.shadowRoot?.querySelector(
466 `sketch-monaco-view[data-file-path="${filePath}"]`,
467 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700468 if (monacoView) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000469 (monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged"
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700470 }
Autoformatter9abf8032025-06-14 23:24:08 +0000471
David Crawshaw26f3f342025-06-14 19:58:32 +0000472 // Force a re-render to update the button state
473 this.requestUpdate();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700474 }
Autoformatter8c463622025-05-16 21:54:17 +0000475
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700476 render() {
477 return html`
478 <div class="controls">
479 <div class="controls-container">
480 <div class="range-row">
481 <sketch-diff-range-picker
482 .gitService="${this.gitService}"
483 @range-change="${this.handleRangeChange}"
484 ></sketch-diff-range-picker>
485 </div>
Autoformatter8c463622025-05-16 21:54:17 +0000486
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700487 <div class="file-row">
David Crawshaw26f3f342025-06-14 19:58:32 +0000488 <div class="file-count">
Autoformatter9abf8032025-06-14 23:24:08 +0000489 ${this.files.length > 0
490 ? `${this.files.length} file${this.files.length === 1 ? "" : "s"} changed`
491 : "No files"}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700492 </div>
493 </div>
494 </div>
495 </div>
496
497 <div class="diff-container">
Autoformatter8c463622025-05-16 21:54:17 +0000498 <div class="diff-content">${this.renderDiffContent()}</div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700499 </div>
500 `;
501 }
502
503 renderDiffContent() {
504 if (this.loading) {
505 return html`<div class="loading">Loading diff...</div>`;
506 }
507
508 if (this.error) {
509 return html`<div class="error">${this.error}</div>`;
510 }
511
512 if (this.files.length === 0) {
513 return html`<sketch-diff-empty-view></sketch-diff-empty-view>`;
514 }
Autoformatter8c463622025-05-16 21:54:17 +0000515
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700516 return html`
David Crawshaw26f3f342025-06-14 19:58:32 +0000517 <div class="multi-file-diff-container">
518 ${this.files.map((file, index) => this.renderFileDiff(file, index))}
519 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700520 `;
521 }
522
523 /**
524 * Load diff data for the current range
525 */
526 async loadDiffData() {
527 this.loading = true;
528 this.error = null;
529
530 try {
531 // Initialize files as empty array if undefined
532 if (!this.files) {
533 this.files = [];
534 }
535
536 // Load diff data based on the current range type
Autoformatter8c463622025-05-16 21:54:17 +0000537 if (this.currentRange.type === "single") {
538 this.files = await this.gitService.getCommitDiff(
539 this.currentRange.commit,
540 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700541 } else {
Autoformatter8c463622025-05-16 21:54:17 +0000542 this.files = await this.gitService.getDiff(
543 this.currentRange.from,
544 this.currentRange.to,
545 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700546 }
547
548 // Ensure files is always an array, even when API returns null
549 if (!this.files) {
550 this.files = [];
551 }
Autoformatter8c463622025-05-16 21:54:17 +0000552
David Crawshaw26f3f342025-06-14 19:58:32 +0000553 // Load content for all files
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700554 if (this.files.length > 0) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000555 // Initialize expand states for new files (default to collapsed)
Autoformatter9abf8032025-06-14 23:24:08 +0000556 this.files.forEach((file) => {
David Crawshaw26f3f342025-06-14 19:58:32 +0000557 if (!this.fileExpandStates.has(file.path)) {
558 this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions)
559 }
560 });
561 await this.loadAllFileContents();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700562 } else {
563 // No files to display - reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000564 this.selectedFilePath = "";
David Crawshaw26f3f342025-06-14 19:58:32 +0000565 this.fileContents.clear();
566 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700567 }
568 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000569 console.error("Error loading diff data:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700570 this.error = `Error loading diff data: ${error.message}`;
571 // Ensure files is an empty array when an error occurs
572 this.files = [];
573 // Reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000574 this.selectedFilePath = "";
David Crawshaw26f3f342025-06-14 19:58:32 +0000575 this.fileContents.clear();
576 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700577 } finally {
578 this.loading = false;
579 }
580 }
581
582 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000583 * Load content for all files in the diff
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700584 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000585 async loadAllFileContents() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700586 this.loading = true;
587 this.error = null;
David Crawshaw26f3f342025-06-14 19:58:32 +0000588 this.fileContents.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700589
590 try {
591 let fromCommit: string;
592 let toCommit: string;
593 let isUnstagedChanges = false;
Autoformatter8c463622025-05-16 21:54:17 +0000594
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700595 // Determine the commits to compare based on the current range
Autoformatter8c463622025-05-16 21:54:17 +0000596 if (this.currentRange.type === "single") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700597 fromCommit = `${this.currentRange.commit}^`;
598 toCommit = this.currentRange.commit;
599 } else {
600 fromCommit = this.currentRange.from;
601 toCommit = this.currentRange.to;
602 // Check if this is an unstaged changes view
Autoformatter8c463622025-05-16 21:54:17 +0000603 isUnstagedChanges = toCommit === "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700604 }
605
David Crawshaw26f3f342025-06-14 19:58:32 +0000606 // Load content for all files
607 const promises = this.files.map(async (file) => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700608 try {
David Crawshaw26f3f342025-06-14 19:58:32 +0000609 let originalCode = "";
610 let modifiedCode = "";
611 let editable = isUnstagedChanges;
Autoformatter8c463622025-05-16 21:54:17 +0000612
David Crawshaw26f3f342025-06-14 19:58:32 +0000613 // Load the original code based on file status
614 if (file.status !== "A") {
615 // For modified, renamed, or deleted files: load original content
616 originalCode = await this.gitService.getFileContent(
617 file.old_hash || "",
618 );
619 }
620
621 // For modified code, always use working copy when editable
622 if (editable) {
623 try {
624 // Always use working copy when editable, regardless of diff status
625 modifiedCode = await this.gitService.getWorkingCopyContent(
626 file.path,
627 );
628 } catch (error) {
629 if (file.status === "D") {
630 // For deleted files, silently use empty content
631 console.warn(
632 `Could not get working copy for deleted file ${file.path}, using empty content`,
633 );
634 modifiedCode = "";
635 } else {
636 // For any other file status, propagate the error
637 console.error(
638 `Failed to get working copy for ${file.path}:`,
639 error,
640 );
641 throw error;
642 }
643 }
644 } else {
645 // For non-editable view, use git content based on file status
646 if (file.status === "D") {
647 // Deleted file: empty modified
648 modifiedCode = "";
649 } else {
650 // Added/modified/renamed: use the content from git
651 modifiedCode = await this.gitService.getFileContent(
652 file.new_hash || "",
653 );
654 }
655 }
656
657 // Don't make deleted files editable
658 if (file.status === "D") {
659 editable = false;
660 }
661
662 this.fileContents.set(file.path, {
663 original: originalCode,
664 modified: modifiedCode,
665 editable,
666 });
667 } catch (error) {
668 console.error(`Error loading content for file ${file.path}:`, error);
669 // Store empty content for failed files to prevent blocking
670 this.fileContents.set(file.path, {
671 original: "",
672 modified: "",
673 editable: false,
674 });
675 }
676 });
677
678 await Promise.all(promises);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700679 } catch (error) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000680 console.error("Error loading file contents:", error);
681 this.error = `Error loading file contents: ${error.message}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700682 } finally {
683 this.loading = false;
684 }
685 }
686
687 /**
688 * Handle range change event from the range picker
689 */
690 handleRangeChange(event: CustomEvent) {
691 const { range } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000692 console.log("Range changed:", range);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700693 this.currentRange = range;
Autoformatter8c463622025-05-16 21:54:17 +0000694
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700695 // Load diff data for the new range
696 this.loadDiffData();
697 }
698
699 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000700 * Render a single file diff section
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700701 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000702 renderFileDiff(file: GitDiffFile, index: number) {
703 const content = this.fileContents.get(file.path);
704 if (!content) {
705 return html`
706 <div class="file-diff-section">
Autoformatter9abf8032025-06-14 23:24:08 +0000707 <div class="file-header">${this.renderFileHeader(file)}</div>
David Crawshaw26f3f342025-06-14 19:58:32 +0000708 <div class="loading">Loading ${file.path}...</div>
709 </div>
710 `;
711 }
712
713 return html`
714 <div class="file-diff-section">
Autoformatter9abf8032025-06-14 23:24:08 +0000715 <div class="file-header">${this.renderFileHeader(file)}</div>
David Crawshaw26f3f342025-06-14 19:58:32 +0000716 <div class="file-diff-editor">
717 <sketch-monaco-view
718 .originalCode="${content.original}"
719 .modifiedCode="${content.modified}"
720 .originalFilename="${file.path}"
721 .modifiedFilename="${file.path}"
722 ?readOnly="${!content.editable}"
723 ?editable-right="${content.editable}"
724 @monaco-comment="${this.handleMonacoComment}"
725 @monaco-save="${this.handleMonacoSave}"
726 @monaco-height-changed="${this.handleMonacoHeightChange}"
727 data-file-index="${index}"
728 data-file-path="${file.path}"
729 ></sketch-monaco-view>
730 </div>
731 </div>
732 `;
733 }
734
735 /**
736 * Render file header with status and path info
737 */
738 renderFileHeader(file: GitDiffFile) {
739 const statusClass = this.getFileStatusClass(file.status);
740 const statusText = this.getFileStatusText(file.status);
741 const changesInfo = this.getChangesInfo(file);
742 const pathInfo = this.getPathInfo(file);
743
744 const isExpanded = this.fileExpandStates.get(file.path) ?? false;
Autoformatter9abf8032025-06-14 23:24:08 +0000745
David Crawshaw26f3f342025-06-14 19:58:32 +0000746 return html`
747 <div class="file-header-left">
748 <span class="file-status ${statusClass}">${statusText}</span>
749 <span class="file-path">${pathInfo}</span>
Autoformatter9abf8032025-06-14 23:24:08 +0000750 ${changesInfo
751 ? html`<span class="file-changes">${changesInfo}</span>`
752 : ""}
David Crawshaw26f3f342025-06-14 19:58:32 +0000753 </div>
754 <div class="file-header-right">
755 <button
756 class="file-expand-button"
757 @click="${() => this.toggleFileExpansion(file.path)}"
758 title="${isExpanded
759 ? "Collapse: Hide unchanged regions to focus on changes"
760 : "Expand: Show all lines including unchanged regions"}"
761 >
Autoformatter9abf8032025-06-14 23:24:08 +0000762 ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
David Crawshaw26f3f342025-06-14 19:58:32 +0000763 </button>
764 </div>
765 `;
766 }
767
768 /**
769 * Get CSS class for file status
770 */
771 getFileStatusClass(status: string): string {
772 switch (status.toUpperCase()) {
773 case "A":
774 return "added";
775 case "M":
776 return "modified";
777 case "D":
778 return "deleted";
779 case "R":
780 default:
781 if (status.toUpperCase().startsWith("R")) {
782 return "renamed";
783 }
784 return "modified";
785 }
786 }
787
788 /**
789 * Get display text for file status
790 */
791 getFileStatusText(status: string): string {
792 switch (status.toUpperCase()) {
793 case "A":
794 return "Added";
795 case "M":
796 return "Modified";
797 case "D":
798 return "Deleted";
799 case "R":
800 default:
801 if (status.toUpperCase().startsWith("R")) {
802 return "Renamed";
803 }
804 return "Modified";
805 }
806 }
807
808 /**
809 * Get changes information (+/-) for display
810 */
811 getChangesInfo(file: GitDiffFile): string {
812 const additions = file.additions || 0;
813 const deletions = file.deletions || 0;
814
815 if (additions === 0 && deletions === 0) {
816 return "";
817 }
818
819 const parts = [];
820 if (additions > 0) {
821 parts.push(`+${additions}`);
822 }
823 if (deletions > 0) {
824 parts.push(`-${deletions}`);
825 }
826
827 return `(${parts.join(", ")})`;
828 }
829
830 /**
831 * Get path information for display, handling renames
832 */
833 getPathInfo(file: GitDiffFile): string {
834 if (file.old_path && file.old_path !== "") {
835 // For renames, show old_path → new_path
836 return `${file.old_path} → ${file.path}`;
837 }
838 // For regular files, just show the path
839 return file.path;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700840 }
841
842 /**
Philip Zeyligere89b3082025-05-29 03:16:06 +0000843 * Render expand all icon (dotted line with arrows pointing away)
844 */
845 renderExpandAllIcon() {
846 return html`
847 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
848 <!-- Dotted line in the middle -->
849 <line
850 x1="2"
851 y1="8"
852 x2="14"
853 y2="8"
854 stroke="currentColor"
855 stroke-width="1"
856 stroke-dasharray="2,1"
857 />
858 <!-- Large arrow pointing up -->
859 <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
860 <!-- Large arrow pointing down -->
861 <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
862 </svg>
863 `;
864 }
865
866 /**
867 * Render collapse icon (arrows pointing towards dotted line)
868 */
869 renderCollapseIcon() {
870 return html`
871 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
872 <!-- Dotted line in the middle -->
873 <line
874 x1="2"
875 y1="8"
876 x2="14"
877 y2="8"
878 stroke="currentColor"
879 stroke-width="1"
880 stroke-dasharray="2,1"
881 />
882 <!-- Large arrow pointing down towards line -->
883 <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
884 <!-- Large arrow pointing up towards line -->
885 <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
886 </svg>
887 `;
888 }
889
890 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700891 * Refresh the diff view by reloading commits and diff data
Autoformatter8c463622025-05-16 21:54:17 +0000892 *
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700893 * This is called when the Monaco diff tab is activated to ensure:
894 * 1. Branch information from git/recentlog is current (branches can change frequently)
895 * 2. The diff content is synchronized with the latest repository state
896 * 3. Users always see up-to-date information without manual refresh
897 */
898 refreshDiffView() {
899 // First refresh the range picker to get updated branch information
Autoformatter8c463622025-05-16 21:54:17 +0000900 const rangePicker = this.shadowRoot?.querySelector(
901 "sketch-diff-range-picker",
902 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700903 if (rangePicker) {
904 (rangePicker as any).loadCommits();
905 }
Autoformatter8c463622025-05-16 21:54:17 +0000906
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700907 if (this.commit) {
Autoformatter8c463622025-05-16 21:54:17 +0000908 this.currentRange = { type: "single", commit: this.commit };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700909 }
Autoformatter8c463622025-05-16 21:54:17 +0000910
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700911 // Then reload diff data based on the current range
912 this.loadDiffData();
913 }
914}
915
916declare global {
917 interface HTMLElementTagNameMap {
918 "sketch-diff2-view": SketchDiff2View;
919 }
920}