blob: 1514fcd47b6619ec18bf8cbdd4d864972dec9441 [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;
52
53 // Find the parent file-diff-editor container
54 const fileDiffEditor = monacoView.closest('.file-diff-editor') as HTMLElement;
55 if (!fileDiffEditor) return;
56
57 // Get the new height from the event
58 const newHeight = event.detail.height;
59
60 // Only update if the height actually changed to avoid unnecessary layout
61 const currentHeight = fileDiffEditor.style.height;
62 const newHeightStr = `${newHeight}px`;
63
64 if (currentHeight !== newHeightStr) {
65 // Update the file-diff-editor height to match monaco's height
66 fileDiffEditor.style.height = newHeightStr;
67
68 // Remove any previous min-height constraint that might interfere
69 fileDiffEditor.style.minHeight = 'auto';
70
71 // IMPORTANT: Tell Monaco to relayout after its container size changed
72 // Monaco has automaticLayout: false, so it won't detect container changes
73 setTimeout(() => {
74 const monacoComponent = monacoView as any;
75 if (monacoComponent && monacoComponent.editor) {
76 // Force layout with explicit dimensions to ensure Monaco fills the space
77 const editorWidth = fileDiffEditor.offsetWidth;
78 monacoComponent.editor.layout({
79 width: editorWidth,
80 height: newHeight
81 });
82 }
83 }, 0);
84 }
85
86 } catch (error) {
87 console.error('Error handling Monaco height change:', error);
88 }
89 }
90
91 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -070092 * Handle save events from the Monaco editor
93 */
94 private async handleMonacoSave(event: CustomEvent) {
95 try {
96 // Validate incoming data
Autoformatter8c463622025-05-16 21:54:17 +000097 if (
98 !event.detail ||
99 !event.detail.path ||
100 event.detail.content === undefined
101 ) {
102 console.error("Invalid save data received");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700103 return;
104 }
Autoformatter8c463622025-05-16 21:54:17 +0000105
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700106 const { path, content } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000107
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700108 // Get Monaco view component
Autoformatter8c463622025-05-16 21:54:17 +0000109 const monacoView = this.shadowRoot?.querySelector("sketch-monaco-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700110 if (!monacoView) {
Autoformatter8c463622025-05-16 21:54:17 +0000111 console.error("Monaco view not found");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700112 return;
113 }
Autoformatter8c463622025-05-16 21:54:17 +0000114
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700115 try {
116 await this.gitService?.saveFileContent(path, content);
117 console.log(`File saved: ${path}`);
118 (monacoView as any).notifySaveComplete(true);
119 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000120 console.error(
121 `Error saving file: ${error instanceof Error ? error.message : String(error)}`,
122 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700123 (monacoView as any).notifySaveComplete(false);
124 }
125 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000126 console.error("Error handling save:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700127 }
128 }
129 @property({ type: String })
130 initialCommit: string = "";
Autoformatter8c463622025-05-16 21:54:17 +0000131
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700132 // The commit to show - used when showing a specific commit from timeline
133 @property({ type: String })
134 commit: string = "";
135
136 @property({ type: String })
137 selectedFilePath: string = "";
138
139 @state()
140 private files: GitDiffFile[] = [];
Autoformatter8c463622025-05-16 21:54:17 +0000141
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700142 @state()
Autoformatter8c463622025-05-16 21:54:17 +0000143 private currentRange: DiffRange = { type: "range", from: "", to: "HEAD" };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700144
145 @state()
David Crawshaw26f3f342025-06-14 19:58:32 +0000146 private fileContents: Map<string, { original: string; modified: string; editable: boolean }> = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700147
148 @state()
David Crawshaw26f3f342025-06-14 19:58:32 +0000149 private fileExpandStates: Map<string, boolean> = new Map();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700150
151 @state()
152 private loading: boolean = false;
153
154 @state()
155 private error: string | null = null;
156
157 static styles = css`
158 :host {
159 display: flex;
160 height: 100%;
161 flex: 1;
162 flex-direction: column;
163 min-height: 0; /* Critical for flex child behavior */
164 overflow: hidden;
165 position: relative; /* Establish positioning context */
166 }
167
168 .controls {
169 padding: 8px 16px;
170 border-bottom: 1px solid var(--border-color, #e0e0e0);
171 background-color: var(--background-light, #f8f8f8);
172 flex-shrink: 0; /* Prevent controls from shrinking */
173 }
Autoformatter8c463622025-05-16 21:54:17 +0000174
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700175 .controls-container {
176 display: flex;
177 flex-direction: column;
178 gap: 12px;
179 }
Autoformatter8c463622025-05-16 21:54:17 +0000180
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700181 .range-row {
182 width: 100%;
183 display: flex;
184 }
Autoformatter8c463622025-05-16 21:54:17 +0000185
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700186 .file-row {
187 width: 100%;
188 display: flex;
189 justify-content: space-between;
190 align-items: center;
191 gap: 10px;
192 }
Autoformatter8c463622025-05-16 21:54:17 +0000193
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700194 sketch-diff-range-picker {
195 width: 100%;
196 }
Autoformatter8c463622025-05-16 21:54:17 +0000197
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700198 sketch-diff-file-picker {
199 flex: 1;
200 }
Autoformatter8c463622025-05-16 21:54:17 +0000201
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700202 .view-toggle-button {
203 background-color: #f0f0f0;
204 border: 1px solid #ccc;
205 border-radius: 4px;
Philip Zeyligere89b3082025-05-29 03:16:06 +0000206 padding: 8px;
207 font-size: 16px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700208 cursor: pointer;
209 white-space: nowrap;
210 transition: background-color 0.2s;
Philip Zeyligere89b3082025-05-29 03:16:06 +0000211 display: flex;
212 align-items: center;
213 justify-content: center;
214 min-width: 36px;
215 min-height: 36px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700216 }
Autoformatter8c463622025-05-16 21:54:17 +0000217
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700218 .view-toggle-button:hover {
219 background-color: #e0e0e0;
220 }
221
222 .diff-container {
223 flex: 1;
David Crawshaw26f3f342025-06-14 19:58:32 +0000224 overflow: auto;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700225 display: flex;
226 flex-direction: column;
David Crawshaw26f3f342025-06-14 19:58:32 +0000227 min-height: 0;
228 position: relative;
229 height: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700230 }
231
232 .diff-content {
233 flex: 1;
David Crawshaw26f3f342025-06-14 19:58:32 +0000234 overflow: auto;
235 min-height: 0;
236 display: flex;
237 flex-direction: column;
238 position: relative;
239 height: 100%;
240 }
241
242 .multi-file-diff-container {
243 display: flex;
244 flex-direction: column;
245 width: 100%;
246 min-height: 100%;
247 }
248
249 .file-diff-section {
250 display: flex;
251 flex-direction: column;
252 border-bottom: 3px solid var(--border-color, #e0e0e0);
253 margin-bottom: 0;
254 }
255
256 .file-diff-section:last-child {
257 border-bottom: none;
258 }
259
260 .file-header {
261 background-color: var(--background-light, #f8f8f8);
262 border-bottom: 1px solid var(--border-color, #e0e0e0);
263 padding: 12px 16px;
264 font-family: var(--font-family, system-ui, sans-serif);
265 font-weight: 500;
266 font-size: 14px;
267 color: var(--text-primary-color, #333);
268 position: sticky;
269 top: 0;
270 z-index: 10;
271 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
272 display: flex;
273 justify-content: space-between;
274 align-items: center;
275 }
276
277 .file-header-left {
278 display: flex;
279 align-items: center;
280 gap: 8px;
281 }
282
283 .file-header-right {
284 display: flex;
285 align-items: center;
286 }
287
288 .file-expand-button {
289 background-color: transparent;
290 border: 1px solid var(--border-color, #e0e0e0);
291 border-radius: 4px;
292 padding: 4px 8px;
293 font-size: 14px;
294 cursor: pointer;
295 transition: background-color 0.2s;
296 display: flex;
297 align-items: center;
298 justify-content: center;
299 min-width: 32px;
300 min-height: 32px;
301 }
302
303 .file-expand-button:hover {
304 background-color: var(--background-hover, #e8e8e8);
305 }
306
307 .file-path {
308 font-family: monospace;
309 font-weight: normal;
310 color: var(--text-secondary-color, #666);
311 }
312
313 .file-status {
314 display: inline-block;
315 padding: 2px 6px;
316 border-radius: 3px;
317 font-size: 12px;
318 font-weight: bold;
319 margin-right: 8px;
320 }
321
322 .file-status.added {
323 background-color: #d4edda;
324 color: #155724;
325 }
326
327 .file-status.modified {
328 background-color: #fff3cd;
329 color: #856404;
330 }
331
332 .file-status.deleted {
333 background-color: #f8d7da;
334 color: #721c24;
335 }
336
337 .file-status.renamed {
338 background-color: #d1ecf1;
339 color: #0c5460;
340 }
341
342 .file-changes {
343 margin-left: 8px;
344 font-size: 12px;
345 color: var(--text-secondary-color, #666);
346 }
347
348 .file-diff-editor {
349 display: flex;
350 flex-direction: column;
351 min-height: 200px;
352 /* Height will be set dynamically by monaco editor */
353 overflow: visible; /* Ensure content is not clipped */
354 }
355
356 .file-count {
357 font-size: 14px;
358 color: var(--text-secondary-color, #666);
359 font-weight: 500;
360 padding: 8px 12px;
361 background-color: var(--background-light, #f8f8f8);
362 border-radius: 4px;
363 border: 1px solid var(--border-color, #e0e0e0);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700364 }
365
Autoformatter8c463622025-05-16 21:54:17 +0000366 .loading,
367 .empty-diff {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700368 display: flex;
369 align-items: center;
370 justify-content: center;
371 height: 100%;
372 font-family: var(--font-family, system-ui, sans-serif);
373 }
Autoformatter8c463622025-05-16 21:54:17 +0000374
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700375 .empty-diff {
376 color: var(--text-secondary-color, #666);
377 font-size: 16px;
378 text-align: center;
379 }
380
381 .error {
382 color: var(--error-color, #dc3545);
383 padding: 16px;
384 font-family: var(--font-family, system-ui, sans-serif);
385 }
386
387 sketch-monaco-view {
388 --editor-width: 100%;
389 --editor-height: 100%;
David Crawshaw26f3f342025-06-14 19:58:32 +0000390 display: flex;
391 flex-direction: column;
392 width: 100%;
393 min-height: 200px;
394 /* Ensure Monaco view takes full container space */
395 flex: 1;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700396 }
397 `;
398
399 @property({ attribute: false, type: Object })
400 gitService!: GitDataService;
Autoformatter8c463622025-05-16 21:54:17 +0000401
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700402 // The gitService must be passed from parent to ensure proper dependency injection
403
404 constructor() {
405 super();
Autoformatter8c463622025-05-16 21:54:17 +0000406 console.log("SketchDiff2View initialized");
407
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700408 // Fix for monaco-aria-container positioning
409 // Add a global style to ensure proper positioning of aria containers
Autoformatter8c463622025-05-16 21:54:17 +0000410 const styleElement = document.createElement("style");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700411 styleElement.textContent = `
412 .monaco-aria-container {
413 position: absolute !important;
414 top: 0 !important;
415 left: 0 !important;
416 width: 1px !important;
417 height: 1px !important;
418 overflow: hidden !important;
419 clip: rect(1px, 1px, 1px, 1px) !important;
420 white-space: nowrap !important;
421 margin: 0 !important;
422 padding: 0 !important;
423 border: 0 !important;
424 z-index: -1 !important;
425 }
426 `;
427 document.head.appendChild(styleElement);
428 }
429
430 connectedCallback() {
431 super.connectedCallback();
432 // Initialize with default range and load data
433 // Get base commit if not set
Autoformatter8c463622025-05-16 21:54:17 +0000434 if (
435 this.currentRange.type === "range" &&
436 !("from" in this.currentRange && this.currentRange.from)
437 ) {
438 this.gitService
439 .getBaseCommitRef()
440 .then((baseRef) => {
441 this.currentRange = { type: "range", from: baseRef, to: "HEAD" };
442 this.loadDiffData();
443 })
444 .catch((error) => {
445 console.error("Error getting base commit ref:", error);
446 // Use default range
447 this.loadDiffData();
448 });
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700449 } else {
450 this.loadDiffData();
451 }
452 }
453
David Crawshaw26f3f342025-06-14 19:58:32 +0000454 // Toggle hideUnchangedRegions setting for a specific file
455 private toggleFileExpansion(filePath: string) {
456 const currentState = this.fileExpandStates.get(filePath) ?? false;
457 const newState = !currentState;
458 this.fileExpandStates.set(filePath, newState);
459
460 // Apply to the specific Monaco view component for this file
461 const monacoView = this.shadowRoot?.querySelector(`sketch-monaco-view[data-file-path="${filePath}"]`);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700462 if (monacoView) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000463 (monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged"
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700464 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000465
466 // Force a re-render to update the button state
467 this.requestUpdate();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700468 }
Autoformatter8c463622025-05-16 21:54:17 +0000469
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700470 render() {
471 return html`
472 <div class="controls">
473 <div class="controls-container">
474 <div class="range-row">
475 <sketch-diff-range-picker
476 .gitService="${this.gitService}"
477 @range-change="${this.handleRangeChange}"
478 ></sketch-diff-range-picker>
479 </div>
Autoformatter8c463622025-05-16 21:54:17 +0000480
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700481 <div class="file-row">
David Crawshaw26f3f342025-06-14 19:58:32 +0000482 <div class="file-count">
483 ${this.files.length > 0 ? `${this.files.length} file${this.files.length === 1 ? '' : 's'} changed` : 'No files'}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700484 </div>
485 </div>
486 </div>
487 </div>
488
489 <div class="diff-container">
Autoformatter8c463622025-05-16 21:54:17 +0000490 <div class="diff-content">${this.renderDiffContent()}</div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700491 </div>
492 `;
493 }
494
495 renderDiffContent() {
496 if (this.loading) {
497 return html`<div class="loading">Loading diff...</div>`;
498 }
499
500 if (this.error) {
501 return html`<div class="error">${this.error}</div>`;
502 }
503
504 if (this.files.length === 0) {
505 return html`<sketch-diff-empty-view></sketch-diff-empty-view>`;
506 }
Autoformatter8c463622025-05-16 21:54:17 +0000507
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700508 return html`
David Crawshaw26f3f342025-06-14 19:58:32 +0000509 <div class="multi-file-diff-container">
510 ${this.files.map((file, index) => this.renderFileDiff(file, index))}
511 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700512 `;
513 }
514
515 /**
516 * Load diff data for the current range
517 */
518 async loadDiffData() {
519 this.loading = true;
520 this.error = null;
521
522 try {
523 // Initialize files as empty array if undefined
524 if (!this.files) {
525 this.files = [];
526 }
527
528 // Load diff data based on the current range type
Autoformatter8c463622025-05-16 21:54:17 +0000529 if (this.currentRange.type === "single") {
530 this.files = await this.gitService.getCommitDiff(
531 this.currentRange.commit,
532 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700533 } else {
Autoformatter8c463622025-05-16 21:54:17 +0000534 this.files = await this.gitService.getDiff(
535 this.currentRange.from,
536 this.currentRange.to,
537 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700538 }
539
540 // Ensure files is always an array, even when API returns null
541 if (!this.files) {
542 this.files = [];
543 }
Autoformatter8c463622025-05-16 21:54:17 +0000544
David Crawshaw26f3f342025-06-14 19:58:32 +0000545 // Load content for all files
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700546 if (this.files.length > 0) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000547 // Initialize expand states for new files (default to collapsed)
548 this.files.forEach(file => {
549 if (!this.fileExpandStates.has(file.path)) {
550 this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions)
551 }
552 });
553 await this.loadAllFileContents();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700554 } else {
555 // No files to display - reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000556 this.selectedFilePath = "";
David Crawshaw26f3f342025-06-14 19:58:32 +0000557 this.fileContents.clear();
558 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700559 }
560 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000561 console.error("Error loading diff data:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700562 this.error = `Error loading diff data: ${error.message}`;
563 // Ensure files is an empty array when an error occurs
564 this.files = [];
565 // Reset the view to initial state
Autoformatter8c463622025-05-16 21:54:17 +0000566 this.selectedFilePath = "";
David Crawshaw26f3f342025-06-14 19:58:32 +0000567 this.fileContents.clear();
568 this.fileExpandStates.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700569 } finally {
570 this.loading = false;
571 }
572 }
573
574 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000575 * Load content for all files in the diff
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700576 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000577 async loadAllFileContents() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700578 this.loading = true;
579 this.error = null;
David Crawshaw26f3f342025-06-14 19:58:32 +0000580 this.fileContents.clear();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700581
582 try {
583 let fromCommit: string;
584 let toCommit: string;
585 let isUnstagedChanges = false;
Autoformatter8c463622025-05-16 21:54:17 +0000586
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700587 // Determine the commits to compare based on the current range
Autoformatter8c463622025-05-16 21:54:17 +0000588 if (this.currentRange.type === "single") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700589 fromCommit = `${this.currentRange.commit}^`;
590 toCommit = this.currentRange.commit;
591 } else {
592 fromCommit = this.currentRange.from;
593 toCommit = this.currentRange.to;
594 // Check if this is an unstaged changes view
Autoformatter8c463622025-05-16 21:54:17 +0000595 isUnstagedChanges = toCommit === "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700596 }
597
David Crawshaw26f3f342025-06-14 19:58:32 +0000598 // Load content for all files
599 const promises = this.files.map(async (file) => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700600 try {
David Crawshaw26f3f342025-06-14 19:58:32 +0000601 let originalCode = "";
602 let modifiedCode = "";
603 let editable = isUnstagedChanges;
Autoformatter8c463622025-05-16 21:54:17 +0000604
David Crawshaw26f3f342025-06-14 19:58:32 +0000605 // Load the original code based on file status
606 if (file.status !== "A") {
607 // For modified, renamed, or deleted files: load original content
608 originalCode = await this.gitService.getFileContent(
609 file.old_hash || "",
610 );
611 }
612
613 // For modified code, always use working copy when editable
614 if (editable) {
615 try {
616 // Always use working copy when editable, regardless of diff status
617 modifiedCode = await this.gitService.getWorkingCopyContent(
618 file.path,
619 );
620 } catch (error) {
621 if (file.status === "D") {
622 // For deleted files, silently use empty content
623 console.warn(
624 `Could not get working copy for deleted file ${file.path}, using empty content`,
625 );
626 modifiedCode = "";
627 } else {
628 // For any other file status, propagate the error
629 console.error(
630 `Failed to get working copy for ${file.path}:`,
631 error,
632 );
633 throw error;
634 }
635 }
636 } else {
637 // For non-editable view, use git content based on file status
638 if (file.status === "D") {
639 // Deleted file: empty modified
640 modifiedCode = "";
641 } else {
642 // Added/modified/renamed: use the content from git
643 modifiedCode = await this.gitService.getFileContent(
644 file.new_hash || "",
645 );
646 }
647 }
648
649 // Don't make deleted files editable
650 if (file.status === "D") {
651 editable = false;
652 }
653
654 this.fileContents.set(file.path, {
655 original: originalCode,
656 modified: modifiedCode,
657 editable,
658 });
659 } catch (error) {
660 console.error(`Error loading content for file ${file.path}:`, error);
661 // Store empty content for failed files to prevent blocking
662 this.fileContents.set(file.path, {
663 original: "",
664 modified: "",
665 editable: false,
666 });
667 }
668 });
669
670 await Promise.all(promises);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700671 } catch (error) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000672 console.error("Error loading file contents:", error);
673 this.error = `Error loading file contents: ${error.message}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700674 } finally {
675 this.loading = false;
676 }
677 }
678
679 /**
680 * Handle range change event from the range picker
681 */
682 handleRangeChange(event: CustomEvent) {
683 const { range } = event.detail;
Autoformatter8c463622025-05-16 21:54:17 +0000684 console.log("Range changed:", range);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700685 this.currentRange = range;
Autoformatter8c463622025-05-16 21:54:17 +0000686
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700687 // Load diff data for the new range
688 this.loadDiffData();
689 }
690
691 /**
David Crawshaw26f3f342025-06-14 19:58:32 +0000692 * Render a single file diff section
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700693 */
David Crawshaw26f3f342025-06-14 19:58:32 +0000694 renderFileDiff(file: GitDiffFile, index: number) {
695 const content = this.fileContents.get(file.path);
696 if (!content) {
697 return html`
698 <div class="file-diff-section">
699 <div class="file-header">
700 ${this.renderFileHeader(file)}
701 </div>
702 <div class="loading">Loading ${file.path}...</div>
703 </div>
704 `;
705 }
706
707 return html`
708 <div class="file-diff-section">
709 <div class="file-header">
710 ${this.renderFileHeader(file)}
711 </div>
712 <div class="file-diff-editor">
713 <sketch-monaco-view
714 .originalCode="${content.original}"
715 .modifiedCode="${content.modified}"
716 .originalFilename="${file.path}"
717 .modifiedFilename="${file.path}"
718 ?readOnly="${!content.editable}"
719 ?editable-right="${content.editable}"
720 @monaco-comment="${this.handleMonacoComment}"
721 @monaco-save="${this.handleMonacoSave}"
722 @monaco-height-changed="${this.handleMonacoHeightChange}"
723 data-file-index="${index}"
724 data-file-path="${file.path}"
725 ></sketch-monaco-view>
726 </div>
727 </div>
728 `;
729 }
730
731 /**
732 * Render file header with status and path info
733 */
734 renderFileHeader(file: GitDiffFile) {
735 const statusClass = this.getFileStatusClass(file.status);
736 const statusText = this.getFileStatusText(file.status);
737 const changesInfo = this.getChangesInfo(file);
738 const pathInfo = this.getPathInfo(file);
739
740 const isExpanded = this.fileExpandStates.get(file.path) ?? false;
741
742 return html`
743 <div class="file-header-left">
744 <span class="file-status ${statusClass}">${statusText}</span>
745 <span class="file-path">${pathInfo}</span>
746 ${changesInfo ? html`<span class="file-changes">${changesInfo}</span>` : ''}
747 </div>
748 <div class="file-header-right">
749 <button
750 class="file-expand-button"
751 @click="${() => this.toggleFileExpansion(file.path)}"
752 title="${isExpanded
753 ? "Collapse: Hide unchanged regions to focus on changes"
754 : "Expand: Show all lines including unchanged regions"}"
755 >
756 ${isExpanded
757 ? this.renderCollapseIcon()
758 : this.renderExpandAllIcon()}
759 </button>
760 </div>
761 `;
762 }
763
764 /**
765 * Get CSS class for file status
766 */
767 getFileStatusClass(status: string): string {
768 switch (status.toUpperCase()) {
769 case "A":
770 return "added";
771 case "M":
772 return "modified";
773 case "D":
774 return "deleted";
775 case "R":
776 default:
777 if (status.toUpperCase().startsWith("R")) {
778 return "renamed";
779 }
780 return "modified";
781 }
782 }
783
784 /**
785 * Get display text for file status
786 */
787 getFileStatusText(status: string): string {
788 switch (status.toUpperCase()) {
789 case "A":
790 return "Added";
791 case "M":
792 return "Modified";
793 case "D":
794 return "Deleted";
795 case "R":
796 default:
797 if (status.toUpperCase().startsWith("R")) {
798 return "Renamed";
799 }
800 return "Modified";
801 }
802 }
803
804 /**
805 * Get changes information (+/-) for display
806 */
807 getChangesInfo(file: GitDiffFile): string {
808 const additions = file.additions || 0;
809 const deletions = file.deletions || 0;
810
811 if (additions === 0 && deletions === 0) {
812 return "";
813 }
814
815 const parts = [];
816 if (additions > 0) {
817 parts.push(`+${additions}`);
818 }
819 if (deletions > 0) {
820 parts.push(`-${deletions}`);
821 }
822
823 return `(${parts.join(", ")})`;
824 }
825
826 /**
827 * Get path information for display, handling renames
828 */
829 getPathInfo(file: GitDiffFile): string {
830 if (file.old_path && file.old_path !== "") {
831 // For renames, show old_path → new_path
832 return `${file.old_path} → ${file.path}`;
833 }
834 // For regular files, just show the path
835 return file.path;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700836 }
837
838 /**
Philip Zeyligere89b3082025-05-29 03:16:06 +0000839 * Render expand all icon (dotted line with arrows pointing away)
840 */
841 renderExpandAllIcon() {
842 return html`
843 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
844 <!-- Dotted line in the middle -->
845 <line
846 x1="2"
847 y1="8"
848 x2="14"
849 y2="8"
850 stroke="currentColor"
851 stroke-width="1"
852 stroke-dasharray="2,1"
853 />
854 <!-- Large arrow pointing up -->
855 <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
856 <!-- Large arrow pointing down -->
857 <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
858 </svg>
859 `;
860 }
861
862 /**
863 * Render collapse icon (arrows pointing towards dotted line)
864 */
865 renderCollapseIcon() {
866 return html`
867 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
868 <!-- Dotted line in the middle -->
869 <line
870 x1="2"
871 y1="8"
872 x2="14"
873 y2="8"
874 stroke="currentColor"
875 stroke-width="1"
876 stroke-dasharray="2,1"
877 />
878 <!-- Large arrow pointing down towards line -->
879 <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
880 <!-- Large arrow pointing up towards line -->
881 <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
882 </svg>
883 `;
884 }
885
886 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700887 * Refresh the diff view by reloading commits and diff data
Autoformatter8c463622025-05-16 21:54:17 +0000888 *
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700889 * This is called when the Monaco diff tab is activated to ensure:
890 * 1. Branch information from git/recentlog is current (branches can change frequently)
891 * 2. The diff content is synchronized with the latest repository state
892 * 3. Users always see up-to-date information without manual refresh
893 */
894 refreshDiffView() {
895 // First refresh the range picker to get updated branch information
Autoformatter8c463622025-05-16 21:54:17 +0000896 const rangePicker = this.shadowRoot?.querySelector(
897 "sketch-diff-range-picker",
898 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700899 if (rangePicker) {
900 (rangePicker as any).loadCommits();
901 }
Autoformatter8c463622025-05-16 21:54:17 +0000902
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700903 if (this.commit) {
Autoformatter8c463622025-05-16 21:54:17 +0000904 this.currentRange = { type: "single", commit: this.commit };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700905 }
Autoformatter8c463622025-05-16 21:54:17 +0000906
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700907 // Then reload diff data based on the current range
908 this.loadDiffData();
909 }
910}
911
912declare global {
913 interface HTMLElementTagNameMap {
914 "sketch-diff2-view": SketchDiff2View;
915 }
916}