| philip.zeyliger | 26bc659 | 2025-06-30 20:15:30 -0700 | [diff] [blame] | 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 2 | import { html } from "lit"; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 3 | import { customElement, property, state } from "lit/decorators.js"; |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 4 | import { SketchTailwindElement } from "./sketch-tailwind-element.js"; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 5 | import "./sketch-monaco-view"; |
| 6 | import "./sketch-diff-range-picker"; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 7 | import "./sketch-diff-empty-view"; |
| philip.zeyliger | 26bc659 | 2025-06-30 20:15:30 -0700 | [diff] [blame] | 8 | import { GitDiffFile, GitDataService } from "./git-data-service"; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 9 | import { 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") |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 15 | export class SketchDiff2View extends SketchTailwindElement { |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 16 | /** |
| 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) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 24 | console.error("Invalid comment data received"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 25 | return; |
| 26 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 27 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 28 | // Create and dispatch event using the standardized format |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 29 | const commentEvent = new CustomEvent("diff-comment", { |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 30 | detail: { comment: event.detail.formattedComment }, |
| 31 | bubbles: true, |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 32 | composed: true, |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 33 | }); |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 34 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 35 | this.dispatchEvent(commentEvent); |
| 36 | } catch (error) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 37 | console.error("Error handling Monaco comment:", error); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 38 | } |
| 39 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 40 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 41 | /** |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 42 | * 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; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 49 | |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 50 | // Find the parent Monaco editor container (now using Tailwind classes) |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 51 | const fileDiffEditor = monacoView.closest( |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 52 | ".flex.flex-col.w-full.min-h-\\[200px\\].flex-1", |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 53 | ) as HTMLElement; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 54 | if (!fileDiffEditor) return; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 55 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 56 | // Get the new height from the event |
| 57 | const newHeight = event.detail.height; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 58 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 59 | // Only update if the height actually changed to avoid unnecessary layout |
| 60 | const currentHeight = fileDiffEditor.style.height; |
| 61 | const newHeightStr = `${newHeight}px`; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 62 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 63 | if (currentHeight !== newHeightStr) { |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 64 | // Update the container height to match monaco's height |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 65 | fileDiffEditor.style.height = newHeightStr; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 66 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 67 | // Remove any previous min-height constraint that might interfere |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 68 | fileDiffEditor.style.minHeight = "auto"; |
| 69 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 70 | // 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, |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 79 | height: newHeight, |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 80 | }); |
| 81 | } |
| 82 | }, 0); |
| 83 | } |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 84 | } catch (error) { |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 85 | console.error("Error handling Monaco height change:", error); |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 86 | } |
| 87 | } |
| 88 | |
| 89 | /** |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 90 | * Handle save events from the Monaco editor |
| 91 | */ |
| 92 | private async handleMonacoSave(event: CustomEvent) { |
| 93 | try { |
| 94 | // Validate incoming data |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 95 | if ( |
| 96 | !event.detail || |
| 97 | !event.detail.path || |
| 98 | event.detail.content === undefined |
| 99 | ) { |
| 100 | console.error("Invalid save data received"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 101 | return; |
| 102 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 103 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 104 | const { path, content } = event.detail; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 105 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 106 | // Get Monaco view component |
| Sean McCullough | f6e1dfe | 2025-07-03 14:59:40 -0700 | [diff] [blame] | 107 | const monacoView = this.querySelector("sketch-monaco-view"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 108 | if (!monacoView) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 109 | console.error("Monaco view not found"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 110 | return; |
| 111 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 112 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 113 | try { |
| 114 | await this.gitService?.saveFileContent(path, content); |
| 115 | console.log(`File saved: ${path}`); |
| 116 | (monacoView as any).notifySaveComplete(true); |
| 117 | } catch (error) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 118 | console.error( |
| 119 | `Error saving file: ${error instanceof Error ? error.message : String(error)}`, |
| 120 | ); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 121 | (monacoView as any).notifySaveComplete(false); |
| 122 | } |
| 123 | } catch (error) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 124 | console.error("Error handling save:", error); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 125 | } |
| 126 | } |
| 127 | @property({ type: String }) |
| 128 | initialCommit: string = ""; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 129 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 130 | // 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[] = []; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 139 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 140 | @state() |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 141 | private currentRange: DiffRange = { type: "range", from: "", to: "HEAD" }; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 142 | |
| 143 | @state() |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 144 | private fileContents: Map< |
| 145 | string, |
| 146 | { original: string; modified: string; editable: boolean } |
| 147 | > = new Map(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 148 | |
| 149 | @state() |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 150 | private fileExpandStates: Map<string, boolean> = new Map(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 151 | |
| 152 | @state() |
| 153 | private loading: boolean = false; |
| 154 | |
| 155 | @state() |
| 156 | private error: string | null = null; |
| 157 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 158 | @state() |
| 159 | private selectedFile: string = ""; // Empty string means "All files" |
| 160 | |
| 161 | @state() |
| 162 | private viewMode: "all" | "single" = "all"; |
| 163 | |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 164 | @state() |
| 165 | private untrackedFiles: string[] = []; |
| 166 | |
| 167 | @state() |
| 168 | private showUntrackedPopup: boolean = false; |
| 169 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 170 | @property({ attribute: false, type: Object }) |
| 171 | gitService!: GitDataService; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 172 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 173 | // The gitService must be passed from parent to ensure proper dependency injection |
| 174 | |
| 175 | constructor() { |
| 176 | super(); |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 177 | console.log("SketchDiff2View initialized"); |
| 178 | |
| David Crawshaw | e2954ce | 2025-06-15 00:06:34 +0000 | [diff] [blame] | 179 | // Fix for monaco-aria-container positioning and hide scrollbars globally |
| 180 | // Add a global style to ensure proper positioning of aria containers and hide scrollbars |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 181 | const styleElement = document.createElement("style"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 182 | styleElement.textContent = ` |
| 183 | .monaco-aria-container { |
| 184 | position: absolute !important; |
| 185 | top: 0 !important; |
| 186 | left: 0 !important; |
| 187 | width: 1px !important; |
| 188 | height: 1px !important; |
| 189 | overflow: hidden !important; |
| 190 | clip: rect(1px, 1px, 1px, 1px) !important; |
| 191 | white-space: nowrap !important; |
| 192 | margin: 0 !important; |
| 193 | padding: 0 !important; |
| 194 | border: 0 !important; |
| 195 | z-index: -1 !important; |
| 196 | } |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 197 | |
| David Crawshaw | e2954ce | 2025-06-15 00:06:34 +0000 | [diff] [blame] | 198 | /* Aggressively hide all Monaco scrollbar elements */ |
| 199 | .monaco-editor .scrollbar, |
| 200 | .monaco-editor .scroll-decoration, |
| 201 | .monaco-editor .invisible.scrollbar, |
| 202 | .monaco-editor .slider, |
| 203 | .monaco-editor .vertical.scrollbar, |
| 204 | .monaco-editor .horizontal.scrollbar, |
| 205 | .monaco-diff-editor .scrollbar, |
| 206 | .monaco-diff-editor .scroll-decoration, |
| 207 | .monaco-diff-editor .invisible.scrollbar, |
| 208 | .monaco-diff-editor .slider, |
| 209 | .monaco-diff-editor .vertical.scrollbar, |
| 210 | .monaco-diff-editor .horizontal.scrollbar { |
| 211 | display: none !important; |
| 212 | visibility: hidden !important; |
| 213 | width: 0 !important; |
| 214 | height: 0 !important; |
| 215 | opacity: 0 !important; |
| 216 | } |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 217 | |
| David Crawshaw | e2954ce | 2025-06-15 00:06:34 +0000 | [diff] [blame] | 218 | /* Target the specific scrollbar classes that Monaco uses */ |
| 219 | .monaco-scrollable-element > .scrollbar, |
| 220 | .monaco-scrollable-element > .scroll-decoration, |
| 221 | .monaco-scrollable-element .slider { |
| 222 | display: none !important; |
| 223 | visibility: hidden !important; |
| 224 | width: 0 !important; |
| 225 | height: 0 !important; |
| 226 | } |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 227 | |
| David Crawshaw | e2954ce | 2025-06-15 00:06:34 +0000 | [diff] [blame] | 228 | /* Remove scrollbar space/padding from content area */ |
| 229 | .monaco-editor .monaco-scrollable-element, |
| 230 | .monaco-diff-editor .monaco-scrollable-element { |
| 231 | padding-right: 0 !important; |
| 232 | padding-bottom: 0 !important; |
| 233 | margin-right: 0 !important; |
| 234 | margin-bottom: 0 !important; |
| 235 | } |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 236 | |
| David Crawshaw | e2954ce | 2025-06-15 00:06:34 +0000 | [diff] [blame] | 237 | /* Ensure the diff content takes full width without scrollbar space */ |
| 238 | .monaco-diff-editor .editor.modified, |
| 239 | .monaco-diff-editor .editor.original { |
| 240 | margin-right: 0 !important; |
| 241 | padding-right: 0 !important; |
| 242 | } |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 243 | `; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 244 | document.head.appendChild(styleElement); |
| 245 | } |
| 246 | |
| Sean McCullough | df23403 | 2025-07-02 20:45:29 -0700 | [diff] [blame] | 247 | // Override createRenderRoot to apply host styles for proper sizing while still using light DOM |
| 248 | createRenderRoot() { |
| 249 | // Use light DOM like SketchTailwindElement but still apply host styles |
| 250 | const style = document.createElement("style"); |
| 251 | style.textContent = ` |
| 252 | sketch-diff2-view { |
| 253 | height: -webkit-fill-available; |
| 254 | } |
| 255 | `; |
| 256 | |
| 257 | // Add the style to the document head if not already present |
| 258 | if (!document.head.querySelector("style[data-sketch-diff2-view]")) { |
| 259 | style.setAttribute("data-sketch-diff2-view", ""); |
| 260 | document.head.appendChild(style); |
| 261 | } |
| 262 | |
| 263 | return this; |
| 264 | } |
| 265 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 266 | connectedCallback() { |
| 267 | super.connectedCallback(); |
| 268 | // Initialize with default range and load data |
| 269 | // Get base commit if not set |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 270 | if ( |
| 271 | this.currentRange.type === "range" && |
| 272 | !("from" in this.currentRange && this.currentRange.from) |
| 273 | ) { |
| 274 | this.gitService |
| 275 | .getBaseCommitRef() |
| 276 | .then((baseRef) => { |
| 277 | this.currentRange = { type: "range", from: baseRef, to: "HEAD" }; |
| 278 | this.loadDiffData(); |
| 279 | }) |
| 280 | .catch((error) => { |
| 281 | console.error("Error getting base commit ref:", error); |
| 282 | // Use default range |
| 283 | this.loadDiffData(); |
| 284 | }); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 285 | } else { |
| 286 | this.loadDiffData(); |
| 287 | } |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 288 | |
| 289 | // Add click listener to close popup when clicking outside |
| 290 | document.addEventListener("click", this.handleDocumentClick.bind(this)); |
| 291 | } |
| 292 | |
| 293 | disconnectedCallback() { |
| 294 | super.disconnectedCallback(); |
| 295 | document.removeEventListener("click", this.handleDocumentClick.bind(this)); |
| 296 | } |
| 297 | |
| 298 | handleDocumentClick(event: Event) { |
| 299 | if (this.showUntrackedPopup) { |
| 300 | const target = event.target as HTMLElement; |
| 301 | // Check if click is outside the popup and button |
| 302 | if (!target.closest(".relative")) { |
| 303 | this.showUntrackedPopup = false; |
| 304 | } |
| 305 | } |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 306 | } |
| 307 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 308 | // Toggle hideUnchangedRegions setting for a specific file |
| 309 | private toggleFileExpansion(filePath: string) { |
| 310 | const currentState = this.fileExpandStates.get(filePath) ?? false; |
| 311 | const newState = !currentState; |
| 312 | this.fileExpandStates.set(filePath, newState); |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 313 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 314 | // Apply to the specific Monaco view component for this file |
| Sean McCullough | f6e1dfe | 2025-07-03 14:59:40 -0700 | [diff] [blame] | 315 | const monacoView = this.querySelector( |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 316 | `sketch-monaco-view[data-file-path="${filePath}"]`, |
| 317 | ); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 318 | if (monacoView) { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 319 | (monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged" |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 320 | } |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 321 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 322 | // Force a re-render to update the button state |
| 323 | this.requestUpdate(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 324 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 325 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 326 | render() { |
| 327 | return html` |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 328 | <div class="px-4 py-2 border-b border-gray-300 bg-gray-100 flex-shrink-0"> |
| 329 | <div class="flex flex-col gap-3"> |
| 330 | <div class="w-full flex items-center gap-3"> |
| 331 | <sketch-diff-range-picker |
| 332 | class="flex-1 min-w-[400px]" |
| 333 | .gitService="${this.gitService}" |
| 334 | @range-change="${this.handleRangeChange}" |
| 335 | ></sketch-diff-range-picker> |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 336 | ${this.renderUntrackedFilesNotification()} |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 337 | <div class="flex-1"></div> |
| 338 | ${this.renderFileSelector()} |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 339 | </div> |
| 340 | </div> |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 341 | </div> |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 342 | |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 343 | <div class="flex-1 overflow-auto flex flex-col min-h-0 relative h-full"> |
| 344 | ${this.renderDiffContent()} |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 345 | </div> |
| 346 | `; |
| 347 | } |
| 348 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 349 | renderFileSelector() { |
| David Crawshaw | 5c6d829 | 2025-06-15 19:09:19 +0000 | [diff] [blame] | 350 | const fileCount = this.files.length; |
| Autoformatter | 6255411 | 2025-06-15 19:23:33 +0000 | [diff] [blame] | 351 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 352 | return html` |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 353 | <div class="flex items-center gap-2"> |
| Philip Zeyliger | 38499cc | 2025-06-15 21:17:05 -0700 | [diff] [blame] | 354 | <select |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 355 | 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 Zeyliger | 38499cc | 2025-06-15 21:17:05 -0700 | [diff] [blame] | 356 | .value="${this.selectedFile}" |
| 357 | @change="${this.handleFileSelection}" |
| 358 | ?disabled="${fileCount === 0}" |
| 359 | > |
| 360 | <option value="">All files (${fileCount})</option> |
| 361 | ${this.files.map( |
| 362 | (file) => html` |
| 363 | <option value="${file.path}"> |
| 364 | ${this.getFileDisplayName(file)} |
| 365 | </option> |
| 366 | `, |
| 367 | )} |
| 368 | </select> |
| 369 | ${this.selectedFile ? this.renderSingleFileExpandButton() : ""} |
| 370 | </div> |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 371 | `; |
| 372 | } |
| 373 | |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 374 | renderUntrackedFilesNotification() { |
| 375 | if (!this.untrackedFiles || this.untrackedFiles.length === 0) { |
| 376 | return ""; |
| 377 | } |
| 378 | |
| 379 | const fileCount = this.untrackedFiles.length; |
| 380 | const fileCountText = |
| 381 | fileCount === 1 ? "1 untracked file" : `${fileCount} untracked files`; |
| 382 | |
| 383 | return html` |
| 384 | <div class="relative"> |
| 385 | <button |
| 386 | class="flex items-center gap-2 px-3 py-1.5 text-sm bg-gray-100 text-gray-700 border border-gray-300 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500" |
| 387 | @click="${this.toggleUntrackedFilesPopup}" |
| 388 | type="button" |
| 389 | > |
| 390 | ${fileCount} untracked |
| 391 | <svg |
| 392 | class="w-4 h-4" |
| 393 | fill="none" |
| 394 | stroke="currentColor" |
| 395 | viewBox="0 0 24 24" |
| 396 | > |
| 397 | <path |
| 398 | stroke-linecap="round" |
| 399 | stroke-linejoin="round" |
| 400 | stroke-width="2" |
| 401 | d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
| 402 | /> |
| 403 | </svg> |
| 404 | </button> |
| 405 | |
| 406 | ${this.showUntrackedPopup |
| 407 | ? html` |
| 408 | <div |
| 409 | class="absolute top-full left-0 mt-2 w-80 bg-white border border-gray-300 rounded-lg shadow-lg z-50" |
| 410 | > |
| 411 | <div class="p-4"> |
| 412 | <div class="flex items-start gap-3 mb-3"> |
| 413 | <svg |
| 414 | class="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" |
| 415 | fill="none" |
| 416 | stroke="currentColor" |
| 417 | viewBox="0 0 24 24" |
| 418 | > |
| 419 | <path |
| 420 | stroke-linecap="round" |
| 421 | stroke-linejoin="round" |
| 422 | stroke-width="2" |
| 423 | d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
| 424 | /> |
| 425 | </svg> |
| 426 | <div class="flex-1"> |
| 427 | <div class="font-medium text-gray-900 mb-1"> |
| 428 | ${fileCountText} |
| 429 | </div> |
| 430 | <div class="text-sm text-gray-600 mb-3"> |
| 431 | These files are not tracked by git. They will be lost if the session ends now. The agent typically does not add files to git until it is ready for feedback. |
| 432 | </div> |
| 433 | </div> |
| 434 | </div> |
| 435 | |
| 436 | <div class="max-h-32 overflow-y-auto"> |
| 437 | <div class="text-sm text-gray-700"> |
| 438 | ${this.untrackedFiles.map( |
| 439 | (file) => html` |
| 440 | <div |
| 441 | class="py-1 px-2 hover:bg-gray-100 rounded font-mono text-xs" |
| 442 | > |
| 443 | ${file} |
| 444 | </div> |
| 445 | `, |
| 446 | )} |
| 447 | </div> |
| 448 | </div> |
| 449 | </div> |
| 450 | </div> |
| 451 | ` |
| 452 | : ""} |
| 453 | </div> |
| 454 | `; |
| 455 | } |
| 456 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 457 | renderDiffContent() { |
| 458 | if (this.loading) { |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 459 | return html`<div class="flex items-center justify-center h-full"> |
| 460 | Loading diff... |
| 461 | </div>`; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 462 | } |
| 463 | |
| 464 | if (this.error) { |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 465 | return html`<div class="text-red-600 p-4">${this.error}</div>`; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 466 | } |
| 467 | |
| 468 | if (this.files.length === 0) { |
| 469 | return html`<sketch-diff-empty-view></sketch-diff-empty-view>`; |
| 470 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 471 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 472 | // Render single file view if a specific file is selected |
| 473 | if (this.selectedFile && this.viewMode === "single") { |
| 474 | return this.renderSingleFileView(); |
| 475 | } |
| 476 | |
| 477 | // Render multi-file view |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 478 | return html` |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 479 | <div class="flex flex-col w-full min-h-full"> |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 480 | ${this.files.map((file, index) => this.renderFileDiff(file, index))} |
| 481 | </div> |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 482 | `; |
| 483 | } |
| 484 | |
| 485 | /** |
| 486 | * Load diff data for the current range |
| 487 | */ |
| 488 | async loadDiffData() { |
| 489 | this.loading = true; |
| 490 | this.error = null; |
| 491 | |
| 492 | try { |
| 493 | // Initialize files as empty array if undefined |
| 494 | if (!this.files) { |
| 495 | this.files = []; |
| 496 | } |
| 497 | |
| David Crawshaw | 216d2fc | 2025-06-15 18:45:53 +0000 | [diff] [blame] | 498 | // Load diff data for the range |
| 499 | this.files = await this.gitService.getDiff( |
| 500 | this.currentRange.from, |
| 501 | this.currentRange.to, |
| 502 | ); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 503 | |
| 504 | // Ensure files is always an array, even when API returns null |
| 505 | if (!this.files) { |
| 506 | this.files = []; |
| 507 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 508 | |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 509 | // Load untracked files for notification |
| 510 | try { |
| 511 | this.untrackedFiles = await this.gitService.getUntrackedFiles(); |
| 512 | } catch (error) { |
| 513 | console.error("Error loading untracked files:", error); |
| 514 | this.untrackedFiles = []; |
| 515 | } |
| 516 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 517 | // Load content for all files |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 518 | if (this.files.length > 0) { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 519 | // Initialize expand states for new files (default to collapsed) |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 520 | this.files.forEach((file) => { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 521 | if (!this.fileExpandStates.has(file.path)) { |
| 522 | this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions) |
| 523 | } |
| 524 | }); |
| 525 | await this.loadAllFileContents(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 526 | } else { |
| 527 | // No files to display - reset the view to initial state |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 528 | this.selectedFilePath = ""; |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 529 | this.selectedFile = ""; |
| 530 | this.viewMode = "all"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 531 | this.fileContents.clear(); |
| 532 | this.fileExpandStates.clear(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 533 | } |
| 534 | } catch (error) { |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 535 | console.error("Error loading diff data:", error); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 536 | this.error = `Error loading diff data: ${error.message}`; |
| 537 | // Ensure files is an empty array when an error occurs |
| 538 | this.files = []; |
| 539 | // Reset the view to initial state |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 540 | this.selectedFilePath = ""; |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 541 | this.selectedFile = ""; |
| 542 | this.viewMode = "all"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 543 | this.fileContents.clear(); |
| 544 | this.fileExpandStates.clear(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 545 | } finally { |
| 546 | this.loading = false; |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | /** |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 551 | * Load content for all files in the diff |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 552 | */ |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 553 | async loadAllFileContents() { |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 554 | this.loading = true; |
| 555 | this.error = null; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 556 | this.fileContents.clear(); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 557 | |
| 558 | try { |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 559 | let isUnstagedChanges = false; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 560 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 561 | // Determine the commits to compare based on the current range |
| philip.zeyliger | 26bc659 | 2025-06-30 20:15:30 -0700 | [diff] [blame] | 562 | const _fromCommit = this.currentRange.from; |
| 563 | const toCommit = this.currentRange.to; |
| David Crawshaw | 216d2fc | 2025-06-15 18:45:53 +0000 | [diff] [blame] | 564 | // Check if this is an unstaged changes view |
| 565 | isUnstagedChanges = toCommit === ""; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 566 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 567 | // Load content for all files |
| 568 | const promises = this.files.map(async (file) => { |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 569 | try { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 570 | let originalCode = ""; |
| 571 | let modifiedCode = ""; |
| 572 | let editable = isUnstagedChanges; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 573 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 574 | // Load the original code based on file status |
| 575 | if (file.status !== "A") { |
| 576 | // For modified, renamed, or deleted files: load original content |
| 577 | originalCode = await this.gitService.getFileContent( |
| 578 | file.old_hash || "", |
| 579 | ); |
| 580 | } |
| 581 | |
| 582 | // For modified code, always use working copy when editable |
| 583 | if (editable) { |
| 584 | try { |
| 585 | // Always use working copy when editable, regardless of diff status |
| 586 | modifiedCode = await this.gitService.getWorkingCopyContent( |
| 587 | file.path, |
| 588 | ); |
| 589 | } catch (error) { |
| 590 | if (file.status === "D") { |
| 591 | // For deleted files, silently use empty content |
| 592 | console.warn( |
| 593 | `Could not get working copy for deleted file ${file.path}, using empty content`, |
| 594 | ); |
| 595 | modifiedCode = ""; |
| 596 | } else { |
| 597 | // For any other file status, propagate the error |
| 598 | console.error( |
| 599 | `Failed to get working copy for ${file.path}:`, |
| 600 | error, |
| 601 | ); |
| 602 | throw error; |
| 603 | } |
| 604 | } |
| 605 | } else { |
| 606 | // For non-editable view, use git content based on file status |
| 607 | if (file.status === "D") { |
| 608 | // Deleted file: empty modified |
| 609 | modifiedCode = ""; |
| 610 | } else { |
| 611 | // Added/modified/renamed: use the content from git |
| 612 | modifiedCode = await this.gitService.getFileContent( |
| 613 | file.new_hash || "", |
| 614 | ); |
| 615 | } |
| 616 | } |
| 617 | |
| 618 | // Don't make deleted files editable |
| 619 | if (file.status === "D") { |
| 620 | editable = false; |
| 621 | } |
| 622 | |
| 623 | this.fileContents.set(file.path, { |
| 624 | original: originalCode, |
| 625 | modified: modifiedCode, |
| 626 | editable, |
| 627 | }); |
| 628 | } catch (error) { |
| 629 | console.error(`Error loading content for file ${file.path}:`, error); |
| 630 | // Store empty content for failed files to prevent blocking |
| 631 | this.fileContents.set(file.path, { |
| 632 | original: "", |
| 633 | modified: "", |
| 634 | editable: false, |
| 635 | }); |
| 636 | } |
| 637 | }); |
| 638 | |
| 639 | await Promise.all(promises); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 640 | } catch (error) { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 641 | console.error("Error loading file contents:", error); |
| 642 | this.error = `Error loading file contents: ${error.message}`; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 643 | } finally { |
| 644 | this.loading = false; |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | /** |
| 649 | * Handle range change event from the range picker |
| 650 | */ |
| 651 | handleRangeChange(event: CustomEvent) { |
| 652 | const { range } = event.detail; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 653 | console.log("Range changed:", range); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 654 | this.currentRange = range; |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 655 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 656 | // Load diff data for the new range |
| 657 | this.loadDiffData(); |
| 658 | } |
| 659 | |
| 660 | /** |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 661 | * Render a single file diff section |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 662 | */ |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 663 | renderFileDiff(file: GitDiffFile, index: number) { |
| 664 | const content = this.fileContents.get(file.path); |
| 665 | if (!content) { |
| 666 | return html` |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 667 | <div |
| 668 | class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0" |
| 669 | > |
| 670 | <div |
| 671 | 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" |
| 672 | > |
| 673 | ${this.renderFileHeader(file)} |
| 674 | </div> |
| 675 | <div class="flex items-center justify-center h-full"> |
| 676 | Loading ${file.path}... |
| 677 | </div> |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 678 | </div> |
| 679 | `; |
| 680 | } |
| 681 | |
| 682 | return html` |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 683 | <div |
| 684 | class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0" |
| 685 | > |
| 686 | <div |
| 687 | 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" |
| 688 | > |
| 689 | ${this.renderFileHeader(file)} |
| 690 | </div> |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 691 | <div class="flex flex-col w-full min-h-[200px] flex-1"> |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 692 | <sketch-monaco-view |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 693 | class="flex flex-col w-full min-h-[200px] flex-1" |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 694 | .originalCode="${content.original}" |
| 695 | .modifiedCode="${content.modified}" |
| 696 | .originalFilename="${file.path}" |
| 697 | .modifiedFilename="${file.path}" |
| 698 | ?readOnly="${!content.editable}" |
| 699 | ?editable-right="${content.editable}" |
| 700 | @monaco-comment="${this.handleMonacoComment}" |
| 701 | @monaco-save="${this.handleMonacoSave}" |
| 702 | @monaco-height-changed="${this.handleMonacoHeightChange}" |
| 703 | data-file-index="${index}" |
| 704 | data-file-path="${file.path}" |
| 705 | ></sketch-monaco-view> |
| 706 | </div> |
| 707 | </div> |
| 708 | `; |
| 709 | } |
| 710 | |
| 711 | /** |
| 712 | * Render file header with status and path info |
| 713 | */ |
| 714 | renderFileHeader(file: GitDiffFile) { |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 715 | const statusClasses = this.getFileStatusTailwindClasses(file.status); |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 716 | const statusText = this.getFileStatusText(file.status); |
| 717 | const changesInfo = this.getChangesInfo(file); |
| 718 | const pathInfo = this.getPathInfo(file); |
| 719 | |
| 720 | const isExpanded = this.fileExpandStates.get(file.path) ?? false; |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 721 | |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 722 | return html` |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 723 | <div class="flex items-center gap-2"> |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 724 | <span |
| 725 | class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 ${statusClasses}" |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 726 | > |
| David Crawshaw | 255dc43 | 2025-07-06 21:58:00 +0000 | [diff] [blame] | 727 | ${statusText} |
| 728 | </span> |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 729 | <span class="font-mono font-normal text-gray-600">${pathInfo}</span> |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 730 | ${changesInfo |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 731 | ? html`<span class="ml-2 text-xs text-gray-600">${changesInfo}</span>` |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 732 | : ""} |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 733 | </div> |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 734 | <div class="flex items-center"> |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 735 | <button |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 736 | 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 Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 737 | @click="${() => this.toggleFileExpansion(file.path)}" |
| 738 | title="${isExpanded |
| 739 | ? "Collapse: Hide unchanged regions to focus on changes" |
| 740 | : "Expand: Show all lines including unchanged regions"}" |
| 741 | > |
| Autoformatter | 9abf803 | 2025-06-14 23:24:08 +0000 | [diff] [blame] | 742 | ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()} |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 743 | </button> |
| 744 | </div> |
| 745 | `; |
| 746 | } |
| 747 | |
| 748 | /** |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 749 | * Get Tailwind CSS classes for file status |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 750 | */ |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 751 | getFileStatusTailwindClasses(status: string): string { |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 752 | switch (status.toUpperCase()) { |
| 753 | case "A": |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 754 | return "bg-green-100 text-green-800"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 755 | case "M": |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 756 | return "bg-yellow-100 text-yellow-800"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 757 | case "D": |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 758 | return "bg-red-100 text-red-800"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 759 | case "R": |
| Josh Bleecher Snyder | b3aff88 | 2025-07-01 02:17:27 +0000 | [diff] [blame] | 760 | case "C": |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 761 | default: |
| 762 | if (status.toUpperCase().startsWith("R")) { |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 763 | return "bg-cyan-100 text-cyan-800"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 764 | } |
| Josh Bleecher Snyder | b3aff88 | 2025-07-01 02:17:27 +0000 | [diff] [blame] | 765 | if (status.toUpperCase().startsWith("C")) { |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 766 | return "bg-indigo-100 text-indigo-800"; |
| Josh Bleecher Snyder | b3aff88 | 2025-07-01 02:17:27 +0000 | [diff] [blame] | 767 | } |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 768 | return "bg-yellow-100 text-yellow-800"; |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 769 | } |
| 770 | } |
| 771 | |
| 772 | /** |
| 773 | * Get display text for file status |
| 774 | */ |
| 775 | getFileStatusText(status: string): string { |
| 776 | switch (status.toUpperCase()) { |
| 777 | case "A": |
| 778 | return "Added"; |
| 779 | case "M": |
| 780 | return "Modified"; |
| 781 | case "D": |
| 782 | return "Deleted"; |
| 783 | case "R": |
| Josh Bleecher Snyder | b3aff88 | 2025-07-01 02:17:27 +0000 | [diff] [blame] | 784 | case "C": |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 785 | default: |
| 786 | if (status.toUpperCase().startsWith("R")) { |
| 787 | return "Renamed"; |
| 788 | } |
| Josh Bleecher Snyder | b3aff88 | 2025-07-01 02:17:27 +0000 | [diff] [blame] | 789 | if (status.toUpperCase().startsWith("C")) { |
| 790 | return "Copied"; |
| 791 | } |
| David Crawshaw | 26f3f34 | 2025-06-14 19:58:32 +0000 | [diff] [blame] | 792 | return "Modified"; |
| 793 | } |
| 794 | } |
| 795 | |
| 796 | /** |
| 797 | * Get changes information (+/-) for display |
| 798 | */ |
| 799 | getChangesInfo(file: GitDiffFile): string { |
| 800 | const additions = file.additions || 0; |
| 801 | const deletions = file.deletions || 0; |
| 802 | |
| 803 | if (additions === 0 && deletions === 0) { |
| 804 | return ""; |
| 805 | } |
| 806 | |
| 807 | const parts = []; |
| 808 | if (additions > 0) { |
| 809 | parts.push(`+${additions}`); |
| 810 | } |
| 811 | if (deletions > 0) { |
| 812 | parts.push(`-${deletions}`); |
| 813 | } |
| 814 | |
| 815 | return `(${parts.join(", ")})`; |
| 816 | } |
| 817 | |
| 818 | /** |
| 819 | * Get path information for display, handling renames |
| 820 | */ |
| 821 | getPathInfo(file: GitDiffFile): string { |
| 822 | if (file.old_path && file.old_path !== "") { |
| 823 | // For renames, show old_path → new_path |
| 824 | return `${file.old_path} → ${file.path}`; |
| 825 | } |
| 826 | // For regular files, just show the path |
| 827 | return file.path; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 828 | } |
| 829 | |
| 830 | /** |
| Philip Zeyliger | e89b308 | 2025-05-29 03:16:06 +0000 | [diff] [blame] | 831 | * Render expand all icon (dotted line with arrows pointing away) |
| 832 | */ |
| 833 | renderExpandAllIcon() { |
| 834 | return html` |
| 835 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> |
| 836 | <!-- Dotted line in the middle --> |
| 837 | <line |
| 838 | x1="2" |
| 839 | y1="8" |
| 840 | x2="14" |
| 841 | y2="8" |
| 842 | stroke="currentColor" |
| 843 | stroke-width="1" |
| 844 | stroke-dasharray="2,1" |
| 845 | /> |
| 846 | <!-- Large arrow pointing up --> |
| 847 | <path d="M8 2 L5 6 L11 6 Z" fill="currentColor" /> |
| 848 | <!-- Large arrow pointing down --> |
| 849 | <path d="M8 14 L5 10 L11 10 Z" fill="currentColor" /> |
| 850 | </svg> |
| 851 | `; |
| 852 | } |
| 853 | |
| 854 | /** |
| 855 | * Render collapse icon (arrows pointing towards dotted line) |
| 856 | */ |
| 857 | renderCollapseIcon() { |
| 858 | return html` |
| 859 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> |
| 860 | <!-- Dotted line in the middle --> |
| 861 | <line |
| 862 | x1="2" |
| 863 | y1="8" |
| 864 | x2="14" |
| 865 | y2="8" |
| 866 | stroke="currentColor" |
| 867 | stroke-width="1" |
| 868 | stroke-dasharray="2,1" |
| 869 | /> |
| 870 | <!-- Large arrow pointing down towards line --> |
| 871 | <path d="M8 6 L5 2 L11 2 Z" fill="currentColor" /> |
| 872 | <!-- Large arrow pointing up towards line --> |
| 873 | <path d="M8 10 L5 14 L11 14 Z" fill="currentColor" /> |
| 874 | </svg> |
| 875 | `; |
| 876 | } |
| 877 | |
| 878 | /** |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 879 | * Handle file selection change from the dropdown |
| 880 | */ |
| 881 | handleFileSelection(event: Event) { |
| 882 | const selectElement = event.target as HTMLSelectElement; |
| 883 | const selectedValue = selectElement.value; |
| Autoformatter | 6255411 | 2025-06-15 19:23:33 +0000 | [diff] [blame] | 884 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 885 | this.selectedFile = selectedValue; |
| 886 | this.viewMode = selectedValue ? "single" : "all"; |
| Autoformatter | 6255411 | 2025-06-15 19:23:33 +0000 | [diff] [blame] | 887 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 888 | // Force re-render |
| 889 | this.requestUpdate(); |
| 890 | } |
| 891 | |
| Josh Bleecher Snyder | a8561f7 | 2025-07-15 23:47:59 +0000 | [diff] [blame^] | 892 | toggleUntrackedFilesPopup() { |
| 893 | this.showUntrackedPopup = !this.showUntrackedPopup; |
| 894 | } |
| 895 | |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 896 | /** |
| 897 | * Get display name for file in the selector |
| 898 | */ |
| 899 | getFileDisplayName(file: GitDiffFile): string { |
| 900 | const status = this.getFileStatusText(file.status); |
| 901 | const pathInfo = this.getPathInfo(file); |
| 902 | return `${status}: ${pathInfo}`; |
| 903 | } |
| 904 | |
| 905 | /** |
| Philip Zeyliger | 38499cc | 2025-06-15 21:17:05 -0700 | [diff] [blame] | 906 | * Render expand/collapse button for single file view in header |
| 907 | */ |
| 908 | renderSingleFileExpandButton() { |
| 909 | if (!this.selectedFile) return ""; |
| 910 | |
| 911 | const isExpanded = this.fileExpandStates.get(this.selectedFile) ?? false; |
| 912 | |
| 913 | return html` |
| 914 | <button |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 915 | 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 Zeyliger | 38499cc | 2025-06-15 21:17:05 -0700 | [diff] [blame] | 916 | @click="${() => this.toggleFileExpansion(this.selectedFile)}" |
| 917 | title="${isExpanded |
| 918 | ? "Collapse: Hide unchanged regions to focus on changes" |
| 919 | : "Expand: Show all lines including unchanged regions"}" |
| 920 | > |
| 921 | ${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()} |
| 922 | </button> |
| 923 | `; |
| 924 | } |
| 925 | |
| 926 | /** |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 927 | * Render single file view with full-screen Monaco editor |
| 928 | */ |
| 929 | renderSingleFileView() { |
| Autoformatter | 6255411 | 2025-06-15 19:23:33 +0000 | [diff] [blame] | 930 | const selectedFileData = this.files.find( |
| 931 | (f) => f.path === this.selectedFile, |
| 932 | ); |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 933 | if (!selectedFileData) { |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 934 | return html`<div class="text-red-600 p-4">Selected file not found</div>`; |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 935 | } |
| 936 | |
| 937 | const content = this.fileContents.get(this.selectedFile); |
| 938 | if (!content) { |
| Autoformatter | 9f5cb2e | 2025-07-03 00:25:35 +0000 | [diff] [blame] | 939 | return html`<div class="flex items-center justify-center h-full"> |
| 940 | Loading ${this.selectedFile}... |
| 941 | </div>`; |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 942 | } |
| 943 | |
| 944 | return html` |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 945 | <div class="flex-1 flex flex-col h-full min-h-0"> |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 946 | <sketch-monaco-view |
| banksean | 5450584 | 2025-07-03 00:18:44 +0000 | [diff] [blame] | 947 | class="flex-1 w-full h-full min-h-0" |
| David Crawshaw | 4cd0129 | 2025-06-15 18:59:13 +0000 | [diff] [blame] | 948 | .originalCode="${content.original}" |
| 949 | .modifiedCode="${content.modified}" |
| 950 | .originalFilename="${selectedFileData.path}" |
| 951 | .modifiedFilename="${selectedFileData.path}" |
| 952 | ?readOnly="${!content.editable}" |
| 953 | ?editable-right="${content.editable}" |
| 954 | @monaco-comment="${this.handleMonacoComment}" |
| 955 | @monaco-save="${this.handleMonacoSave}" |
| 956 | data-file-path="${selectedFileData.path}" |
| 957 | ></sketch-monaco-view> |
| 958 | </div> |
| 959 | `; |
| 960 | } |
| 961 | |
| 962 | /** |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 963 | * Refresh the diff view by reloading commits and diff data |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 964 | * |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 965 | * This is called when the Monaco diff tab is activated to ensure: |
| 966 | * 1. Branch information from git/recentlog is current (branches can change frequently) |
| 967 | * 2. The diff content is synchronized with the latest repository state |
| 968 | * 3. Users always see up-to-date information without manual refresh |
| 969 | */ |
| 970 | refreshDiffView() { |
| 971 | // First refresh the range picker to get updated branch information |
| Sean McCullough | f6e1dfe | 2025-07-03 14:59:40 -0700 | [diff] [blame] | 972 | const rangePicker = this.querySelector("sketch-diff-range-picker"); |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 973 | if (rangePicker) { |
| 974 | (rangePicker as any).loadCommits(); |
| 975 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 976 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 977 | if (this.commit) { |
| David Crawshaw | 216d2fc | 2025-06-15 18:45:53 +0000 | [diff] [blame] | 978 | // Convert single commit to range (commit^ to commit) |
| Autoformatter | 6255411 | 2025-06-15 19:23:33 +0000 | [diff] [blame] | 979 | this.currentRange = { |
| 980 | type: "range", |
| 981 | from: `${this.commit}^`, |
| 982 | to: this.commit, |
| 983 | }; |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 984 | } |
| Autoformatter | 8c46362 | 2025-05-16 21:54:17 +0000 | [diff] [blame] | 985 | |
| Philip Zeyliger | 272a90e | 2025-05-16 14:49:51 -0700 | [diff] [blame] | 986 | // Then reload diff data based on the current range |
| 987 | this.loadDiffData(); |
| 988 | } |
| 989 | } |
| 990 | |
| 991 | declare global { |
| 992 | interface HTMLElementTagNameMap { |
| 993 | "sketch-diff2-view": SketchDiff2View; |
| 994 | } |
| 995 | } |