blob: cae5aca688ef20ceee31a78f81020f2e66e1a712 [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
7import * as monaco from "monaco-editor";
8
9// Configure Monaco to use local workers with correct relative paths
10
11// Define Monaco CSS styles as a string constant
12const monacoStyles = `
13 /* Import Monaco editor styles */
14 @import url('./static/monaco/min/vs/editor/editor.main.css');
15
16 /* Codicon font is now defined globally in sketch-app-shell.css */
17
18 /* Custom Monaco styles */
19 .monaco-editor {
20 width: 100%;
21 height: 100%;
22 }
23
philip.zeyliger7351cd92025-06-14 12:25:31 -070024 // /* Custom font stack - ensure we have good monospace fonts */
25 // .monaco-editor .view-lines,
26 // .monaco-editor .view-line,
27 // .monaco-editor-pane,
28 // .monaco-editor .inputarea {
29 // font-family: "Menlo", "Monaco", "Consolas", "Courier New", monospace !important;
30 // font-size: 13px !important;
31 // font-feature-settings: "liga" 0, "calt" 0 !important;
32 // line-height: 1.5 !important;
33 // }
Philip Zeyliger272a90e2025-05-16 14:49:51 -070034
35 /* Ensure light theme colors */
36 .monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input {
37 background-color: var(--monaco-editor-bg, #ffffff) !important;
38 }
39
40 .monaco-editor .margin {
41 background-color: var(--monaco-editor-margin, #f5f5f5) !important;
42 }
David Crawshawe2954ce2025-06-15 00:06:34 +000043
44 /* Hide all scrollbars completely */
45 .monaco-editor .scrollbar,
46 .monaco-editor .scroll-decoration,
47 .monaco-editor .invisible.scrollbar,
48 .monaco-editor .slider,
49 .monaco-editor .vertical.scrollbar,
50 .monaco-editor .horizontal.scrollbar {
51 display: none !important;
52 visibility: hidden !important;
53 width: 0 !important;
54 height: 0 !important;
55 }
56
57 /* Ensure content area takes full width/height without scrollbar space */
58 .monaco-editor .monaco-scrollable-element {
59 /* Remove any padding/margin that might be reserved for scrollbars */
60 padding-right: 0 !important;
61 padding-bottom: 0 !important;
62 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -070063`;
64
65// Configure Monaco to use local workers with correct relative paths
66// Monaco looks for this global configuration to determine how to load web workers
67// @ts-ignore - MonacoEnvironment is added to the global scope at runtime
68self.MonacoEnvironment = {
69 getWorkerUrl: function (_moduleId, label) {
70 if (label === "json") {
71 return "./static/json.worker.js";
72 }
73 if (label === "css" || label === "scss" || label === "less") {
74 return "./static/css.worker.js";
75 }
76 if (label === "html" || label === "handlebars" || label === "razor") {
77 return "./static/html.worker.js";
78 }
79 if (label === "typescript" || label === "javascript") {
80 return "./static/ts.worker.js";
81 }
82 return "./static/editor.worker.js";
83 },
84};
85
86@customElement("sketch-monaco-view")
87export class CodeDiffEditor extends LitElement {
88 // Editable state
89 @property({ type: Boolean, attribute: "editable-right" })
90 editableRight?: boolean;
91 private container: Ref<HTMLElement> = createRef();
92 editor?: monaco.editor.IStandaloneDiffEditor;
Philip Zeyliger272a90e2025-05-16 14:49:51 -070093
94 // Save state properties
95 @state() private saveState: "idle" | "modified" | "saving" | "saved" = "idle";
96 @state() private debounceSaveTimeout: number | null = null;
97 @state() private lastSavedContent: string = "";
98 @property() originalCode?: string = "// Original code here";
99 @property() modifiedCode?: string = "// Modified code here";
100 @property() originalFilename?: string = "original.js";
101 @property() modifiedFilename?: string = "modified.js";
102
103 /* Selected text and indicators */
104 @state()
105 private selectedText: string | null = null;
106
107 @state()
108 private selectionRange: {
109 startLineNumber: number;
110 startColumn: number;
111 endLineNumber: number;
112 endColumn: number;
113 } | null = null;
114
115 @state()
116 private showCommentIndicator: boolean = false;
117
118 @state()
119 private indicatorPosition: { top: number; left: number } = {
120 top: 0,
121 left: 0,
122 };
123
124 @state()
125 private showCommentBox: boolean = false;
126
127 @state()
128 private commentText: string = "";
129
130 @state()
131 private activeEditor: "original" | "modified" = "modified"; // Track which editor is active
132
133 // Custom event to request save action from external components
134 private requestSave() {
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000135 if (!this.editableRight || this.saveState !== "modified") return;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700136
137 this.saveState = "saving";
138
139 // Get current content from modified editor
140 const modifiedContent = this.modifiedModel?.getValue() || "";
141
142 // Create and dispatch the save event
143 const saveEvent = new CustomEvent("monaco-save", {
144 detail: {
145 path: this.modifiedFilename,
146 content: modifiedContent,
147 },
148 bubbles: true,
149 composed: true,
150 });
151
152 this.dispatchEvent(saveEvent);
153 }
154
155 // Method to be called from parent when save is complete
156 public notifySaveComplete(success: boolean) {
157 if (success) {
158 this.saveState = "saved";
159 // Update last saved content
160 this.lastSavedContent = this.modifiedModel?.getValue() || "";
161 // Reset to idle after a delay
162 setTimeout(() => {
163 this.saveState = "idle";
164 }, 2000);
165 } else {
166 // Return to modified state on error
167 this.saveState = "modified";
168 }
169 }
170
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000171 // Rescue people with strong save-constantly habits
172 private setupKeyboardShortcuts() {
173 if (!this.editor) return;
174 const modifiedEditor = this.editor.getModifiedEditor();
175 if (!modifiedEditor) return;
176
177 modifiedEditor.addCommand(
178 monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
179 () => {
180 this.requestSave();
Autoformatter57893c22025-05-29 13:49:53 +0000181 },
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000182 );
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000183 }
184
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700185 // Setup content change listener for debounced save
186 private setupContentChangeListener() {
187 if (!this.editor || !this.editableRight) return;
188
189 const modifiedEditor = this.editor.getModifiedEditor();
190 if (!modifiedEditor || !modifiedEditor.getModel()) return;
191
192 // Store initial content
193 this.lastSavedContent = modifiedEditor.getModel()!.getValue();
194
195 // Listen for content changes
196 modifiedEditor.getModel()!.onDidChangeContent(() => {
197 const currentContent = modifiedEditor.getModel()!.getValue();
198
199 // Check if content has actually changed from last saved state
200 if (currentContent !== this.lastSavedContent) {
201 this.saveState = "modified";
202
203 // Debounce save request
204 if (this.debounceSaveTimeout) {
205 window.clearTimeout(this.debounceSaveTimeout);
206 }
207
208 this.debounceSaveTimeout = window.setTimeout(() => {
209 this.requestSave();
210 this.debounceSaveTimeout = null;
211 }, 1000); // 1 second debounce
212 }
213 });
214 }
215
216 static styles = css`
217 /* Save indicator styles */
218 .save-indicator {
219 position: absolute;
220 top: 4px;
221 right: 4px;
222 padding: 3px 8px;
223 border-radius: 3px;
224 font-size: 12px;
225 font-family: system-ui, sans-serif;
226 color: white;
227 z-index: 100;
228 opacity: 0.9;
229 pointer-events: none;
230 transition: opacity 0.3s ease;
231 }
232
Philip Zeyligere89b3082025-05-29 03:16:06 +0000233 .save-indicator.idle {
234 background-color: #6c757d;
235 }
236
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700237 .save-indicator.modified {
238 background-color: #f0ad4e;
239 }
240
241 .save-indicator.saving {
242 background-color: #5bc0de;
243 }
244
245 .save-indicator.saved {
246 background-color: #5cb85c;
247 }
248
249 /* Editor host styles */
250 :host {
251 --editor-width: 100%;
252 --editor-height: 100%;
253 display: flex;
David Crawshaw26f3f342025-06-14 19:58:32 +0000254 flex: none; /* Don't grow/shrink - size is determined by content */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700255 min-height: 0; /* Critical for flex layout */
256 position: relative; /* Establish positioning context */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700257 width: 100%; /* Take full width */
David Crawshaw26f3f342025-06-14 19:58:32 +0000258 /* Height will be set dynamically by setupAutoSizing */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700259 }
260 main {
David Crawshaw26f3f342025-06-14 19:58:32 +0000261 width: 100%;
262 height: 100%; /* Fill the host element completely */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700263 border: 1px solid #e0e0e0;
David Crawshaw26f3f342025-06-14 19:58:32 +0000264 flex: none; /* Size determined by parent */
265 min-height: 200px; /* Ensure a minimum height for the editor */
266 /* Remove absolute positioning - use normal block layout */
267 position: relative;
268 display: block;
David Crawshawdba26b52025-06-15 00:33:45 +0000269 box-sizing: border-box; /* Include border in width calculation */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700270 }
271
272 /* Comment indicator and box styles */
273 .comment-indicator {
274 position: fixed;
275 background-color: rgba(66, 133, 244, 0.9);
276 color: white;
277 border-radius: 3px;
278 padding: 3px 8px;
279 font-size: 12px;
280 cursor: pointer;
281 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
282 z-index: 10000;
283 animation: fadeIn 0.2s ease-in-out;
284 display: flex;
285 align-items: center;
286 gap: 4px;
287 pointer-events: all;
288 }
289
290 .comment-indicator:hover {
291 background-color: rgba(66, 133, 244, 1);
292 }
293
294 .comment-indicator span {
295 line-height: 1;
296 }
297
298 .comment-box {
299 position: fixed;
300 background-color: white;
301 border: 1px solid #ddd;
302 border-radius: 4px;
303 box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
304 padding: 12px;
305 z-index: 10001;
306 width: 350px;
307 animation: fadeIn 0.2s ease-in-out;
308 max-height: 80vh;
309 overflow-y: auto;
310 }
311
312 .comment-box-header {
313 display: flex;
314 justify-content: space-between;
315 align-items: center;
316 margin-bottom: 8px;
317 }
318
319 .comment-box-header h3 {
320 margin: 0;
321 font-size: 14px;
322 font-weight: 500;
323 }
324
325 .close-button {
326 background: none;
327 border: none;
328 cursor: pointer;
329 font-size: 16px;
330 color: #666;
331 padding: 2px 6px;
332 }
333
334 .close-button:hover {
335 color: #333;
336 }
337
338 .selected-text-preview {
339 background-color: #f5f5f5;
340 border: 1px solid #eee;
341 border-radius: 3px;
342 padding: 8px;
343 margin-bottom: 10px;
344 font-family: monospace;
345 font-size: 12px;
346 max-height: 80px;
347 overflow-y: auto;
348 white-space: pre-wrap;
349 word-break: break-all;
350 }
351
352 .comment-textarea {
353 width: 100%;
354 min-height: 80px;
355 padding: 8px;
356 border: 1px solid #ddd;
357 border-radius: 3px;
358 resize: vertical;
359 font-family: inherit;
360 margin-bottom: 10px;
361 box-sizing: border-box;
362 }
363
364 .comment-actions {
365 display: flex;
366 justify-content: flex-end;
367 gap: 8px;
368 }
369
370 .comment-actions button {
371 padding: 6px 12px;
372 border-radius: 3px;
373 cursor: pointer;
374 font-size: 12px;
375 }
376
377 .cancel-button {
378 background-color: transparent;
379 border: 1px solid #ddd;
380 }
381
382 .cancel-button:hover {
383 background-color: #f5f5f5;
384 }
385
386 .submit-button {
387 background-color: #4285f4;
388 color: white;
389 border: none;
390 }
391
392 .submit-button:hover {
393 background-color: #3367d6;
394 }
395
396 @keyframes fadeIn {
397 from {
398 opacity: 0;
399 }
400 to {
401 opacity: 1;
402 }
403 }
404 `;
405
406 render() {
407 return html`
408 <style>
409 ${monacoStyles}
410 </style>
411 <main ${ref(this.container)}></main>
412
413 <!-- Save indicator - shown when editing -->
Philip Zeyligere89b3082025-05-29 03:16:06 +0000414 ${this.editableRight
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700415 ? html`
416 <div class="save-indicator ${this.saveState}">
Philip Zeyligere89b3082025-05-29 03:16:06 +0000417 ${this.saveState === "idle"
418 ? "Editable"
419 : this.saveState === "modified"
420 ? "Modified..."
421 : this.saveState === "saving"
422 ? "Saving..."
423 : this.saveState === "saved"
424 ? "Saved"
425 : ""}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700426 </div>
427 `
428 : ""}
429
430 <!-- Comment indicator - shown when text is selected -->
431 ${this.showCommentIndicator
432 ? html`
433 <div
434 class="comment-indicator"
435 style="top: ${this.indicatorPosition.top}px; left: ${this
436 .indicatorPosition.left}px;"
437 @click="${this.handleIndicatorClick}"
438 @mouseenter="${() => {
439 this._isHovering = true;
440 }}"
441 @mouseleave="${() => {
442 this._isHovering = false;
443 }}"
444 >
445 <span>💬</span>
446 <span>Add comment</span>
447 </div>
448 `
449 : ""}
450
451 <!-- Comment box - shown when indicator is clicked -->
452 ${this.showCommentBox
453 ? html`
454 <div
455 class="comment-box"
456 style="${this.calculateCommentBoxPosition()}"
457 @mouseenter="${() => {
458 this._isHovering = true;
459 }}"
460 @mouseleave="${() => {
461 this._isHovering = false;
462 }}"
463 >
464 <div class="comment-box-header">
465 <h3>Add comment</h3>
466 <button class="close-button" @click="${this.closeCommentBox}">
467 ×
468 </button>
469 </div>
470 <div class="selected-text-preview">${this.selectedText}</div>
471 <textarea
472 class="comment-textarea"
473 placeholder="Type your comment here..."
474 .value="${this.commentText}"
475 @input="${this.handleCommentInput}"
476 ></textarea>
477 <div class="comment-actions">
478 <button class="cancel-button" @click="${this.closeCommentBox}">
479 Cancel
480 </button>
481 <button class="submit-button" @click="${this.submitComment}">
Josh Bleecher Snyderafeafea2025-05-23 20:27:39 +0000482 Add
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700483 </button>
484 </div>
485 </div>
486 `
487 : ""}
488 `;
489 }
490
491 /**
492 * Calculate the optimal position for the comment box to keep it in view
493 */
494 private calculateCommentBoxPosition(): string {
495 // Get viewport dimensions
496 const viewportWidth = window.innerWidth;
497 const viewportHeight = window.innerHeight;
498
499 // Default position (below indicator)
500 let top = this.indicatorPosition.top + 30;
501 let left = this.indicatorPosition.left;
502
503 // Estimated box dimensions
504 const boxWidth = 350;
505 const boxHeight = 300;
506
507 // Check if box would go off the right edge
508 if (left + boxWidth > viewportWidth) {
509 left = viewportWidth - boxWidth - 20; // Keep 20px margin
510 }
511
512 // Check if box would go off the bottom
513 const bottomSpace = viewportHeight - top;
514 if (bottomSpace < boxHeight) {
515 // Not enough space below, try to position above if possible
516 if (this.indicatorPosition.top > boxHeight) {
517 // Position above the indicator
518 top = this.indicatorPosition.top - boxHeight - 10;
519 } else {
520 // Not enough space above either, position at top of viewport with margin
521 top = 10;
522 }
523 }
524
525 // Ensure box is never positioned off-screen
526 top = Math.max(10, top);
527 left = Math.max(10, left);
528
529 return `top: ${top}px; left: ${left}px;`;
530 }
531
532 setOriginalCode(code: string, filename?: string) {
533 this.originalCode = code;
534 if (filename) {
535 this.originalFilename = filename;
536 }
537
538 // Update the model if the editor is initialized
539 if (this.editor) {
540 const model = this.editor.getOriginalEditor().getModel();
541 if (model) {
542 model.setValue(code);
543 if (filename) {
544 monaco.editor.setModelLanguage(
545 model,
546 this.getLanguageForFile(filename),
547 );
548 }
549 }
550 }
551 }
552
553 setModifiedCode(code: string, filename?: string) {
554 this.modifiedCode = code;
555 if (filename) {
556 this.modifiedFilename = filename;
557 }
558
559 // Update the model if the editor is initialized
560 if (this.editor) {
561 const model = this.editor.getModifiedEditor().getModel();
562 if (model) {
563 model.setValue(code);
564 if (filename) {
565 monaco.editor.setModelLanguage(
566 model,
567 this.getLanguageForFile(filename),
568 );
569 }
570 }
571 }
572 }
573
Philip Zeyliger70273072025-05-28 18:26:14 +0000574 private _extensionToLanguageMap: Map<string, string> | null = null;
575
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700576 private getLanguageForFile(filename: string): string {
Philip Zeyliger70273072025-05-28 18:26:14 +0000577 // Get the file extension (including the dot for exact matching)
578 const extension = "." + (filename.split(".").pop()?.toLowerCase() || "");
579
580 // Build the extension-to-language map on first use
581 if (!this._extensionToLanguageMap) {
582 this._extensionToLanguageMap = new Map();
583 const languages = monaco.languages.getLanguages();
584
585 for (const language of languages) {
586 if (language.extensions) {
587 for (const ext of language.extensions) {
588 // Monaco extensions already include the dot, so use them directly
589 this._extensionToLanguageMap.set(ext.toLowerCase(), language.id);
590 }
591 }
592 }
593 }
594
595 return this._extensionToLanguageMap.get(extension) || "plaintext";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700596 }
597
598 /**
599 * Update editor options
600 */
601 setOptions(value: monaco.editor.IDiffEditorConstructionOptions) {
602 if (this.editor) {
603 this.editor.updateOptions(value);
David Crawshaw26f3f342025-06-14 19:58:32 +0000604 // Re-fit content after options change
605 if (this.fitEditorToContent) {
606 setTimeout(() => this.fitEditorToContent!(), 50);
607 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700608 }
609 }
610
611 /**
612 * Toggle hideUnchangedRegions feature
613 */
614 toggleHideUnchangedRegions(enabled: boolean) {
615 if (this.editor) {
616 this.editor.updateOptions({
617 hideUnchangedRegions: {
618 enabled: enabled,
619 contextLineCount: 3,
620 minimumLineCount: 3,
621 revealLineCount: 10,
622 },
623 });
David Crawshaw26f3f342025-06-14 19:58:32 +0000624 // Re-fit content after toggling
625 if (this.fitEditorToContent) {
626 setTimeout(() => this.fitEditorToContent!(), 100);
627 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700628 }
629 }
630
631 // Models for the editor
632 private originalModel?: monaco.editor.ITextModel;
633 private modifiedModel?: monaco.editor.ITextModel;
634
635 private initializeEditor() {
636 try {
philip.zeyliger7351cd92025-06-14 12:25:31 -0700637 // Disable semantic validation globally for TypeScript/JavaScript if available
638 if (monaco.languages && monaco.languages.typescript) {
639 monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
640 noSemanticValidation: true,
641 });
642 monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
643 noSemanticValidation: true,
644 });
645 }
Autoformatter8c463622025-05-16 21:54:17 +0000646
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700647 // First time initialization
648 if (!this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000649 // Create the diff editor with auto-sizing configuration
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700650 this.editor = monaco.editor.createDiffEditor(this.container.value!, {
David Crawshaw26f3f342025-06-14 19:58:32 +0000651 automaticLayout: false, // We'll resize manually
Autoformatter8c463622025-05-16 21:54:17 +0000652 readOnly: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700653 theme: "vs", // Always use light mode
654 renderSideBySide: true,
655 ignoreTrimWhitespace: false,
David Crawshawf00c7b12025-06-15 00:24:46 +0000656 renderOverviewRuler: false, // Disable the overview ruler
David Crawshaw26f3f342025-06-14 19:58:32 +0000657 scrollbar: {
Autoformatter9abf8032025-06-14 23:24:08 +0000658 vertical: "hidden",
659 horizontal: "hidden",
David Crawshaw26f3f342025-06-14 19:58:32 +0000660 handleMouseWheel: false, // Let outer scroller eat the wheel
David Crawshawe2954ce2025-06-15 00:06:34 +0000661 useShadows: false, // Disable scrollbar shadows
662 verticalHasArrows: false, // Remove scrollbar arrows
663 horizontalHasArrows: false, // Remove scrollbar arrows
664 verticalScrollbarSize: 0, // Set scrollbar track width to 0
665 horizontalScrollbarSize: 0, // Set scrollbar track height to 0
David Crawshaw26f3f342025-06-14 19:58:32 +0000666 },
667 minimap: { enabled: false },
668 overviewRulerLanes: 0,
669 scrollBeyondLastLine: false,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700670 // Focus on the differences by hiding unchanged regions
671 hideUnchangedRegions: {
672 enabled: true, // Enable the feature
673 contextLineCount: 3, // Show 3 lines of context around each difference
674 minimumLineCount: 3, // Hide regions only when they're at least 3 lines
675 revealLineCount: 10, // Show 10 lines when expanding a hidden region
676 },
677 });
678
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700679 // Set up selection change event listeners for both editors
680 this.setupSelectionChangeListeners();
681
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000682 this.setupKeyboardShortcuts();
683
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700684 // If this is an editable view, set the correct read-only state for each editor
685 if (this.editableRight) {
686 // Make sure the original editor is always read-only
687 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
688 // Make sure the modified editor is editable
689 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
690 }
philip.zeyliger7351cd92025-06-14 12:25:31 -0700691
David Crawshaw26f3f342025-06-14 19:58:32 +0000692 // Set up auto-sizing
693 this.setupAutoSizing();
694
philip.zeyliger7351cd92025-06-14 12:25:31 -0700695 // Add Monaco editor to debug global
696 this.addToDebugGlobal();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700697 }
698
699 // Create or update models
700 this.updateModels();
Autoformatter8c463622025-05-16 21:54:17 +0000701 // Set up content change listener
702 this.setupContentChangeListener();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700703
philip.zeyliger7351cd92025-06-14 12:25:31 -0700704 // Fix cursor positioning issues by ensuring fonts are loaded
philip.zeyliger7351cd92025-06-14 12:25:31 -0700705 document.fonts.ready.then(() => {
706 if (this.editor) {
707 monaco.editor.remeasureFonts();
David Crawshaw26f3f342025-06-14 19:58:32 +0000708 this.fitEditorToContent();
philip.zeyliger7351cd92025-06-14 12:25:31 -0700709 }
710 });
711
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700712 // Force layout recalculation after a short delay
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700713 setTimeout(() => {
714 if (this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000715 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700716 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000717 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700718 } catch (error) {
719 console.error("Error initializing Monaco editor:", error);
720 }
721 }
722
723 /**
724 * Sets up event listeners for text selection in both editors.
725 * This enables showing the comment UI when users select text and
726 * manages the visibility of UI components based on user interactions.
727 */
728 private setupSelectionChangeListeners() {
729 try {
730 if (!this.editor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700731 return;
732 }
733
734 // Get both original and modified editors
735 const originalEditor = this.editor.getOriginalEditor();
736 const modifiedEditor = this.editor.getModifiedEditor();
737
738 if (!originalEditor || !modifiedEditor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700739 return;
740 }
741
742 // Add selection change listener to original editor
743 originalEditor.onDidChangeCursorSelection((e) => {
744 this.handleSelectionChange(e, originalEditor, "original");
745 });
746
747 // Add selection change listener to modified editor
748 modifiedEditor.onDidChangeCursorSelection((e) => {
749 this.handleSelectionChange(e, modifiedEditor, "modified");
750 });
751
752 // Create a debounced function for mouse move handling
753 let mouseMoveTimeout: number | null = null;
754 const handleMouseMove = () => {
755 // Clear any existing timeout
756 if (mouseMoveTimeout) {
757 window.clearTimeout(mouseMoveTimeout);
758 }
759
760 // If there's text selected and we're not showing the comment box, keep indicator visible
761 if (this.selectedText && !this.showCommentBox) {
762 this.showCommentIndicator = true;
763 this.requestUpdate();
764 }
765
766 // Set a new timeout to hide the indicator after a delay
767 mouseMoveTimeout = window.setTimeout(() => {
768 // Only hide if we're not showing the comment box and not actively hovering
769 if (!this.showCommentBox && !this._isHovering) {
770 this.showCommentIndicator = false;
771 this.requestUpdate();
772 }
773 }, 2000); // Hide after 2 seconds of inactivity
774 };
775
776 // Add mouse move listeners with debouncing
777 originalEditor.onMouseMove(() => handleMouseMove());
778 modifiedEditor.onMouseMove(() => handleMouseMove());
779
780 // Track hover state over the indicator and comment box
781 this._isHovering = false;
782
783 // Use the global document click handler to detect clicks outside
784 this._documentClickHandler = (e: MouseEvent) => {
785 try {
786 const target = e.target as HTMLElement;
787 const isIndicator =
788 target.matches(".comment-indicator") ||
789 !!target.closest(".comment-indicator");
790 const isCommentBox =
791 target.matches(".comment-box") || !!target.closest(".comment-box");
792
793 // If click is outside our UI elements
794 if (!isIndicator && !isCommentBox) {
795 // If we're not showing the comment box, hide the indicator
796 if (!this.showCommentBox) {
797 this.showCommentIndicator = false;
798 this.requestUpdate();
799 }
800 }
801 } catch (error) {
802 console.error("Error in document click handler:", error);
803 }
804 };
805
806 // Add the document click listener
807 document.addEventListener("click", this._documentClickHandler);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700808 } catch (error) {
809 console.error("Error setting up selection listeners:", error);
810 }
811 }
812
813 // Track mouse hover state
814 private _isHovering = false;
815
816 // Store document click handler for cleanup
817 private _documentClickHandler: ((e: MouseEvent) => void) | null = null;
818
819 /**
820 * Handle selection change events from either editor
821 */
822 private handleSelectionChange(
823 e: monaco.editor.ICursorSelectionChangedEvent,
824 editor: monaco.editor.IStandaloneCodeEditor,
825 editorType: "original" | "modified",
826 ) {
827 try {
828 // If we're not making a selection (just moving cursor), do nothing
829 if (e.selection.isEmpty()) {
830 // Don't hide indicator or box if already shown
831 if (!this.showCommentBox) {
832 this.selectedText = null;
833 this.selectionRange = null;
834 this.showCommentIndicator = false;
835 }
836 return;
837 }
838
839 // Get selected text
840 const model = editor.getModel();
841 if (!model) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700842 return;
843 }
844
845 // Make sure selection is within valid range
846 const lineCount = model.getLineCount();
847 if (
848 e.selection.startLineNumber > lineCount ||
849 e.selection.endLineNumber > lineCount
850 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700851 return;
852 }
853
854 // Store which editor is active
855 this.activeEditor = editorType;
856
857 // Store selection range
858 this.selectionRange = {
859 startLineNumber: e.selection.startLineNumber,
860 startColumn: e.selection.startColumn,
861 endLineNumber: e.selection.endLineNumber,
862 endColumn: e.selection.endColumn,
863 };
864
865 try {
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000866 // Expand selection to full lines for better context
867 const expandedSelection = {
868 startLineNumber: e.selection.startLineNumber,
869 startColumn: 1, // Start at beginning of line
870 endLineNumber: e.selection.endLineNumber,
871 endColumn: model.getLineMaxColumn(e.selection.endLineNumber), // End at end of line
872 };
873
874 // Get the selected text using the expanded selection
875 this.selectedText = model.getValueInRange(expandedSelection);
Autoformatter7ad1c7a2025-05-29 02:00:19 +0000876
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000877 // Update the selection range to reflect the full lines
878 this.selectionRange = {
879 startLineNumber: expandedSelection.startLineNumber,
880 startColumn: expandedSelection.startColumn,
881 endLineNumber: expandedSelection.endLineNumber,
882 endColumn: expandedSelection.endColumn,
883 };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700884 } catch (error) {
885 console.error("Error getting selected text:", error);
886 return;
887 }
888
889 // If there's selected text, show the indicator
890 if (this.selectedText && this.selectedText.trim() !== "") {
891 // Calculate indicator position safely
892 try {
893 // Use the editor's DOM node as positioning context
894 const editorDomNode = editor.getDomNode();
895 if (!editorDomNode) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700896 return;
897 }
898
899 // Get position from editor
900 const position = {
901 lineNumber: e.selection.endLineNumber,
902 column: e.selection.endColumn,
903 };
904
905 // Use editor's built-in method for coordinate conversion
906 const selectionCoords = editor.getScrolledVisiblePosition(position);
907
908 if (selectionCoords) {
909 // Get accurate DOM position for the selection end
910 const editorRect = editorDomNode.getBoundingClientRect();
911
912 // Calculate the actual screen position
913 const screenLeft = editorRect.left + selectionCoords.left;
914 const screenTop = editorRect.top + selectionCoords.top;
915
916 // Store absolute screen coordinates
917 this.indicatorPosition = {
918 top: screenTop,
919 left: screenLeft + 10, // Slight offset
920 };
921
922 // Check window boundaries to ensure the indicator stays visible
923 const viewportWidth = window.innerWidth;
924 const viewportHeight = window.innerHeight;
925
926 // Keep indicator within viewport bounds
927 if (this.indicatorPosition.left + 150 > viewportWidth) {
928 this.indicatorPosition.left = viewportWidth - 160;
929 }
930
931 if (this.indicatorPosition.top + 40 > viewportHeight) {
932 this.indicatorPosition.top = viewportHeight - 50;
933 }
934
935 // Show the indicator
936 this.showCommentIndicator = true;
937
938 // Request an update to ensure UI reflects changes
939 this.requestUpdate();
940 }
941 } catch (error) {
942 console.error("Error positioning comment indicator:", error);
943 }
944 }
945 } catch (error) {
946 console.error("Error handling selection change:", error);
947 }
948 }
949
950 /**
951 * Handle click on the comment indicator
952 */
953 private handleIndicatorClick(e: Event) {
954 try {
955 e.stopPropagation();
956 e.preventDefault();
957
958 this.showCommentBox = true;
959 this.commentText = ""; // Reset comment text
960
961 // Don't hide the indicator while comment box is shown
962 this.showCommentIndicator = true;
963
964 // Ensure UI updates
965 this.requestUpdate();
966 } catch (error) {
967 console.error("Error handling indicator click:", error);
968 }
969 }
970
971 /**
972 * Handle changes to the comment text
973 */
974 private handleCommentInput(e: Event) {
975 const target = e.target as HTMLTextAreaElement;
976 this.commentText = target.value;
977 }
978
979 /**
980 * Close the comment box
981 */
982 private closeCommentBox() {
983 this.showCommentBox = false;
984 // Also hide the indicator
985 this.showCommentIndicator = false;
986 }
987
988 /**
989 * Submit the comment
990 */
991 private submitComment() {
992 try {
993 if (!this.selectedText || !this.commentText) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700994 return;
995 }
996
997 // Get the correct filename based on active editor
998 const fileContext =
999 this.activeEditor === "original"
1000 ? this.originalFilename || "Original file"
1001 : this.modifiedFilename || "Modified file";
1002
1003 // Include editor info to make it clear which version was commented on
1004 const editorLabel =
1005 this.activeEditor === "original" ? "[Original]" : "[Modified]";
1006
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001007 // Add line number information if available
1008 let lineInfo = "";
1009 if (this.selectionRange) {
1010 const startLine = this.selectionRange.startLineNumber;
1011 const endLine = this.selectionRange.endLineNumber;
1012 if (startLine === endLine) {
1013 lineInfo = ` (line ${startLine})`;
1014 } else {
1015 lineInfo = ` (lines ${startLine}-${endLine})`;
1016 }
1017 }
1018
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001019 // Format the comment in a readable way
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001020 const formattedComment = `\`\`\`\n${fileContext} ${editorLabel}${lineInfo}:\n${this.selectedText}\n\`\`\`\n\n${this.commentText}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001021
1022 // Close UI before dispatching to prevent interaction conflicts
1023 this.closeCommentBox();
1024
1025 // Use setTimeout to ensure the UI has updated before sending the event
1026 setTimeout(() => {
1027 try {
1028 // Dispatch a custom event with the comment details
1029 const event = new CustomEvent("monaco-comment", {
1030 detail: {
1031 fileContext,
1032 selectedText: this.selectedText,
1033 commentText: this.commentText,
1034 formattedComment,
1035 selectionRange: this.selectionRange,
1036 activeEditor: this.activeEditor,
1037 },
1038 bubbles: true,
1039 composed: true,
1040 });
1041
1042 this.dispatchEvent(event);
1043 } catch (error) {
1044 console.error("Error dispatching comment event:", error);
1045 }
1046 }, 0);
1047 } catch (error) {
1048 console.error("Error submitting comment:", error);
1049 this.closeCommentBox();
1050 }
1051 }
1052
1053 private updateModels() {
1054 try {
1055 // Get language based on filename
1056 const originalLang = this.getLanguageForFile(this.originalFilename || "");
1057 const modifiedLang = this.getLanguageForFile(this.modifiedFilename || "");
1058
1059 // Always create new models with unique URIs based on timestamp to avoid conflicts
1060 const timestamp = new Date().getTime();
1061 // TODO: Could put filename in these URIs; unclear how they're used right now.
1062 const originalUri = monaco.Uri.parse(
1063 `file:///original-${timestamp}.${originalLang}`,
1064 );
1065 const modifiedUri = monaco.Uri.parse(
1066 `file:///modified-${timestamp}.${modifiedLang}`,
1067 );
1068
1069 // Store references to old models
1070 const oldOriginalModel = this.originalModel;
1071 const oldModifiedModel = this.modifiedModel;
1072
1073 // Nullify instance variables to prevent accidental use
1074 this.originalModel = undefined;
1075 this.modifiedModel = undefined;
1076
1077 // Clear the editor model first to release Monaco's internal references
1078 if (this.editor) {
1079 this.editor.setModel(null);
1080 }
1081
1082 // Now it's safe to dispose the old models
1083 if (oldOriginalModel) {
1084 oldOriginalModel.dispose();
1085 }
1086
1087 if (oldModifiedModel) {
1088 oldModifiedModel.dispose();
1089 }
1090
1091 // Create new models
1092 this.originalModel = monaco.editor.createModel(
1093 this.originalCode || "",
1094 originalLang,
1095 originalUri,
1096 );
1097
1098 this.modifiedModel = monaco.editor.createModel(
1099 this.modifiedCode || "",
1100 modifiedLang,
1101 modifiedUri,
1102 );
1103
1104 // Set the new models on the editor
1105 if (this.editor) {
1106 this.editor.setModel({
1107 original: this.originalModel,
1108 modified: this.modifiedModel,
1109 });
Autoformatter9abf8032025-06-14 23:24:08 +00001110
David Crawshaw26f3f342025-06-14 19:58:32 +00001111 // Set initial hideUnchangedRegions state (default to enabled/collapsed)
1112 this.editor.updateOptions({
1113 hideUnchangedRegions: {
1114 enabled: true, // Default to collapsed state
1115 contextLineCount: 3,
1116 minimumLineCount: 3,
1117 revealLineCount: 10,
1118 },
1119 });
Autoformatter9abf8032025-06-14 23:24:08 +00001120
David Crawshaw26f3f342025-06-14 19:58:32 +00001121 // Fit content after setting new models
1122 if (this.fitEditorToContent) {
1123 setTimeout(() => this.fitEditorToContent!(), 50);
1124 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001125 }
1126 this.setupContentChangeListener();
1127 } catch (error) {
1128 console.error("Error updating Monaco models:", error);
1129 }
1130 }
1131
1132 updated(changedProperties: Map<string, any>) {
1133 // If any relevant properties changed, just update the models
1134 if (
1135 changedProperties.has("originalCode") ||
1136 changedProperties.has("modifiedCode") ||
1137 changedProperties.has("originalFilename") ||
1138 changedProperties.has("modifiedFilename") ||
1139 changedProperties.has("editableRight")
1140 ) {
1141 if (this.editor) {
1142 this.updateModels();
1143
David Crawshaw26f3f342025-06-14 19:58:32 +00001144 // Force auto-sizing after model updates
1145 // Use a slightly longer delay to ensure layout is stable
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001146 setTimeout(() => {
David Crawshaw26f3f342025-06-14 19:58:32 +00001147 if (this.fitEditorToContent) {
1148 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001149 }
David Crawshaw26f3f342025-06-14 19:58:32 +00001150 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001151 } else {
1152 // If the editor isn't initialized yet but we received content,
1153 // initialize it now
1154 this.initializeEditor();
1155 }
1156 }
1157 }
1158
David Crawshaw26f3f342025-06-14 19:58:32 +00001159 // Set up auto-sizing for multi-file diff view
1160 private setupAutoSizing() {
1161 if (!this.editor) return;
1162
1163 const fitContent = () => {
1164 try {
1165 const originalEditor = this.editor!.getOriginalEditor();
1166 const modifiedEditor = this.editor!.getModifiedEditor();
Autoformatter9abf8032025-06-14 23:24:08 +00001167
David Crawshaw26f3f342025-06-14 19:58:32 +00001168 const originalHeight = originalEditor.getContentHeight();
1169 const modifiedHeight = modifiedEditor.getContentHeight();
Autoformatter9abf8032025-06-14 23:24:08 +00001170
David Crawshaw26f3f342025-06-14 19:58:32 +00001171 // Use the maximum height of both editors, plus some padding
1172 const maxHeight = Math.max(originalHeight, modifiedHeight) + 18; // 1 blank line bottom padding
Autoformatter9abf8032025-06-14 23:24:08 +00001173
David Crawshaw26f3f342025-06-14 19:58:32 +00001174 // Set both container and host height to enable proper scrolling
1175 if (this.container.value) {
1176 // Set explicit heights on both container and host
1177 this.container.value.style.height = `${maxHeight}px`;
1178 this.style.height = `${maxHeight}px`; // Update host element height
Autoformatter9abf8032025-06-14 23:24:08 +00001179
David Crawshaw26f3f342025-06-14 19:58:32 +00001180 // Emit the height change event BEFORE calling layout
1181 // This ensures parent containers resize first
Autoformatter9abf8032025-06-14 23:24:08 +00001182 this.dispatchEvent(
1183 new CustomEvent("monaco-height-changed", {
1184 detail: { height: maxHeight },
1185 bubbles: true,
1186 composed: true,
1187 }),
1188 );
1189
David Crawshaw26f3f342025-06-14 19:58:32 +00001190 // Layout after both this component and parents have updated
1191 setTimeout(() => {
1192 if (this.editor && this.container.value) {
1193 // Use explicit dimensions to ensure Monaco uses full available space
David Crawshawdba26b52025-06-15 00:33:45 +00001194 // Use clientWidth instead of offsetWidth to avoid border overflow
1195 const width = this.container.value.clientWidth;
David Crawshaw26f3f342025-06-14 19:58:32 +00001196 this.editor.layout({
1197 width: width,
Autoformatter9abf8032025-06-14 23:24:08 +00001198 height: maxHeight,
David Crawshaw26f3f342025-06-14 19:58:32 +00001199 });
1200 }
1201 }, 10);
1202 }
1203 } catch (error) {
Autoformatter9abf8032025-06-14 23:24:08 +00001204 console.error("Error in fitContent:", error);
David Crawshaw26f3f342025-06-14 19:58:32 +00001205 }
1206 };
1207
1208 // Store the fit function for external access
1209 this.fitEditorToContent = fitContent;
1210
1211 // Set up listeners for content size changes
1212 this.editor.getOriginalEditor().onDidContentSizeChange(fitContent);
1213 this.editor.getModifiedEditor().onDidContentSizeChange(fitContent);
1214
1215 // Initial fit
1216 fitContent();
1217 }
1218
1219 private fitEditorToContent: (() => void) | null = null;
1220
David Crawshawe2954ce2025-06-15 00:06:34 +00001221 /**
1222 * Set up window resize handler to ensure Monaco editor adapts to browser window changes
1223 */
1224 private setupWindowResizeHandler() {
1225 // Create a debounced resize handler to avoid too many layout calls
1226 let resizeTimeout: number | null = null;
Autoformatterad15b6c2025-06-15 00:29:26 +00001227
David Crawshawe2954ce2025-06-15 00:06:34 +00001228 this._windowResizeHandler = () => {
1229 // Clear any existing timeout
1230 if (resizeTimeout) {
1231 window.clearTimeout(resizeTimeout);
1232 }
Autoformatterad15b6c2025-06-15 00:29:26 +00001233
David Crawshawe2954ce2025-06-15 00:06:34 +00001234 // Debounce the resize to avoid excessive layout calls
1235 resizeTimeout = window.setTimeout(() => {
1236 if (this.editor && this.container.value) {
1237 // Trigger layout recalculation
1238 if (this.fitEditorToContent) {
1239 this.fitEditorToContent();
1240 } else {
1241 // Fallback: just trigger a layout with current container dimensions
David Crawshawdba26b52025-06-15 00:33:45 +00001242 // Use clientWidth/Height instead of offsetWidth/Height to avoid border overflow
1243 const width = this.container.value.clientWidth;
1244 const height = this.container.value.clientHeight;
David Crawshawe2954ce2025-06-15 00:06:34 +00001245 this.editor.layout({ width, height });
1246 }
1247 }
1248 }, 100); // 100ms debounce
1249 };
Autoformatterad15b6c2025-06-15 00:29:26 +00001250
David Crawshawe2954ce2025-06-15 00:06:34 +00001251 // Add the event listener
Autoformatterad15b6c2025-06-15 00:29:26 +00001252 window.addEventListener("resize", this._windowResizeHandler);
David Crawshawe2954ce2025-06-15 00:06:34 +00001253 }
1254
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001255 // Add resize observer to ensure editor resizes when container changes
1256 firstUpdated() {
1257 // Initialize the editor
1258 this.initializeEditor();
1259
David Crawshawe2954ce2025-06-15 00:06:34 +00001260 // Set up window resize handler to ensure Monaco editor adapts to browser window changes
1261 this.setupWindowResizeHandler();
1262
David Crawshaw26f3f342025-06-14 19:58:32 +00001263 // For multi-file diff, we don't use ResizeObserver since we control the size
1264 // Instead, we rely on auto-sizing based on content
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001265
1266 // If editable, set up edit mode and content change listener
1267 if (this.editableRight && this.editor) {
1268 // Ensure the original editor is read-only
1269 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
1270 // Ensure the modified editor is editable
1271 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
1272 }
1273 }
1274
1275 private _resizeObserver: ResizeObserver | null = null;
David Crawshawe2954ce2025-06-15 00:06:34 +00001276 private _windowResizeHandler: (() => void) | null = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001277
philip.zeyliger7351cd92025-06-14 12:25:31 -07001278 /**
1279 * Add this Monaco editor instance to the global debug object
1280 * This allows inspection and debugging via browser console
1281 */
1282 private addToDebugGlobal() {
1283 try {
1284 // Initialize the debug global if it doesn't exist
1285 if (!(window as any).sketchDebug) {
1286 (window as any).sketchDebug = {
1287 monaco: monaco,
1288 editors: [],
1289 remeasureFonts: () => {
1290 monaco.editor.remeasureFonts();
1291 (window as any).sketchDebug.editors.forEach(
1292 (editor: any, index: number) => {
1293 if (editor && editor.layout) {
1294 editor.layout();
1295 }
1296 },
1297 );
1298 },
1299 layoutAll: () => {
1300 (window as any).sketchDebug.editors.forEach(
1301 (editor: any, index: number) => {
1302 if (editor && editor.layout) {
1303 editor.layout();
1304 }
1305 },
1306 );
1307 },
1308 getActiveEditors: () => {
1309 return (window as any).sketchDebug.editors.filter(
1310 (editor: any) => editor !== null,
1311 );
1312 },
1313 };
1314 }
1315
1316 // Add this editor to the debug collection
1317 if (this.editor) {
1318 (window as any).sketchDebug.editors.push(this.editor);
1319 }
1320 } catch (error) {
1321 console.error("Error adding Monaco editor to debug global:", error);
1322 }
1323 }
1324
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001325 disconnectedCallback() {
1326 super.disconnectedCallback();
1327
1328 try {
philip.zeyliger7351cd92025-06-14 12:25:31 -07001329 // Remove editor from debug global before disposal
1330 if (this.editor && (window as any).sketchDebug?.editors) {
1331 const index = (window as any).sketchDebug.editors.indexOf(this.editor);
1332 if (index > -1) {
1333 (window as any).sketchDebug.editors[index] = null;
1334 }
1335 }
1336
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001337 // Clean up resources when element is removed
1338 if (this.editor) {
1339 this.editor.dispose();
1340 this.editor = undefined;
1341 }
1342
1343 // Dispose models to prevent memory leaks
1344 if (this.originalModel) {
1345 this.originalModel.dispose();
1346 this.originalModel = undefined;
1347 }
1348
1349 if (this.modifiedModel) {
1350 this.modifiedModel.dispose();
1351 this.modifiedModel = undefined;
1352 }
1353
David Crawshaw26f3f342025-06-14 19:58:32 +00001354 // Clean up resize observer (if any)
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001355 if (this._resizeObserver) {
1356 this._resizeObserver.disconnect();
1357 this._resizeObserver = null;
1358 }
Autoformatter9abf8032025-06-14 23:24:08 +00001359
David Crawshaw26f3f342025-06-14 19:58:32 +00001360 // Clear the fit function reference
1361 this.fitEditorToContent = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001362
1363 // Remove document click handler if set
1364 if (this._documentClickHandler) {
1365 document.removeEventListener("click", this._documentClickHandler);
1366 this._documentClickHandler = null;
1367 }
Autoformatterad15b6c2025-06-15 00:29:26 +00001368
David Crawshawe2954ce2025-06-15 00:06:34 +00001369 // Remove window resize handler if set
1370 if (this._windowResizeHandler) {
Autoformatterad15b6c2025-06-15 00:29:26 +00001371 window.removeEventListener("resize", this._windowResizeHandler);
David Crawshawe2954ce2025-06-15 00:06:34 +00001372 this._windowResizeHandler = null;
1373 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001374 } catch (error) {
1375 console.error("Error in disconnectedCallback:", error);
1376 }
1377 }
1378
1379 // disconnectedCallback implementation is defined below
1380}
1381
1382declare global {
1383 interface HTMLElementTagNameMap {
1384 "sketch-monaco-view": CodeDiffEditor;
1385 }
1386}