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