blob: 6dcf19d6b9cc5687f2762fcc0c96d2644f32b953 [file] [log] [blame]
philip.zeyliger26bc6592025-06-30 20:15:30 -07001/* eslint-disable @typescript-eslint/no-explicit-any */
banksean23a35b82025-07-20 21:18:31 +00002import { html } from "lit";
philip.zeyliger6b8b7662025-06-16 03:06:30 +00003import { customElement, state } from "lit/decorators.js";
Autoformatterf964b502025-06-17 04:30:35 +00004import {
5 GitDiffFile,
6 GitDataService,
7 DefaultGitDataService,
8} from "./git-data-service";
philip.zeyliger6b8b7662025-06-16 03:06:30 +00009import "./sketch-monaco-view";
banksean23a35b82025-07-20 21:18:31 +000010import { SketchTailwindElement } from "./sketch-tailwind-element";
philip.zeyliger6b8b7662025-06-16 03:06:30 +000011
12@customElement("mobile-diff")
banksean23a35b82025-07-20 21:18:31 +000013export class MobileDiff extends SketchTailwindElement {
philip.zeyliger6b8b7662025-06-16 03:06:30 +000014 private gitService: GitDataService = new DefaultGitDataService();
15
16 @state()
17 private files: GitDiffFile[] = [];
18
19 @state()
Autoformatterf964b502025-06-17 04:30:35 +000020 private fileContents: Map<string, { original: string; modified: string }> =
21 new Map();
philip.zeyliger6b8b7662025-06-16 03:06:30 +000022
23 @state()
24 private loading: boolean = false;
25
26 @state()
27 private error: string | null = null;
28
29 @state()
30 private baseCommit: string = "";
31
32 @state()
33 private fileExpandStates: Map<string, boolean> = new Map();
34
philip.zeyliger6b8b7662025-06-16 03:06:30 +000035 connectedCallback() {
36 super.connectedCallback();
37 this.loadDiffData();
38 }
39
40 private async loadDiffData() {
41 this.loading = true;
42 this.error = null;
43 this.files = [];
44 this.fileContents.clear();
45
46 try {
47 // Get base commit reference
48 this.baseCommit = await this.gitService.getBaseCommitRef();
Autoformatterf964b502025-06-17 04:30:35 +000049
philip.zeyliger6b8b7662025-06-16 03:06:30 +000050 // Get diff from base commit to untracked changes (empty string for working directory)
51 this.files = await this.gitService.getDiff(this.baseCommit, "");
52
53 // Ensure files is always an array
54 if (!this.files) {
55 this.files = [];
56 }
57
58 if (this.files.length > 0) {
59 await this.loadAllFileContents();
60 }
61 } catch (error) {
62 console.error("Error loading diff data:", error);
63 this.error = `Error loading diff: ${error instanceof Error ? error.message : String(error)}`;
64 // Ensure files is always an array even on error
65 this.files = [];
66 } finally {
67 this.loading = false;
68 }
69 }
70
71 private async loadAllFileContents() {
72 try {
73 const promises = this.files.map(async (file) => {
74 try {
75 let originalCode = "";
76 let modifiedCode = "";
77
78 // Load original content (from the base commit)
79 if (file.status !== "A") {
80 // For modified, renamed, or deleted files: load original content
Autoformatterf964b502025-06-17 04:30:35 +000081 originalCode = await this.gitService.getFileContent(
82 file.old_hash || "",
83 );
philip.zeyliger6b8b7662025-06-16 03:06:30 +000084 }
85
86 // Load modified content (from working directory)
87 if (file.status === "D") {
88 // Deleted file: empty modified content
89 modifiedCode = "";
90 } else {
91 // Added/modified/renamed: use working copy content
92 try {
Autoformatterf964b502025-06-17 04:30:35 +000093 modifiedCode = await this.gitService.getWorkingCopyContent(
94 file.path,
95 );
philip.zeyliger6b8b7662025-06-16 03:06:30 +000096 } catch (error) {
Autoformatterf964b502025-06-17 04:30:35 +000097 console.warn(
98 `Could not get working copy for ${file.path}:`,
99 error,
100 );
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000101 modifiedCode = "";
102 }
103 }
104
105 this.fileContents.set(file.path, {
106 original: originalCode,
107 modified: modifiedCode,
108 });
109 } catch (error) {
110 console.error(`Error loading content for file ${file.path}:`, error);
111 // Store empty content for failed files
112 this.fileContents.set(file.path, {
113 original: "",
114 modified: "",
115 });
116 }
117 });
118
119 await Promise.all(promises);
120 } catch (error) {
121 console.error("Error loading file contents:", error);
122 throw error;
123 }
124 }
125
126 private getFileStatusClass(status: string): string {
127 switch (status.toUpperCase()) {
128 case "A":
129 return "added";
130 case "M":
131 return "modified";
132 case "D":
133 return "deleted";
134 case "R":
135 default:
136 if (status.toUpperCase().startsWith("R")) {
137 return "renamed";
138 }
139 return "modified";
140 }
141 }
142
banksean23a35b82025-07-20 21:18:31 +0000143 private getFileStatusTailwindClass(status: string): string {
144 switch (status.toUpperCase()) {
145 case "A":
146 return "bg-green-100 text-green-800";
147 case "M":
148 return "bg-yellow-100 text-yellow-800";
149 case "D":
150 return "bg-red-100 text-red-800";
151 case "R":
152 default:
153 if (status.toUpperCase().startsWith("R")) {
154 return "bg-blue-100 text-blue-800";
155 }
156 return "bg-yellow-100 text-yellow-800";
157 }
158 }
159
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000160 private getFileStatusText(status: string): string {
161 switch (status.toUpperCase()) {
162 case "A":
163 return "Added";
164 case "M":
165 return "Modified";
166 case "D":
167 return "Deleted";
168 case "R":
169 default:
170 if (status.toUpperCase().startsWith("R")) {
171 return "Renamed";
172 }
173 return "Modified";
174 }
175 }
176
177 private getChangesInfo(file: GitDiffFile): string {
178 const additions = file.additions || 0;
179 const deletions = file.deletions || 0;
180
181 if (additions === 0 && deletions === 0) {
182 return "";
183 }
184
185 const parts = [];
186 if (additions > 0) {
187 parts.push(`+${additions}`);
188 }
189 if (deletions > 0) {
190 parts.push(`-${deletions}`);
191 }
192
193 return `(${parts.join(", ")})`;
194 }
195
196 private getPathInfo(file: GitDiffFile): string {
197 if (file.old_path && file.old_path !== "") {
198 // For renames, show old_path → new_path
199 return `${file.old_path} → ${file.path}`;
200 }
201 // For regular files, just show the path
202 return file.path;
203 }
204
205 private toggleFileExpansion(filePath: string) {
206 const currentState = this.fileExpandStates.get(filePath) ?? false;
207 const newState = !currentState;
208 this.fileExpandStates.set(filePath, newState);
209
210 // Apply to the specific Monaco view component for this file
banksean44320562025-07-21 11:09:38 -0700211 const monacoView = this.querySelector(
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000212 `sketch-monaco-view[data-file-path="${filePath}"]`,
213 );
214 if (monacoView) {
215 (monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged"
216 }
217
218 // Force a re-render to update the button state
219 this.requestUpdate();
220 }
221
222 private renderExpandAllIcon() {
223 return html`
224 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
225 <!-- Dotted line in the middle -->
226 <line
227 x1="2"
228 y1="8"
229 x2="14"
230 y2="8"
231 stroke="currentColor"
232 stroke-width="1"
233 stroke-dasharray="2,1"
234 />
235 <!-- Large arrow pointing up -->
236 <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
237 <!-- Large arrow pointing down -->
238 <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
239 </svg>
240 `;
241 }
242
243 private renderCollapseIcon() {
244 return html`
245 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
246 <!-- Dotted line in the middle -->
247 <line
248 x1="2"
249 y1="8"
250 x2="14"
251 y2="8"
252 stroke="currentColor"
253 stroke-width="1"
254 stroke-dasharray="2,1"
255 />
256 <!-- Large arrow pointing down towards line -->
257 <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
258 <!-- Large arrow pointing up towards line -->
259 <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
260 </svg>
261 `;
262 }
263
264 private renderFileDiff(file: GitDiffFile) {
265 const content = this.fileContents.get(file.path);
266 const isExpanded = this.fileExpandStates.get(file.path) ?? false;
Autoformatterf964b502025-06-17 04:30:35 +0000267
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000268 if (!content) {
269 return html`
banksean23a35b82025-07-20 21:18:31 +0000270 <div class="mb-4 last:mb-0">
271 <div
272 class="bg-gray-50 border border-gray-200 border-b-0 p-3 font-mono text-sm font-medium text-gray-700 sticky top-0 z-10 flex items-center justify-between"
273 >
274 <div class="flex items-center">
275 <span
276 class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 font-sans ${this.getFileStatusTailwindClass(
277 file.status,
278 )}"
279 >
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000280 ${this.getFileStatusText(file.status)}
281 </span>
282 ${this.getPathInfo(file)}
Autoformatterf964b502025-06-17 04:30:35 +0000283 ${this.getChangesInfo(file)
banksean23a35b82025-07-20 21:18:31 +0000284 ? html`<span class="ml-2 text-xs text-gray-500"
Autoformatterf964b502025-06-17 04:30:35 +0000285 >${this.getChangesInfo(file)}</span
286 >`
287 : ""}
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000288 </div>
banksean23a35b82025-07-20 21:18:31 +0000289 <button class="text-gray-400" disabled>
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000290 ${this.renderExpandAllIcon()}
291 </button>
292 </div>
banksean23a35b82025-07-20 21:18:31 +0000293 <div
294 class="border border-gray-200 border-t-0 min-h-[200px] overflow-hidden bg-white"
295 >
296 <div
297 class="flex items-center justify-center h-full text-base text-gray-500 text-center p-5"
298 >
299 Loading ${file.path}...
300 </div>
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000301 </div>
302 </div>
303 `;
304 }
305
306 return html`
banksean23a35b82025-07-20 21:18:31 +0000307 <div class="mb-4 last:mb-0">
308 <div
309 class="bg-gray-50 border border-gray-200 border-b-0 p-3 font-mono text-sm font-medium text-gray-700 sticky top-0 z-10 flex items-center justify-between"
310 >
311 <div class="flex items-center">
312 <span
313 class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 font-sans ${this.getFileStatusTailwindClass(
314 file.status,
315 )}"
316 >
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000317 ${this.getFileStatusText(file.status)}
318 </span>
319 ${this.getPathInfo(file)}
Autoformatterf964b502025-06-17 04:30:35 +0000320 ${this.getChangesInfo(file)
banksean23a35b82025-07-20 21:18:31 +0000321 ? html`<span class="ml-2 text-xs text-gray-500"
Autoformatterf964b502025-06-17 04:30:35 +0000322 >${this.getChangesInfo(file)}</span
323 >`
324 : ""}
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000325 </div>
326 <button
banksean23a35b82025-07-20 21:18:31 +0000327 class="text-gray-600 hover:text-gray-800 p-1 rounded"
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000328 @click="${() => this.toggleFileExpansion(file.path)}"
329 title="${isExpanded
330 ? "Collapse: Hide unchanged regions to focus on changes"
331 : "Expand: Show all lines including unchanged regions"}"
332 >
Autoformatterf964b502025-06-17 04:30:35 +0000333 ${isExpanded
334 ? this.renderCollapseIcon()
335 : this.renderExpandAllIcon()}
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000336 </button>
337 </div>
banksean23a35b82025-07-20 21:18:31 +0000338 <div
339 class="border border-gray-200 border-t-0 min-h-[200px] overflow-hidden bg-white"
340 >
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000341 <sketch-monaco-view
banksean23a35b82025-07-20 21:18:31 +0000342 class="w-full min-h-[200px]"
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000343 .originalCode="${content.original}"
344 .modifiedCode="${content.modified}"
345 .originalFilename="${file.path}"
346 .modifiedFilename="${file.path}"
347 ?readOnly="true"
348 ?inline="true"
349 data-file-path="${file.path}"
350 ></sketch-monaco-view>
351 </div>
352 </div>
353 `;
354 }
355
356 render() {
357 return html`
banksean23a35b82025-07-20 21:18:31 +0000358 <div class="flex flex-col h-full min-h-0 overflow-hidden bg-white">
359 <div
360 class="flex-1 overflow-auto min-h-0"
361 style="-webkit-overflow-scrolling: touch;"
362 >
363 ${this.loading
364 ? html`<div
365 class="flex items-center justify-center h-full text-base text-gray-500 text-center p-5"
366 >
367 Loading diff...
368 </div>`
369 : this.error
370 ? html`<div
371 class="flex items-center justify-center h-full text-base text-red-600 text-center p-5"
372 >
373 ${this.error}
374 </div>`
375 : !this.files || this.files.length === 0
376 ? html`<div
377 class="flex items-center justify-center h-full text-base text-gray-500 text-center p-5"
378 >
379 No changes to show
380 </div>`
381 : this.files.map((file) => this.renderFileDiff(file))}
382 </div>
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000383 </div>
384 `;
385 }
386}
387
388declare global {
389 interface HTMLElementTagNameMap {
390 "mobile-diff": MobileDiff;
391 }
Autoformatterf964b502025-06-17 04:30:35 +0000392}