blob: 03b6744b382ee09e7ba51c4ccd1a9d8d4491a7bc [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 { createRef, Ref, ref } from "lit/directives/ref.js";
4
5// See https://rodydavis.com/posts/lit-monaco-editor for some ideas.
6
philip.zeyligerc0a44592025-06-15 21:24:57 -07007import type * as monaco from "monaco-editor";
Philip Zeyliger272a90e2025-05-16 14:49:51 -07008
philip.zeyligerc0a44592025-06-15 21:24:57 -07009// Monaco is loaded dynamically - see loadMonaco() function
10declare global {
11 interface Window {
12 monaco?: typeof monaco;
13 }
14}
15
16// Monaco hash will be injected at build time
17declare const __MONACO_HASH__: string;
18
19// Load Monaco editor dynamically
20let monacoLoadPromise: Promise<any> | null = null;
21
22function loadMonaco(): Promise<typeof monaco> {
23 if (monacoLoadPromise) {
24 return monacoLoadPromise;
25 }
26
27 if (window.monaco) {
28 return Promise.resolve(window.monaco);
29 }
30
31 monacoLoadPromise = new Promise((resolve, reject) => {
32 // Get the Monaco hash from build-time constant
33 const monacoHash = __MONACO_HASH__;
Autoformatter2f8464c2025-06-16 04:27:05 +000034
philip.zeyligerc0a44592025-06-15 21:24:57 -070035 // Try to load the external Monaco bundle
Autoformatter2f8464c2025-06-16 04:27:05 +000036 const script = document.createElement("script");
philip.zeyligerc0a44592025-06-15 21:24:57 -070037 script.onload = () => {
38 // The Monaco bundle should set window.monaco
39 if (window.monaco) {
40 resolve(window.monaco);
41 } else {
Autoformatter2f8464c2025-06-16 04:27:05 +000042 reject(new Error("Monaco not loaded from external bundle"));
philip.zeyligerc0a44592025-06-15 21:24:57 -070043 }
44 };
45 script.onerror = (error) => {
Autoformatter2f8464c2025-06-16 04:27:05 +000046 console.warn("Failed to load external Monaco bundle:", error);
47 reject(new Error("Monaco external bundle failed to load"));
philip.zeyligerc0a44592025-06-15 21:24:57 -070048 };
Autoformatter2f8464c2025-06-16 04:27:05 +000049
philip.zeyligerc0a44592025-06-15 21:24:57 -070050 // Don't set type="module" since we're using IIFE format
51 script.src = `./static/monaco-standalone-${monacoHash}.js`;
52 document.head.appendChild(script);
53 });
54
55 return monacoLoadPromise;
56}
Philip Zeyliger272a90e2025-05-16 14:49:51 -070057
58// Define Monaco CSS styles as a string constant
59const monacoStyles = `
60 /* Import Monaco editor styles */
61 @import url('./static/monaco/min/vs/editor/editor.main.css');
62
63 /* Codicon font is now defined globally in sketch-app-shell.css */
64
65 /* Custom Monaco styles */
66 .monaco-editor {
67 width: 100%;
68 height: 100%;
69 }
70
Philip Zeyliger272a90e2025-05-16 14:49:51 -070071 /* Ensure light theme colors */
72 .monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input {
73 background-color: var(--monaco-editor-bg, #ffffff) !important;
74 }
75
76 .monaco-editor .margin {
77 background-color: var(--monaco-editor-margin, #f5f5f5) !important;
78 }
79`;
80
81// Configure Monaco to use local workers with correct relative paths
82// Monaco looks for this global configuration to determine how to load web workers
83// @ts-ignore - MonacoEnvironment is added to the global scope at runtime
84self.MonacoEnvironment = {
85 getWorkerUrl: function (_moduleId, label) {
86 if (label === "json") {
87 return "./static/json.worker.js";
88 }
89 if (label === "css" || label === "scss" || label === "less") {
90 return "./static/css.worker.js";
91 }
92 if (label === "html" || label === "handlebars" || label === "razor") {
93 return "./static/html.worker.js";
94 }
95 if (label === "typescript" || label === "javascript") {
96 return "./static/ts.worker.js";
97 }
98 return "./static/editor.worker.js";
99 },
100};
101
102@customElement("sketch-monaco-view")
103export class CodeDiffEditor extends LitElement {
104 // Editable state
105 @property({ type: Boolean, attribute: "editable-right" })
106 editableRight?: boolean;
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000107
108 // Inline diff mode (for mobile)
109 @property({ type: Boolean, attribute: "inline" })
110 inline?: boolean;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700111 private container: Ref<HTMLElement> = createRef();
112 editor?: monaco.editor.IStandaloneDiffEditor;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700113
114 // Save state properties
115 @state() private saveState: "idle" | "modified" | "saving" | "saved" = "idle";
116 @state() private debounceSaveTimeout: number | null = null;
117 @state() private lastSavedContent: string = "";
118 @property() originalCode?: string = "// Original code here";
119 @property() modifiedCode?: string = "// Modified code here";
120 @property() originalFilename?: string = "original.js";
121 @property() modifiedFilename?: string = "modified.js";
122
123 /* Selected text and indicators */
124 @state()
125 private selectedText: string | null = null;
126
127 @state()
128 private selectionRange: {
129 startLineNumber: number;
130 startColumn: number;
131 endLineNumber: number;
132 endColumn: number;
133 } | null = null;
134
135 @state()
136 private showCommentIndicator: boolean = false;
137
138 @state()
139 private indicatorPosition: { top: number; left: number } = {
140 top: 0,
141 left: 0,
142 };
143
144 @state()
145 private showCommentBox: boolean = false;
146
147 @state()
148 private commentText: string = "";
149
150 @state()
151 private activeEditor: "original" | "modified" = "modified"; // Track which editor is active
152
153 // Custom event to request save action from external components
154 private requestSave() {
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000155 if (!this.editableRight || this.saveState !== "modified") return;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700156
157 this.saveState = "saving";
158
159 // Get current content from modified editor
160 const modifiedContent = this.modifiedModel?.getValue() || "";
161
162 // Create and dispatch the save event
163 const saveEvent = new CustomEvent("monaco-save", {
164 detail: {
165 path: this.modifiedFilename,
166 content: modifiedContent,
167 },
168 bubbles: true,
169 composed: true,
170 });
171
172 this.dispatchEvent(saveEvent);
173 }
174
175 // Method to be called from parent when save is complete
176 public notifySaveComplete(success: boolean) {
177 if (success) {
178 this.saveState = "saved";
179 // Update last saved content
180 this.lastSavedContent = this.modifiedModel?.getValue() || "";
181 // Reset to idle after a delay
182 setTimeout(() => {
183 this.saveState = "idle";
184 }, 2000);
185 } else {
186 // Return to modified state on error
187 this.saveState = "modified";
188 }
189 }
190
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000191 // Rescue people with strong save-constantly habits
192 private setupKeyboardShortcuts() {
193 if (!this.editor) return;
194 const modifiedEditor = this.editor.getModifiedEditor();
195 if (!modifiedEditor) return;
196
philip.zeyligerc0a44592025-06-15 21:24:57 -0700197 const monaco = window.monaco;
198 if (!monaco) return;
Autoformatter2f8464c2025-06-16 04:27:05 +0000199
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000200 modifiedEditor.addCommand(
201 monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
202 () => {
203 this.requestSave();
Autoformatter57893c22025-05-29 13:49:53 +0000204 },
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000205 );
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000206 }
207
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700208 // Setup content change listener for debounced save
209 private setupContentChangeListener() {
210 if (!this.editor || !this.editableRight) return;
211
212 const modifiedEditor = this.editor.getModifiedEditor();
213 if (!modifiedEditor || !modifiedEditor.getModel()) return;
214
215 // Store initial content
216 this.lastSavedContent = modifiedEditor.getModel()!.getValue();
217
218 // Listen for content changes
219 modifiedEditor.getModel()!.onDidChangeContent(() => {
220 const currentContent = modifiedEditor.getModel()!.getValue();
221
222 // Check if content has actually changed from last saved state
223 if (currentContent !== this.lastSavedContent) {
224 this.saveState = "modified";
225
226 // Debounce save request
227 if (this.debounceSaveTimeout) {
228 window.clearTimeout(this.debounceSaveTimeout);
229 }
230
231 this.debounceSaveTimeout = window.setTimeout(() => {
232 this.requestSave();
233 this.debounceSaveTimeout = null;
234 }, 1000); // 1 second debounce
235 }
236 });
237 }
238
239 static styles = css`
240 /* Save indicator styles */
241 .save-indicator {
242 position: absolute;
243 top: 4px;
244 right: 4px;
245 padding: 3px 8px;
246 border-radius: 3px;
247 font-size: 12px;
248 font-family: system-ui, sans-serif;
249 color: white;
250 z-index: 100;
251 opacity: 0.9;
252 pointer-events: none;
253 transition: opacity 0.3s ease;
254 }
255
Philip Zeyligere89b3082025-05-29 03:16:06 +0000256 .save-indicator.idle {
257 background-color: #6c757d;
258 }
259
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700260 .save-indicator.modified {
261 background-color: #f0ad4e;
262 }
263
264 .save-indicator.saving {
265 background-color: #5bc0de;
266 }
267
268 .save-indicator.saved {
269 background-color: #5cb85c;
270 }
271
272 /* Editor host styles */
273 :host {
274 --editor-width: 100%;
275 --editor-height: 100%;
276 display: flex;
David Crawshaw26f3f342025-06-14 19:58:32 +0000277 flex: none; /* Don't grow/shrink - size is determined by content */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700278 min-height: 0; /* Critical for flex layout */
279 position: relative; /* Establish positioning context */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700280 width: 100%; /* Take full width */
David Crawshaw26f3f342025-06-14 19:58:32 +0000281 /* Height will be set dynamically by setupAutoSizing */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700282 }
283 main {
David Crawshaw26f3f342025-06-14 19:58:32 +0000284 width: 100%;
285 height: 100%; /* Fill the host element completely */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700286 border: 1px solid #e0e0e0;
David Crawshaw26f3f342025-06-14 19:58:32 +0000287 flex: none; /* Size determined by parent */
288 min-height: 200px; /* Ensure a minimum height for the editor */
289 /* Remove absolute positioning - use normal block layout */
290 position: relative;
291 display: block;
David Crawshawdba26b52025-06-15 00:33:45 +0000292 box-sizing: border-box; /* Include border in width calculation */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700293 }
294
295 /* Comment indicator and box styles */
296 .comment-indicator {
297 position: fixed;
298 background-color: rgba(66, 133, 244, 0.9);
299 color: white;
300 border-radius: 3px;
301 padding: 3px 8px;
302 font-size: 12px;
303 cursor: pointer;
304 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
305 z-index: 10000;
306 animation: fadeIn 0.2s ease-in-out;
307 display: flex;
308 align-items: center;
309 gap: 4px;
310 pointer-events: all;
311 }
312
313 .comment-indicator:hover {
314 background-color: rgba(66, 133, 244, 1);
315 }
316
317 .comment-indicator span {
318 line-height: 1;
319 }
320
321 .comment-box {
322 position: fixed;
323 background-color: white;
324 border: 1px solid #ddd;
325 border-radius: 4px;
326 box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
327 padding: 12px;
328 z-index: 10001;
329 width: 350px;
330 animation: fadeIn 0.2s ease-in-out;
331 max-height: 80vh;
332 overflow-y: auto;
333 }
334
335 .comment-box-header {
336 display: flex;
337 justify-content: space-between;
338 align-items: center;
339 margin-bottom: 8px;
340 }
341
342 .comment-box-header h3 {
343 margin: 0;
344 font-size: 14px;
345 font-weight: 500;
346 }
347
348 .close-button {
349 background: none;
350 border: none;
351 cursor: pointer;
352 font-size: 16px;
353 color: #666;
354 padding: 2px 6px;
355 }
356
357 .close-button:hover {
358 color: #333;
359 }
360
361 .selected-text-preview {
362 background-color: #f5f5f5;
363 border: 1px solid #eee;
364 border-radius: 3px;
365 padding: 8px;
366 margin-bottom: 10px;
367 font-family: monospace;
368 font-size: 12px;
369 max-height: 80px;
370 overflow-y: auto;
371 white-space: pre-wrap;
372 word-break: break-all;
373 }
374
375 .comment-textarea {
376 width: 100%;
377 min-height: 80px;
378 padding: 8px;
379 border: 1px solid #ddd;
380 border-radius: 3px;
381 resize: vertical;
382 font-family: inherit;
383 margin-bottom: 10px;
384 box-sizing: border-box;
385 }
386
387 .comment-actions {
388 display: flex;
389 justify-content: flex-end;
390 gap: 8px;
391 }
392
393 .comment-actions button {
394 padding: 6px 12px;
395 border-radius: 3px;
396 cursor: pointer;
397 font-size: 12px;
398 }
399
400 .cancel-button {
401 background-color: transparent;
402 border: 1px solid #ddd;
403 }
404
405 .cancel-button:hover {
406 background-color: #f5f5f5;
407 }
408
409 .submit-button {
410 background-color: #4285f4;
411 color: white;
412 border: none;
413 }
414
415 .submit-button:hover {
416 background-color: #3367d6;
417 }
418
419 @keyframes fadeIn {
420 from {
421 opacity: 0;
422 }
423 to {
424 opacity: 1;
425 }
426 }
427 `;
428
429 render() {
430 return html`
431 <style>
432 ${monacoStyles}
433 </style>
434 <main ${ref(this.container)}></main>
435
436 <!-- Save indicator - shown when editing -->
Philip Zeyligere89b3082025-05-29 03:16:06 +0000437 ${this.editableRight
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700438 ? html`
439 <div class="save-indicator ${this.saveState}">
Philip Zeyligere89b3082025-05-29 03:16:06 +0000440 ${this.saveState === "idle"
441 ? "Editable"
442 : this.saveState === "modified"
443 ? "Modified..."
444 : this.saveState === "saving"
445 ? "Saving..."
446 : this.saveState === "saved"
447 ? "Saved"
448 : ""}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700449 </div>
450 `
451 : ""}
452
453 <!-- Comment indicator - shown when text is selected -->
454 ${this.showCommentIndicator
455 ? html`
456 <div
457 class="comment-indicator"
458 style="top: ${this.indicatorPosition.top}px; left: ${this
459 .indicatorPosition.left}px;"
460 @click="${this.handleIndicatorClick}"
461 @mouseenter="${() => {
462 this._isHovering = true;
463 }}"
464 @mouseleave="${() => {
465 this._isHovering = false;
466 }}"
467 >
468 <span>💬</span>
469 <span>Add comment</span>
470 </div>
471 `
472 : ""}
473
474 <!-- Comment box - shown when indicator is clicked -->
475 ${this.showCommentBox
476 ? html`
477 <div
478 class="comment-box"
479 style="${this.calculateCommentBoxPosition()}"
480 @mouseenter="${() => {
481 this._isHovering = true;
482 }}"
483 @mouseleave="${() => {
484 this._isHovering = false;
485 }}"
486 >
487 <div class="comment-box-header">
488 <h3>Add comment</h3>
489 <button class="close-button" @click="${this.closeCommentBox}">
490 ×
491 </button>
492 </div>
493 <div class="selected-text-preview">${this.selectedText}</div>
494 <textarea
495 class="comment-textarea"
496 placeholder="Type your comment here..."
497 .value="${this.commentText}"
498 @input="${this.handleCommentInput}"
499 ></textarea>
500 <div class="comment-actions">
501 <button class="cancel-button" @click="${this.closeCommentBox}">
502 Cancel
503 </button>
504 <button class="submit-button" @click="${this.submitComment}">
Josh Bleecher Snyderafeafea2025-05-23 20:27:39 +0000505 Add
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700506 </button>
507 </div>
508 </div>
509 `
510 : ""}
511 `;
512 }
513
514 /**
515 * Calculate the optimal position for the comment box to keep it in view
516 */
517 private calculateCommentBoxPosition(): string {
518 // Get viewport dimensions
519 const viewportWidth = window.innerWidth;
520 const viewportHeight = window.innerHeight;
521
522 // Default position (below indicator)
523 let top = this.indicatorPosition.top + 30;
524 let left = this.indicatorPosition.left;
525
526 // Estimated box dimensions
527 const boxWidth = 350;
528 const boxHeight = 300;
529
530 // Check if box would go off the right edge
531 if (left + boxWidth > viewportWidth) {
532 left = viewportWidth - boxWidth - 20; // Keep 20px margin
533 }
534
535 // Check if box would go off the bottom
536 const bottomSpace = viewportHeight - top;
537 if (bottomSpace < boxHeight) {
538 // Not enough space below, try to position above if possible
539 if (this.indicatorPosition.top > boxHeight) {
540 // Position above the indicator
541 top = this.indicatorPosition.top - boxHeight - 10;
542 } else {
543 // Not enough space above either, position at top of viewport with margin
544 top = 10;
545 }
546 }
547
548 // Ensure box is never positioned off-screen
549 top = Math.max(10, top);
550 left = Math.max(10, left);
551
552 return `top: ${top}px; left: ${left}px;`;
553 }
554
555 setOriginalCode(code: string, filename?: string) {
556 this.originalCode = code;
557 if (filename) {
558 this.originalFilename = filename;
559 }
560
561 // Update the model if the editor is initialized
562 if (this.editor) {
563 const model = this.editor.getOriginalEditor().getModel();
564 if (model) {
565 model.setValue(code);
566 if (filename) {
philip.zeyligerc0a44592025-06-15 21:24:57 -0700567 window.monaco!.editor.setModelLanguage(
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700568 model,
569 this.getLanguageForFile(filename),
570 );
571 }
572 }
573 }
574 }
575
576 setModifiedCode(code: string, filename?: string) {
577 this.modifiedCode = code;
578 if (filename) {
579 this.modifiedFilename = filename;
580 }
581
582 // Update the model if the editor is initialized
583 if (this.editor) {
584 const model = this.editor.getModifiedEditor().getModel();
585 if (model) {
586 model.setValue(code);
587 if (filename) {
philip.zeyligerc0a44592025-06-15 21:24:57 -0700588 window.monaco!.editor.setModelLanguage(
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700589 model,
590 this.getLanguageForFile(filename),
591 );
592 }
593 }
594 }
595 }
596
Philip Zeyliger70273072025-05-28 18:26:14 +0000597 private _extensionToLanguageMap: Map<string, string> | null = null;
598
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700599 private getLanguageForFile(filename: string): string {
Philip Zeyliger70273072025-05-28 18:26:14 +0000600 // Get the file extension (including the dot for exact matching)
601 const extension = "." + (filename.split(".").pop()?.toLowerCase() || "");
602
603 // Build the extension-to-language map on first use
604 if (!this._extensionToLanguageMap) {
605 this._extensionToLanguageMap = new Map();
philip.zeyligerc0a44592025-06-15 21:24:57 -0700606 const languages = window.monaco!.languages.getLanguages();
Philip Zeyliger70273072025-05-28 18:26:14 +0000607
608 for (const language of languages) {
609 if (language.extensions) {
610 for (const ext of language.extensions) {
611 // Monaco extensions already include the dot, so use them directly
612 this._extensionToLanguageMap.set(ext.toLowerCase(), language.id);
613 }
614 }
615 }
616 }
617
618 return this._extensionToLanguageMap.get(extension) || "plaintext";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700619 }
620
621 /**
622 * Update editor options
623 */
624 setOptions(value: monaco.editor.IDiffEditorConstructionOptions) {
625 if (this.editor) {
626 this.editor.updateOptions(value);
David Crawshaw26f3f342025-06-14 19:58:32 +0000627 // Re-fit content after options change
628 if (this.fitEditorToContent) {
629 setTimeout(() => this.fitEditorToContent!(), 50);
630 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700631 }
632 }
633
634 /**
635 * Toggle hideUnchangedRegions feature
636 */
637 toggleHideUnchangedRegions(enabled: boolean) {
638 if (this.editor) {
639 this.editor.updateOptions({
640 hideUnchangedRegions: {
641 enabled: enabled,
642 contextLineCount: 3,
643 minimumLineCount: 3,
644 revealLineCount: 10,
645 },
646 });
David Crawshaw26f3f342025-06-14 19:58:32 +0000647 // Re-fit content after toggling
648 if (this.fitEditorToContent) {
649 setTimeout(() => this.fitEditorToContent!(), 100);
650 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700651 }
652 }
653
654 // Models for the editor
655 private originalModel?: monaco.editor.ITextModel;
656 private modifiedModel?: monaco.editor.ITextModel;
657
philip.zeyligerc0a44592025-06-15 21:24:57 -0700658 private async initializeEditor() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700659 try {
philip.zeyligerc0a44592025-06-15 21:24:57 -0700660 // Load Monaco dynamically
661 const monaco = await loadMonaco();
Autoformatter2f8464c2025-06-16 04:27:05 +0000662
philip.zeyliger7351cd92025-06-14 12:25:31 -0700663 // Disable semantic validation globally for TypeScript/JavaScript if available
664 if (monaco.languages && monaco.languages.typescript) {
665 monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
666 noSemanticValidation: true,
667 });
668 monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
669 noSemanticValidation: true,
670 });
671 }
Autoformatter8c463622025-05-16 21:54:17 +0000672
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700673 // First time initialization
674 if (!this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000675 // Create the diff editor with auto-sizing configuration
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700676 this.editor = monaco.editor.createDiffEditor(this.container.value!, {
David Crawshaw26f3f342025-06-14 19:58:32 +0000677 automaticLayout: false, // We'll resize manually
Autoformatter8c463622025-05-16 21:54:17 +0000678 readOnly: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700679 theme: "vs", // Always use light mode
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000680 renderSideBySide: !this.inline,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700681 ignoreTrimWhitespace: false,
David Crawshaw26f3f342025-06-14 19:58:32 +0000682 scrollbar: {
Philip Zeyligere0860932025-06-18 13:01:17 -0700683 // Ideally we'd handle the mouse wheel for the horizontal scrollbar,
684 // but there doesn't seem to be that option. Setting
685 // alwaysConsumeMousewheel false and handleMouseWheel true didn't
686 // work for me.
687 handleMouseWheel: false,
David Crawshaw26f3f342025-06-14 19:58:32 +0000688 },
Philip Zeyligere0860932025-06-18 13:01:17 -0700689 renderOverviewRuler: false, // Disable overview ruler
David Crawshaw26f3f342025-06-14 19:58:32 +0000690 scrollBeyondLastLine: false,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700691 // Focus on the differences by hiding unchanged regions
692 hideUnchangedRegions: {
693 enabled: true, // Enable the feature
Philip Zeyligere0860932025-06-18 13:01:17 -0700694 contextLineCount: 5, // Show 3 lines of context around each difference
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700695 minimumLineCount: 3, // Hide regions only when they're at least 3 lines
696 revealLineCount: 10, // Show 10 lines when expanding a hidden region
697 },
698 });
699
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700700 // Set up selection change event listeners for both editors
701 this.setupSelectionChangeListeners();
702
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000703 this.setupKeyboardShortcuts();
704
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700705 // If this is an editable view, set the correct read-only state for each editor
706 if (this.editableRight) {
707 // Make sure the original editor is always read-only
708 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
709 // Make sure the modified editor is editable
710 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
711 }
philip.zeyliger7351cd92025-06-14 12:25:31 -0700712
David Crawshaw26f3f342025-06-14 19:58:32 +0000713 // Set up auto-sizing
714 this.setupAutoSizing();
715
philip.zeyliger7351cd92025-06-14 12:25:31 -0700716 // Add Monaco editor to debug global
717 this.addToDebugGlobal();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700718 }
719
720 // Create or update models
721 this.updateModels();
Autoformatter8c463622025-05-16 21:54:17 +0000722 // Set up content change listener
723 this.setupContentChangeListener();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700724
philip.zeyliger7351cd92025-06-14 12:25:31 -0700725 // Fix cursor positioning issues by ensuring fonts are loaded
philip.zeyliger7351cd92025-06-14 12:25:31 -0700726 document.fonts.ready.then(() => {
727 if (this.editor) {
728 monaco.editor.remeasureFonts();
David Crawshaw26f3f342025-06-14 19:58:32 +0000729 this.fitEditorToContent();
philip.zeyliger7351cd92025-06-14 12:25:31 -0700730 }
731 });
732
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700733 // Force layout recalculation after a short delay
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700734 setTimeout(() => {
735 if (this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000736 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700737 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000738 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700739 } catch (error) {
740 console.error("Error initializing Monaco editor:", error);
741 }
742 }
743
744 /**
745 * Sets up event listeners for text selection in both editors.
746 * This enables showing the comment UI when users select text and
747 * manages the visibility of UI components based on user interactions.
748 */
749 private setupSelectionChangeListeners() {
750 try {
751 if (!this.editor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700752 return;
753 }
754
755 // Get both original and modified editors
756 const originalEditor = this.editor.getOriginalEditor();
757 const modifiedEditor = this.editor.getModifiedEditor();
758
759 if (!originalEditor || !modifiedEditor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700760 return;
761 }
762
763 // Add selection change listener to original editor
764 originalEditor.onDidChangeCursorSelection((e) => {
765 this.handleSelectionChange(e, originalEditor, "original");
766 });
767
768 // Add selection change listener to modified editor
769 modifiedEditor.onDidChangeCursorSelection((e) => {
770 this.handleSelectionChange(e, modifiedEditor, "modified");
771 });
772
773 // Create a debounced function for mouse move handling
774 let mouseMoveTimeout: number | null = null;
775 const handleMouseMove = () => {
776 // Clear any existing timeout
777 if (mouseMoveTimeout) {
778 window.clearTimeout(mouseMoveTimeout);
779 }
780
781 // If there's text selected and we're not showing the comment box, keep indicator visible
782 if (this.selectedText && !this.showCommentBox) {
783 this.showCommentIndicator = true;
784 this.requestUpdate();
785 }
786
787 // Set a new timeout to hide the indicator after a delay
788 mouseMoveTimeout = window.setTimeout(() => {
789 // Only hide if we're not showing the comment box and not actively hovering
790 if (!this.showCommentBox && !this._isHovering) {
791 this.showCommentIndicator = false;
792 this.requestUpdate();
793 }
794 }, 2000); // Hide after 2 seconds of inactivity
795 };
796
797 // Add mouse move listeners with debouncing
798 originalEditor.onMouseMove(() => handleMouseMove());
799 modifiedEditor.onMouseMove(() => handleMouseMove());
800
801 // Track hover state over the indicator and comment box
802 this._isHovering = false;
803
804 // Use the global document click handler to detect clicks outside
805 this._documentClickHandler = (e: MouseEvent) => {
806 try {
807 const target = e.target as HTMLElement;
808 const isIndicator =
809 target.matches(".comment-indicator") ||
810 !!target.closest(".comment-indicator");
811 const isCommentBox =
812 target.matches(".comment-box") || !!target.closest(".comment-box");
813
814 // If click is outside our UI elements
815 if (!isIndicator && !isCommentBox) {
816 // If we're not showing the comment box, hide the indicator
817 if (!this.showCommentBox) {
818 this.showCommentIndicator = false;
819 this.requestUpdate();
820 }
821 }
822 } catch (error) {
823 console.error("Error in document click handler:", error);
824 }
825 };
826
827 // Add the document click listener
828 document.addEventListener("click", this._documentClickHandler);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700829 } catch (error) {
830 console.error("Error setting up selection listeners:", error);
831 }
832 }
833
834 // Track mouse hover state
835 private _isHovering = false;
836
837 // Store document click handler for cleanup
838 private _documentClickHandler: ((e: MouseEvent) => void) | null = null;
839
840 /**
841 * Handle selection change events from either editor
842 */
843 private handleSelectionChange(
844 e: monaco.editor.ICursorSelectionChangedEvent,
845 editor: monaco.editor.IStandaloneCodeEditor,
846 editorType: "original" | "modified",
847 ) {
848 try {
849 // If we're not making a selection (just moving cursor), do nothing
850 if (e.selection.isEmpty()) {
851 // Don't hide indicator or box if already shown
852 if (!this.showCommentBox) {
853 this.selectedText = null;
854 this.selectionRange = null;
855 this.showCommentIndicator = false;
856 }
857 return;
858 }
859
860 // Get selected text
861 const model = editor.getModel();
862 if (!model) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700863 return;
864 }
865
866 // Make sure selection is within valid range
867 const lineCount = model.getLineCount();
868 if (
869 e.selection.startLineNumber > lineCount ||
870 e.selection.endLineNumber > lineCount
871 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700872 return;
873 }
874
875 // Store which editor is active
876 this.activeEditor = editorType;
877
878 // Store selection range
879 this.selectionRange = {
880 startLineNumber: e.selection.startLineNumber,
881 startColumn: e.selection.startColumn,
882 endLineNumber: e.selection.endLineNumber,
883 endColumn: e.selection.endColumn,
884 };
885
886 try {
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000887 // Expand selection to full lines for better context
888 const expandedSelection = {
889 startLineNumber: e.selection.startLineNumber,
890 startColumn: 1, // Start at beginning of line
891 endLineNumber: e.selection.endLineNumber,
892 endColumn: model.getLineMaxColumn(e.selection.endLineNumber), // End at end of line
893 };
894
895 // Get the selected text using the expanded selection
896 this.selectedText = model.getValueInRange(expandedSelection);
Autoformatter7ad1c7a2025-05-29 02:00:19 +0000897
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000898 // Update the selection range to reflect the full lines
899 this.selectionRange = {
900 startLineNumber: expandedSelection.startLineNumber,
901 startColumn: expandedSelection.startColumn,
902 endLineNumber: expandedSelection.endLineNumber,
903 endColumn: expandedSelection.endColumn,
904 };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700905 } catch (error) {
906 console.error("Error getting selected text:", error);
907 return;
908 }
909
910 // If there's selected text, show the indicator
911 if (this.selectedText && this.selectedText.trim() !== "") {
912 // Calculate indicator position safely
913 try {
914 // Use the editor's DOM node as positioning context
915 const editorDomNode = editor.getDomNode();
916 if (!editorDomNode) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700917 return;
918 }
919
920 // Get position from editor
921 const position = {
922 lineNumber: e.selection.endLineNumber,
923 column: e.selection.endColumn,
924 };
925
926 // Use editor's built-in method for coordinate conversion
927 const selectionCoords = editor.getScrolledVisiblePosition(position);
928
929 if (selectionCoords) {
930 // Get accurate DOM position for the selection end
931 const editorRect = editorDomNode.getBoundingClientRect();
932
933 // Calculate the actual screen position
934 const screenLeft = editorRect.left + selectionCoords.left;
935 const screenTop = editorRect.top + selectionCoords.top;
936
937 // Store absolute screen coordinates
938 this.indicatorPosition = {
939 top: screenTop,
940 left: screenLeft + 10, // Slight offset
941 };
942
943 // Check window boundaries to ensure the indicator stays visible
944 const viewportWidth = window.innerWidth;
945 const viewportHeight = window.innerHeight;
946
947 // Keep indicator within viewport bounds
948 if (this.indicatorPosition.left + 150 > viewportWidth) {
949 this.indicatorPosition.left = viewportWidth - 160;
950 }
951
952 if (this.indicatorPosition.top + 40 > viewportHeight) {
953 this.indicatorPosition.top = viewportHeight - 50;
954 }
955
956 // Show the indicator
957 this.showCommentIndicator = true;
958
959 // Request an update to ensure UI reflects changes
960 this.requestUpdate();
961 }
962 } catch (error) {
963 console.error("Error positioning comment indicator:", error);
964 }
965 }
966 } catch (error) {
967 console.error("Error handling selection change:", error);
968 }
969 }
970
971 /**
972 * Handle click on the comment indicator
973 */
974 private handleIndicatorClick(e: Event) {
975 try {
976 e.stopPropagation();
977 e.preventDefault();
978
979 this.showCommentBox = true;
980 this.commentText = ""; // Reset comment text
981
982 // Don't hide the indicator while comment box is shown
983 this.showCommentIndicator = true;
984
985 // Ensure UI updates
986 this.requestUpdate();
987 } catch (error) {
988 console.error("Error handling indicator click:", error);
989 }
990 }
991
992 /**
993 * Handle changes to the comment text
994 */
995 private handleCommentInput(e: Event) {
996 const target = e.target as HTMLTextAreaElement;
997 this.commentText = target.value;
998 }
999
1000 /**
1001 * Close the comment box
1002 */
1003 private closeCommentBox() {
1004 this.showCommentBox = false;
1005 // Also hide the indicator
1006 this.showCommentIndicator = false;
1007 }
1008
1009 /**
1010 * Submit the comment
1011 */
1012 private submitComment() {
1013 try {
1014 if (!this.selectedText || !this.commentText) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001015 return;
1016 }
1017
1018 // Get the correct filename based on active editor
1019 const fileContext =
1020 this.activeEditor === "original"
1021 ? this.originalFilename || "Original file"
1022 : this.modifiedFilename || "Modified file";
1023
1024 // Include editor info to make it clear which version was commented on
1025 const editorLabel =
1026 this.activeEditor === "original" ? "[Original]" : "[Modified]";
1027
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001028 // Add line number information if available
1029 let lineInfo = "";
1030 if (this.selectionRange) {
1031 const startLine = this.selectionRange.startLineNumber;
1032 const endLine = this.selectionRange.endLineNumber;
1033 if (startLine === endLine) {
1034 lineInfo = ` (line ${startLine})`;
1035 } else {
1036 lineInfo = ` (lines ${startLine}-${endLine})`;
1037 }
1038 }
1039
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001040 // Format the comment in a readable way
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001041 const formattedComment = `\`\`\`\n${fileContext} ${editorLabel}${lineInfo}:\n${this.selectedText}\n\`\`\`\n\n${this.commentText}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001042
1043 // Close UI before dispatching to prevent interaction conflicts
1044 this.closeCommentBox();
1045
1046 // Use setTimeout to ensure the UI has updated before sending the event
1047 setTimeout(() => {
1048 try {
1049 // Dispatch a custom event with the comment details
1050 const event = new CustomEvent("monaco-comment", {
1051 detail: {
1052 fileContext,
1053 selectedText: this.selectedText,
1054 commentText: this.commentText,
1055 formattedComment,
1056 selectionRange: this.selectionRange,
1057 activeEditor: this.activeEditor,
1058 },
1059 bubbles: true,
1060 composed: true,
1061 });
1062
1063 this.dispatchEvent(event);
1064 } catch (error) {
1065 console.error("Error dispatching comment event:", error);
1066 }
1067 }, 0);
1068 } catch (error) {
1069 console.error("Error submitting comment:", error);
1070 this.closeCommentBox();
1071 }
1072 }
1073
1074 private updateModels() {
1075 try {
1076 // Get language based on filename
1077 const originalLang = this.getLanguageForFile(this.originalFilename || "");
1078 const modifiedLang = this.getLanguageForFile(this.modifiedFilename || "");
1079
1080 // Always create new models with unique URIs based on timestamp to avoid conflicts
1081 const timestamp = new Date().getTime();
1082 // TODO: Could put filename in these URIs; unclear how they're used right now.
philip.zeyligerc0a44592025-06-15 21:24:57 -07001083 const originalUri = window.monaco!.Uri.parse(
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001084 `file:///original-${timestamp}.${originalLang}`,
1085 );
philip.zeyligerc0a44592025-06-15 21:24:57 -07001086 const modifiedUri = window.monaco!.Uri.parse(
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001087 `file:///modified-${timestamp}.${modifiedLang}`,
1088 );
1089
1090 // Store references to old models
1091 const oldOriginalModel = this.originalModel;
1092 const oldModifiedModel = this.modifiedModel;
1093
1094 // Nullify instance variables to prevent accidental use
1095 this.originalModel = undefined;
1096 this.modifiedModel = undefined;
1097
1098 // Clear the editor model first to release Monaco's internal references
1099 if (this.editor) {
1100 this.editor.setModel(null);
1101 }
1102
1103 // Now it's safe to dispose the old models
1104 if (oldOriginalModel) {
1105 oldOriginalModel.dispose();
1106 }
1107
1108 if (oldModifiedModel) {
1109 oldModifiedModel.dispose();
1110 }
1111
1112 // Create new models
philip.zeyligerc0a44592025-06-15 21:24:57 -07001113 this.originalModel = window.monaco!.editor.createModel(
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001114 this.originalCode || "",
1115 originalLang,
1116 originalUri,
1117 );
1118
philip.zeyligerc0a44592025-06-15 21:24:57 -07001119 this.modifiedModel = window.monaco!.editor.createModel(
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001120 this.modifiedCode || "",
1121 modifiedLang,
1122 modifiedUri,
1123 );
1124
1125 // Set the new models on the editor
1126 if (this.editor) {
1127 this.editor.setModel({
1128 original: this.originalModel,
1129 modified: this.modifiedModel,
1130 });
Autoformatter9abf8032025-06-14 23:24:08 +00001131
David Crawshaw26f3f342025-06-14 19:58:32 +00001132 // Set initial hideUnchangedRegions state (default to enabled/collapsed)
1133 this.editor.updateOptions({
1134 hideUnchangedRegions: {
1135 enabled: true, // Default to collapsed state
1136 contextLineCount: 3,
1137 minimumLineCount: 3,
1138 revealLineCount: 10,
1139 },
1140 });
Autoformatter9abf8032025-06-14 23:24:08 +00001141
David Crawshaw26f3f342025-06-14 19:58:32 +00001142 // Fit content after setting new models
1143 if (this.fitEditorToContent) {
1144 setTimeout(() => this.fitEditorToContent!(), 50);
1145 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001146 }
1147 this.setupContentChangeListener();
1148 } catch (error) {
1149 console.error("Error updating Monaco models:", error);
1150 }
1151 }
1152
philip.zeyligerc0a44592025-06-15 21:24:57 -07001153 async updated(changedProperties: Map<string, any>) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001154 // If any relevant properties changed, just update the models
1155 if (
1156 changedProperties.has("originalCode") ||
1157 changedProperties.has("modifiedCode") ||
1158 changedProperties.has("originalFilename") ||
1159 changedProperties.has("modifiedFilename") ||
1160 changedProperties.has("editableRight")
1161 ) {
1162 if (this.editor) {
1163 this.updateModels();
1164
David Crawshaw26f3f342025-06-14 19:58:32 +00001165 // Force auto-sizing after model updates
1166 // Use a slightly longer delay to ensure layout is stable
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001167 setTimeout(() => {
David Crawshaw26f3f342025-06-14 19:58:32 +00001168 if (this.fitEditorToContent) {
1169 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001170 }
David Crawshaw26f3f342025-06-14 19:58:32 +00001171 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001172 } else {
1173 // If the editor isn't initialized yet but we received content,
1174 // initialize it now
philip.zeyligerc0a44592025-06-15 21:24:57 -07001175 await this.initializeEditor();
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001176 }
1177 }
1178 }
1179
David Crawshaw26f3f342025-06-14 19:58:32 +00001180 // Set up auto-sizing for multi-file diff view
1181 private setupAutoSizing() {
1182 if (!this.editor) return;
1183
1184 const fitContent = () => {
1185 try {
1186 const originalEditor = this.editor!.getOriginalEditor();
1187 const modifiedEditor = this.editor!.getModifiedEditor();
Autoformatter9abf8032025-06-14 23:24:08 +00001188
David Crawshaw26f3f342025-06-14 19:58:32 +00001189 const originalHeight = originalEditor.getContentHeight();
1190 const modifiedHeight = modifiedEditor.getContentHeight();
Autoformatter9abf8032025-06-14 23:24:08 +00001191
David Crawshaw26f3f342025-06-14 19:58:32 +00001192 // Use the maximum height of both editors, plus some padding
1193 const maxHeight = Math.max(originalHeight, modifiedHeight) + 18; // 1 blank line bottom padding
Autoformatter9abf8032025-06-14 23:24:08 +00001194
David Crawshaw26f3f342025-06-14 19:58:32 +00001195 // Set both container and host height to enable proper scrolling
1196 if (this.container.value) {
1197 // Set explicit heights on both container and host
1198 this.container.value.style.height = `${maxHeight}px`;
1199 this.style.height = `${maxHeight}px`; // Update host element height
Autoformatter9abf8032025-06-14 23:24:08 +00001200
David Crawshaw26f3f342025-06-14 19:58:32 +00001201 // Emit the height change event BEFORE calling layout
1202 // This ensures parent containers resize first
Autoformatter9abf8032025-06-14 23:24:08 +00001203 this.dispatchEvent(
1204 new CustomEvent("monaco-height-changed", {
1205 detail: { height: maxHeight },
1206 bubbles: true,
1207 composed: true,
1208 }),
1209 );
1210
David Crawshaw26f3f342025-06-14 19:58:32 +00001211 // Layout after both this component and parents have updated
1212 setTimeout(() => {
1213 if (this.editor && this.container.value) {
1214 // Use explicit dimensions to ensure Monaco uses full available space
David Crawshawdba26b52025-06-15 00:33:45 +00001215 // Use clientWidth instead of offsetWidth to avoid border overflow
1216 const width = this.container.value.clientWidth;
David Crawshaw26f3f342025-06-14 19:58:32 +00001217 this.editor.layout({
1218 width: width,
Autoformatter9abf8032025-06-14 23:24:08 +00001219 height: maxHeight,
David Crawshaw26f3f342025-06-14 19:58:32 +00001220 });
1221 }
1222 }, 10);
1223 }
1224 } catch (error) {
Autoformatter9abf8032025-06-14 23:24:08 +00001225 console.error("Error in fitContent:", error);
David Crawshaw26f3f342025-06-14 19:58:32 +00001226 }
1227 };
1228
1229 // Store the fit function for external access
1230 this.fitEditorToContent = fitContent;
1231
1232 // Set up listeners for content size changes
1233 this.editor.getOriginalEditor().onDidContentSizeChange(fitContent);
1234 this.editor.getModifiedEditor().onDidContentSizeChange(fitContent);
1235
1236 // Initial fit
1237 fitContent();
1238 }
1239
1240 private fitEditorToContent: (() => void) | null = null;
1241
David Crawshawe2954ce2025-06-15 00:06:34 +00001242 /**
1243 * Set up window resize handler to ensure Monaco editor adapts to browser window changes
1244 */
1245 private setupWindowResizeHandler() {
1246 // Create a debounced resize handler to avoid too many layout calls
1247 let resizeTimeout: number | null = null;
Autoformatterad15b6c2025-06-15 00:29:26 +00001248
David Crawshawe2954ce2025-06-15 00:06:34 +00001249 this._windowResizeHandler = () => {
1250 // Clear any existing timeout
1251 if (resizeTimeout) {
1252 window.clearTimeout(resizeTimeout);
1253 }
Autoformatterad15b6c2025-06-15 00:29:26 +00001254
David Crawshawe2954ce2025-06-15 00:06:34 +00001255 // Debounce the resize to avoid excessive layout calls
1256 resizeTimeout = window.setTimeout(() => {
1257 if (this.editor && this.container.value) {
1258 // Trigger layout recalculation
1259 if (this.fitEditorToContent) {
1260 this.fitEditorToContent();
1261 } else {
1262 // Fallback: just trigger a layout with current container dimensions
David Crawshawdba26b52025-06-15 00:33:45 +00001263 // Use clientWidth/Height instead of offsetWidth/Height to avoid border overflow
1264 const width = this.container.value.clientWidth;
1265 const height = this.container.value.clientHeight;
David Crawshawe2954ce2025-06-15 00:06:34 +00001266 this.editor.layout({ width, height });
1267 }
1268 }
1269 }, 100); // 100ms debounce
1270 };
Autoformatterad15b6c2025-06-15 00:29:26 +00001271
David Crawshawe2954ce2025-06-15 00:06:34 +00001272 // Add the event listener
Autoformatterad15b6c2025-06-15 00:29:26 +00001273 window.addEventListener("resize", this._windowResizeHandler);
David Crawshawe2954ce2025-06-15 00:06:34 +00001274 }
1275
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001276 // Add resize observer to ensure editor resizes when container changes
philip.zeyligerc0a44592025-06-15 21:24:57 -07001277 async firstUpdated() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001278 // Initialize the editor
philip.zeyligerc0a44592025-06-15 21:24:57 -07001279 await this.initializeEditor();
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001280
David Crawshawe2954ce2025-06-15 00:06:34 +00001281 // Set up window resize handler to ensure Monaco editor adapts to browser window changes
1282 this.setupWindowResizeHandler();
1283
David Crawshaw26f3f342025-06-14 19:58:32 +00001284 // For multi-file diff, we don't use ResizeObserver since we control the size
1285 // Instead, we rely on auto-sizing based on content
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001286
1287 // If editable, set up edit mode and content change listener
1288 if (this.editableRight && this.editor) {
1289 // Ensure the original editor is read-only
1290 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
1291 // Ensure the modified editor is editable
1292 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
1293 }
1294 }
1295
1296 private _resizeObserver: ResizeObserver | null = null;
David Crawshawe2954ce2025-06-15 00:06:34 +00001297 private _windowResizeHandler: (() => void) | null = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001298
philip.zeyliger7351cd92025-06-14 12:25:31 -07001299 /**
1300 * Add this Monaco editor instance to the global debug object
1301 * This allows inspection and debugging via browser console
1302 */
1303 private addToDebugGlobal() {
1304 try {
1305 // Initialize the debug global if it doesn't exist
1306 if (!(window as any).sketchDebug) {
1307 (window as any).sketchDebug = {
philip.zeyligerc0a44592025-06-15 21:24:57 -07001308 monaco: window.monaco!,
philip.zeyliger7351cd92025-06-14 12:25:31 -07001309 editors: [],
1310 remeasureFonts: () => {
philip.zeyligerc0a44592025-06-15 21:24:57 -07001311 window.monaco!.editor.remeasureFonts();
philip.zeyliger7351cd92025-06-14 12:25:31 -07001312 (window as any).sketchDebug.editors.forEach(
1313 (editor: any, index: number) => {
1314 if (editor && editor.layout) {
1315 editor.layout();
1316 }
1317 },
1318 );
1319 },
1320 layoutAll: () => {
1321 (window as any).sketchDebug.editors.forEach(
1322 (editor: any, index: number) => {
1323 if (editor && editor.layout) {
1324 editor.layout();
1325 }
1326 },
1327 );
1328 },
1329 getActiveEditors: () => {
1330 return (window as any).sketchDebug.editors.filter(
1331 (editor: any) => editor !== null,
1332 );
1333 },
1334 };
1335 }
1336
1337 // Add this editor to the debug collection
1338 if (this.editor) {
1339 (window as any).sketchDebug.editors.push(this.editor);
1340 }
1341 } catch (error) {
1342 console.error("Error adding Monaco editor to debug global:", error);
1343 }
1344 }
1345
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001346 disconnectedCallback() {
1347 super.disconnectedCallback();
1348
1349 try {
philip.zeyliger7351cd92025-06-14 12:25:31 -07001350 // Remove editor from debug global before disposal
1351 if (this.editor && (window as any).sketchDebug?.editors) {
1352 const index = (window as any).sketchDebug.editors.indexOf(this.editor);
1353 if (index > -1) {
1354 (window as any).sketchDebug.editors[index] = null;
1355 }
1356 }
1357
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001358 // Clean up resources when element is removed
1359 if (this.editor) {
1360 this.editor.dispose();
1361 this.editor = undefined;
1362 }
1363
1364 // Dispose models to prevent memory leaks
1365 if (this.originalModel) {
1366 this.originalModel.dispose();
1367 this.originalModel = undefined;
1368 }
1369
1370 if (this.modifiedModel) {
1371 this.modifiedModel.dispose();
1372 this.modifiedModel = undefined;
1373 }
1374
David Crawshaw26f3f342025-06-14 19:58:32 +00001375 // Clean up resize observer (if any)
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001376 if (this._resizeObserver) {
1377 this._resizeObserver.disconnect();
1378 this._resizeObserver = null;
1379 }
Autoformatter9abf8032025-06-14 23:24:08 +00001380
David Crawshaw26f3f342025-06-14 19:58:32 +00001381 // Clear the fit function reference
1382 this.fitEditorToContent = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001383
1384 // Remove document click handler if set
1385 if (this._documentClickHandler) {
1386 document.removeEventListener("click", this._documentClickHandler);
1387 this._documentClickHandler = null;
1388 }
Autoformatterad15b6c2025-06-15 00:29:26 +00001389
David Crawshawe2954ce2025-06-15 00:06:34 +00001390 // Remove window resize handler if set
1391 if (this._windowResizeHandler) {
Autoformatterad15b6c2025-06-15 00:29:26 +00001392 window.removeEventListener("resize", this._windowResizeHandler);
David Crawshawe2954ce2025-06-15 00:06:34 +00001393 this._windowResizeHandler = null;
1394 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001395 } catch (error) {
1396 console.error("Error in disconnectedCallback:", error);
1397 }
1398 }
1399
1400 // disconnectedCallback implementation is defined below
1401}
1402
1403declare global {
1404 interface HTMLElementTagNameMap {
1405 "sketch-monaco-view": CodeDiffEditor;
1406 }
1407}