blob: 8f542fe61d58f3a95c585d3be8c668a5b6093524 [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;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700269 }
270
271 /* Comment indicator and box styles */
272 .comment-indicator {
273 position: fixed;
274 background-color: rgba(66, 133, 244, 0.9);
275 color: white;
276 border-radius: 3px;
277 padding: 3px 8px;
278 font-size: 12px;
279 cursor: pointer;
280 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
281 z-index: 10000;
282 animation: fadeIn 0.2s ease-in-out;
283 display: flex;
284 align-items: center;
285 gap: 4px;
286 pointer-events: all;
287 }
288
289 .comment-indicator:hover {
290 background-color: rgba(66, 133, 244, 1);
291 }
292
293 .comment-indicator span {
294 line-height: 1;
295 }
296
297 .comment-box {
298 position: fixed;
299 background-color: white;
300 border: 1px solid #ddd;
301 border-radius: 4px;
302 box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
303 padding: 12px;
304 z-index: 10001;
305 width: 350px;
306 animation: fadeIn 0.2s ease-in-out;
307 max-height: 80vh;
308 overflow-y: auto;
309 }
310
311 .comment-box-header {
312 display: flex;
313 justify-content: space-between;
314 align-items: center;
315 margin-bottom: 8px;
316 }
317
318 .comment-box-header h3 {
319 margin: 0;
320 font-size: 14px;
321 font-weight: 500;
322 }
323
324 .close-button {
325 background: none;
326 border: none;
327 cursor: pointer;
328 font-size: 16px;
329 color: #666;
330 padding: 2px 6px;
331 }
332
333 .close-button:hover {
334 color: #333;
335 }
336
337 .selected-text-preview {
338 background-color: #f5f5f5;
339 border: 1px solid #eee;
340 border-radius: 3px;
341 padding: 8px;
342 margin-bottom: 10px;
343 font-family: monospace;
344 font-size: 12px;
345 max-height: 80px;
346 overflow-y: auto;
347 white-space: pre-wrap;
348 word-break: break-all;
349 }
350
351 .comment-textarea {
352 width: 100%;
353 min-height: 80px;
354 padding: 8px;
355 border: 1px solid #ddd;
356 border-radius: 3px;
357 resize: vertical;
358 font-family: inherit;
359 margin-bottom: 10px;
360 box-sizing: border-box;
361 }
362
363 .comment-actions {
364 display: flex;
365 justify-content: flex-end;
366 gap: 8px;
367 }
368
369 .comment-actions button {
370 padding: 6px 12px;
371 border-radius: 3px;
372 cursor: pointer;
373 font-size: 12px;
374 }
375
376 .cancel-button {
377 background-color: transparent;
378 border: 1px solid #ddd;
379 }
380
381 .cancel-button:hover {
382 background-color: #f5f5f5;
383 }
384
385 .submit-button {
386 background-color: #4285f4;
387 color: white;
388 border: none;
389 }
390
391 .submit-button:hover {
392 background-color: #3367d6;
393 }
394
395 @keyframes fadeIn {
396 from {
397 opacity: 0;
398 }
399 to {
400 opacity: 1;
401 }
402 }
403 `;
404
405 render() {
406 return html`
407 <style>
408 ${monacoStyles}
409 </style>
410 <main ${ref(this.container)}></main>
411
412 <!-- Save indicator - shown when editing -->
Philip Zeyligere89b3082025-05-29 03:16:06 +0000413 ${this.editableRight
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700414 ? html`
415 <div class="save-indicator ${this.saveState}">
Philip Zeyligere89b3082025-05-29 03:16:06 +0000416 ${this.saveState === "idle"
417 ? "Editable"
418 : this.saveState === "modified"
419 ? "Modified..."
420 : this.saveState === "saving"
421 ? "Saving..."
422 : this.saveState === "saved"
423 ? "Saved"
424 : ""}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700425 </div>
426 `
427 : ""}
428
429 <!-- Comment indicator - shown when text is selected -->
430 ${this.showCommentIndicator
431 ? html`
432 <div
433 class="comment-indicator"
434 style="top: ${this.indicatorPosition.top}px; left: ${this
435 .indicatorPosition.left}px;"
436 @click="${this.handleIndicatorClick}"
437 @mouseenter="${() => {
438 this._isHovering = true;
439 }}"
440 @mouseleave="${() => {
441 this._isHovering = false;
442 }}"
443 >
444 <span>💬</span>
445 <span>Add comment</span>
446 </div>
447 `
448 : ""}
449
450 <!-- Comment box - shown when indicator is clicked -->
451 ${this.showCommentBox
452 ? html`
453 <div
454 class="comment-box"
455 style="${this.calculateCommentBoxPosition()}"
456 @mouseenter="${() => {
457 this._isHovering = true;
458 }}"
459 @mouseleave="${() => {
460 this._isHovering = false;
461 }}"
462 >
463 <div class="comment-box-header">
464 <h3>Add comment</h3>
465 <button class="close-button" @click="${this.closeCommentBox}">
466 ×
467 </button>
468 </div>
469 <div class="selected-text-preview">${this.selectedText}</div>
470 <textarea
471 class="comment-textarea"
472 placeholder="Type your comment here..."
473 .value="${this.commentText}"
474 @input="${this.handleCommentInput}"
475 ></textarea>
476 <div class="comment-actions">
477 <button class="cancel-button" @click="${this.closeCommentBox}">
478 Cancel
479 </button>
480 <button class="submit-button" @click="${this.submitComment}">
Josh Bleecher Snyderafeafea2025-05-23 20:27:39 +0000481 Add
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700482 </button>
483 </div>
484 </div>
485 `
486 : ""}
487 `;
488 }
489
490 /**
491 * Calculate the optimal position for the comment box to keep it in view
492 */
493 private calculateCommentBoxPosition(): string {
494 // Get viewport dimensions
495 const viewportWidth = window.innerWidth;
496 const viewportHeight = window.innerHeight;
497
498 // Default position (below indicator)
499 let top = this.indicatorPosition.top + 30;
500 let left = this.indicatorPosition.left;
501
502 // Estimated box dimensions
503 const boxWidth = 350;
504 const boxHeight = 300;
505
506 // Check if box would go off the right edge
507 if (left + boxWidth > viewportWidth) {
508 left = viewportWidth - boxWidth - 20; // Keep 20px margin
509 }
510
511 // Check if box would go off the bottom
512 const bottomSpace = viewportHeight - top;
513 if (bottomSpace < boxHeight) {
514 // Not enough space below, try to position above if possible
515 if (this.indicatorPosition.top > boxHeight) {
516 // Position above the indicator
517 top = this.indicatorPosition.top - boxHeight - 10;
518 } else {
519 // Not enough space above either, position at top of viewport with margin
520 top = 10;
521 }
522 }
523
524 // Ensure box is never positioned off-screen
525 top = Math.max(10, top);
526 left = Math.max(10, left);
527
528 return `top: ${top}px; left: ${left}px;`;
529 }
530
531 setOriginalCode(code: string, filename?: string) {
532 this.originalCode = code;
533 if (filename) {
534 this.originalFilename = filename;
535 }
536
537 // Update the model if the editor is initialized
538 if (this.editor) {
539 const model = this.editor.getOriginalEditor().getModel();
540 if (model) {
541 model.setValue(code);
542 if (filename) {
543 monaco.editor.setModelLanguage(
544 model,
545 this.getLanguageForFile(filename),
546 );
547 }
548 }
549 }
550 }
551
552 setModifiedCode(code: string, filename?: string) {
553 this.modifiedCode = code;
554 if (filename) {
555 this.modifiedFilename = filename;
556 }
557
558 // Update the model if the editor is initialized
559 if (this.editor) {
560 const model = this.editor.getModifiedEditor().getModel();
561 if (model) {
562 model.setValue(code);
563 if (filename) {
564 monaco.editor.setModelLanguage(
565 model,
566 this.getLanguageForFile(filename),
567 );
568 }
569 }
570 }
571 }
572
Philip Zeyliger70273072025-05-28 18:26:14 +0000573 private _extensionToLanguageMap: Map<string, string> | null = null;
574
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700575 private getLanguageForFile(filename: string): string {
Philip Zeyliger70273072025-05-28 18:26:14 +0000576 // Get the file extension (including the dot for exact matching)
577 const extension = "." + (filename.split(".").pop()?.toLowerCase() || "");
578
579 // Build the extension-to-language map on first use
580 if (!this._extensionToLanguageMap) {
581 this._extensionToLanguageMap = new Map();
582 const languages = monaco.languages.getLanguages();
583
584 for (const language of languages) {
585 if (language.extensions) {
586 for (const ext of language.extensions) {
587 // Monaco extensions already include the dot, so use them directly
588 this._extensionToLanguageMap.set(ext.toLowerCase(), language.id);
589 }
590 }
591 }
592 }
593
594 return this._extensionToLanguageMap.get(extension) || "plaintext";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700595 }
596
597 /**
598 * Update editor options
599 */
600 setOptions(value: monaco.editor.IDiffEditorConstructionOptions) {
601 if (this.editor) {
602 this.editor.updateOptions(value);
David Crawshaw26f3f342025-06-14 19:58:32 +0000603 // Re-fit content after options change
604 if (this.fitEditorToContent) {
605 setTimeout(() => this.fitEditorToContent!(), 50);
606 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700607 }
608 }
609
610 /**
611 * Toggle hideUnchangedRegions feature
612 */
613 toggleHideUnchangedRegions(enabled: boolean) {
614 if (this.editor) {
615 this.editor.updateOptions({
616 hideUnchangedRegions: {
617 enabled: enabled,
618 contextLineCount: 3,
619 minimumLineCount: 3,
620 revealLineCount: 10,
621 },
622 });
David Crawshaw26f3f342025-06-14 19:58:32 +0000623 // Re-fit content after toggling
624 if (this.fitEditorToContent) {
625 setTimeout(() => this.fitEditorToContent!(), 100);
626 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700627 }
628 }
629
630 // Models for the editor
631 private originalModel?: monaco.editor.ITextModel;
632 private modifiedModel?: monaco.editor.ITextModel;
633
634 private initializeEditor() {
635 try {
philip.zeyliger7351cd92025-06-14 12:25:31 -0700636 // Disable semantic validation globally for TypeScript/JavaScript if available
637 if (monaco.languages && monaco.languages.typescript) {
638 monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
639 noSemanticValidation: true,
640 });
641 monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
642 noSemanticValidation: true,
643 });
644 }
Autoformatter8c463622025-05-16 21:54:17 +0000645
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700646 // First time initialization
647 if (!this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000648 // Create the diff editor with auto-sizing configuration
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700649 this.editor = monaco.editor.createDiffEditor(this.container.value!, {
David Crawshaw26f3f342025-06-14 19:58:32 +0000650 automaticLayout: false, // We'll resize manually
Autoformatter8c463622025-05-16 21:54:17 +0000651 readOnly: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700652 theme: "vs", // Always use light mode
653 renderSideBySide: true,
654 ignoreTrimWhitespace: false,
David Crawshawf00c7b12025-06-15 00:24:46 +0000655 renderOverviewRuler: false, // Disable the overview ruler
David Crawshaw26f3f342025-06-14 19:58:32 +0000656 scrollbar: {
Autoformatter9abf8032025-06-14 23:24:08 +0000657 vertical: "hidden",
658 horizontal: "hidden",
David Crawshaw26f3f342025-06-14 19:58:32 +0000659 handleMouseWheel: false, // Let outer scroller eat the wheel
David Crawshawe2954ce2025-06-15 00:06:34 +0000660 useShadows: false, // Disable scrollbar shadows
661 verticalHasArrows: false, // Remove scrollbar arrows
662 horizontalHasArrows: false, // Remove scrollbar arrows
663 verticalScrollbarSize: 0, // Set scrollbar track width to 0
664 horizontalScrollbarSize: 0, // Set scrollbar track height to 0
David Crawshaw26f3f342025-06-14 19:58:32 +0000665 },
666 minimap: { enabled: false },
667 overviewRulerLanes: 0,
668 scrollBeyondLastLine: false,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700669 // Focus on the differences by hiding unchanged regions
670 hideUnchangedRegions: {
671 enabled: true, // Enable the feature
672 contextLineCount: 3, // Show 3 lines of context around each difference
673 minimumLineCount: 3, // Hide regions only when they're at least 3 lines
674 revealLineCount: 10, // Show 10 lines when expanding a hidden region
675 },
676 });
677
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700678 // Set up selection change event listeners for both editors
679 this.setupSelectionChangeListeners();
680
Josh Bleecher Snyder4d90f342025-05-29 02:18:38 +0000681 this.setupKeyboardShortcuts();
682
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700683 // If this is an editable view, set the correct read-only state for each editor
684 if (this.editableRight) {
685 // Make sure the original editor is always read-only
686 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
687 // Make sure the modified editor is editable
688 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
689 }
philip.zeyliger7351cd92025-06-14 12:25:31 -0700690
David Crawshaw26f3f342025-06-14 19:58:32 +0000691 // Set up auto-sizing
692 this.setupAutoSizing();
693
philip.zeyliger7351cd92025-06-14 12:25:31 -0700694 // Add Monaco editor to debug global
695 this.addToDebugGlobal();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700696 }
697
698 // Create or update models
699 this.updateModels();
Autoformatter8c463622025-05-16 21:54:17 +0000700 // Set up content change listener
701 this.setupContentChangeListener();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700702
philip.zeyliger7351cd92025-06-14 12:25:31 -0700703 // Fix cursor positioning issues by ensuring fonts are loaded
philip.zeyliger7351cd92025-06-14 12:25:31 -0700704 document.fonts.ready.then(() => {
705 if (this.editor) {
706 monaco.editor.remeasureFonts();
David Crawshaw26f3f342025-06-14 19:58:32 +0000707 this.fitEditorToContent();
philip.zeyliger7351cd92025-06-14 12:25:31 -0700708 }
709 });
710
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700711 // Force layout recalculation after a short delay
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700712 setTimeout(() => {
713 if (this.editor) {
David Crawshaw26f3f342025-06-14 19:58:32 +0000714 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700715 }
David Crawshaw26f3f342025-06-14 19:58:32 +0000716 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700717 } catch (error) {
718 console.error("Error initializing Monaco editor:", error);
719 }
720 }
721
722 /**
723 * Sets up event listeners for text selection in both editors.
724 * This enables showing the comment UI when users select text and
725 * manages the visibility of UI components based on user interactions.
726 */
727 private setupSelectionChangeListeners() {
728 try {
729 if (!this.editor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700730 return;
731 }
732
733 // Get both original and modified editors
734 const originalEditor = this.editor.getOriginalEditor();
735 const modifiedEditor = this.editor.getModifiedEditor();
736
737 if (!originalEditor || !modifiedEditor) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700738 return;
739 }
740
741 // Add selection change listener to original editor
742 originalEditor.onDidChangeCursorSelection((e) => {
743 this.handleSelectionChange(e, originalEditor, "original");
744 });
745
746 // Add selection change listener to modified editor
747 modifiedEditor.onDidChangeCursorSelection((e) => {
748 this.handleSelectionChange(e, modifiedEditor, "modified");
749 });
750
751 // Create a debounced function for mouse move handling
752 let mouseMoveTimeout: number | null = null;
753 const handleMouseMove = () => {
754 // Clear any existing timeout
755 if (mouseMoveTimeout) {
756 window.clearTimeout(mouseMoveTimeout);
757 }
758
759 // If there's text selected and we're not showing the comment box, keep indicator visible
760 if (this.selectedText && !this.showCommentBox) {
761 this.showCommentIndicator = true;
762 this.requestUpdate();
763 }
764
765 // Set a new timeout to hide the indicator after a delay
766 mouseMoveTimeout = window.setTimeout(() => {
767 // Only hide if we're not showing the comment box and not actively hovering
768 if (!this.showCommentBox && !this._isHovering) {
769 this.showCommentIndicator = false;
770 this.requestUpdate();
771 }
772 }, 2000); // Hide after 2 seconds of inactivity
773 };
774
775 // Add mouse move listeners with debouncing
776 originalEditor.onMouseMove(() => handleMouseMove());
777 modifiedEditor.onMouseMove(() => handleMouseMove());
778
779 // Track hover state over the indicator and comment box
780 this._isHovering = false;
781
782 // Use the global document click handler to detect clicks outside
783 this._documentClickHandler = (e: MouseEvent) => {
784 try {
785 const target = e.target as HTMLElement;
786 const isIndicator =
787 target.matches(".comment-indicator") ||
788 !!target.closest(".comment-indicator");
789 const isCommentBox =
790 target.matches(".comment-box") || !!target.closest(".comment-box");
791
792 // If click is outside our UI elements
793 if (!isIndicator && !isCommentBox) {
794 // If we're not showing the comment box, hide the indicator
795 if (!this.showCommentBox) {
796 this.showCommentIndicator = false;
797 this.requestUpdate();
798 }
799 }
800 } catch (error) {
801 console.error("Error in document click handler:", error);
802 }
803 };
804
805 // Add the document click listener
806 document.addEventListener("click", this._documentClickHandler);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700807 } catch (error) {
808 console.error("Error setting up selection listeners:", error);
809 }
810 }
811
812 // Track mouse hover state
813 private _isHovering = false;
814
815 // Store document click handler for cleanup
816 private _documentClickHandler: ((e: MouseEvent) => void) | null = null;
817
818 /**
819 * Handle selection change events from either editor
820 */
821 private handleSelectionChange(
822 e: monaco.editor.ICursorSelectionChangedEvent,
823 editor: monaco.editor.IStandaloneCodeEditor,
824 editorType: "original" | "modified",
825 ) {
826 try {
827 // If we're not making a selection (just moving cursor), do nothing
828 if (e.selection.isEmpty()) {
829 // Don't hide indicator or box if already shown
830 if (!this.showCommentBox) {
831 this.selectedText = null;
832 this.selectionRange = null;
833 this.showCommentIndicator = false;
834 }
835 return;
836 }
837
838 // Get selected text
839 const model = editor.getModel();
840 if (!model) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700841 return;
842 }
843
844 // Make sure selection is within valid range
845 const lineCount = model.getLineCount();
846 if (
847 e.selection.startLineNumber > lineCount ||
848 e.selection.endLineNumber > lineCount
849 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700850 return;
851 }
852
853 // Store which editor is active
854 this.activeEditor = editorType;
855
856 // Store selection range
857 this.selectionRange = {
858 startLineNumber: e.selection.startLineNumber,
859 startColumn: e.selection.startColumn,
860 endLineNumber: e.selection.endLineNumber,
861 endColumn: e.selection.endColumn,
862 };
863
864 try {
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000865 // Expand selection to full lines for better context
866 const expandedSelection = {
867 startLineNumber: e.selection.startLineNumber,
868 startColumn: 1, // Start at beginning of line
869 endLineNumber: e.selection.endLineNumber,
870 endColumn: model.getLineMaxColumn(e.selection.endLineNumber), // End at end of line
871 };
872
873 // Get the selected text using the expanded selection
874 this.selectedText = model.getValueInRange(expandedSelection);
Autoformatter7ad1c7a2025-05-29 02:00:19 +0000875
Josh Bleecher Snyder444f7f02025-05-28 21:16:55 +0000876 // Update the selection range to reflect the full lines
877 this.selectionRange = {
878 startLineNumber: expandedSelection.startLineNumber,
879 startColumn: expandedSelection.startColumn,
880 endLineNumber: expandedSelection.endLineNumber,
881 endColumn: expandedSelection.endColumn,
882 };
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700883 } catch (error) {
884 console.error("Error getting selected text:", error);
885 return;
886 }
887
888 // If there's selected text, show the indicator
889 if (this.selectedText && this.selectedText.trim() !== "") {
890 // Calculate indicator position safely
891 try {
892 // Use the editor's DOM node as positioning context
893 const editorDomNode = editor.getDomNode();
894 if (!editorDomNode) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700895 return;
896 }
897
898 // Get position from editor
899 const position = {
900 lineNumber: e.selection.endLineNumber,
901 column: e.selection.endColumn,
902 };
903
904 // Use editor's built-in method for coordinate conversion
905 const selectionCoords = editor.getScrolledVisiblePosition(position);
906
907 if (selectionCoords) {
908 // Get accurate DOM position for the selection end
909 const editorRect = editorDomNode.getBoundingClientRect();
910
911 // Calculate the actual screen position
912 const screenLeft = editorRect.left + selectionCoords.left;
913 const screenTop = editorRect.top + selectionCoords.top;
914
915 // Store absolute screen coordinates
916 this.indicatorPosition = {
917 top: screenTop,
918 left: screenLeft + 10, // Slight offset
919 };
920
921 // Check window boundaries to ensure the indicator stays visible
922 const viewportWidth = window.innerWidth;
923 const viewportHeight = window.innerHeight;
924
925 // Keep indicator within viewport bounds
926 if (this.indicatorPosition.left + 150 > viewportWidth) {
927 this.indicatorPosition.left = viewportWidth - 160;
928 }
929
930 if (this.indicatorPosition.top + 40 > viewportHeight) {
931 this.indicatorPosition.top = viewportHeight - 50;
932 }
933
934 // Show the indicator
935 this.showCommentIndicator = true;
936
937 // Request an update to ensure UI reflects changes
938 this.requestUpdate();
939 }
940 } catch (error) {
941 console.error("Error positioning comment indicator:", error);
942 }
943 }
944 } catch (error) {
945 console.error("Error handling selection change:", error);
946 }
947 }
948
949 /**
950 * Handle click on the comment indicator
951 */
952 private handleIndicatorClick(e: Event) {
953 try {
954 e.stopPropagation();
955 e.preventDefault();
956
957 this.showCommentBox = true;
958 this.commentText = ""; // Reset comment text
959
960 // Don't hide the indicator while comment box is shown
961 this.showCommentIndicator = true;
962
963 // Ensure UI updates
964 this.requestUpdate();
965 } catch (error) {
966 console.error("Error handling indicator click:", error);
967 }
968 }
969
970 /**
971 * Handle changes to the comment text
972 */
973 private handleCommentInput(e: Event) {
974 const target = e.target as HTMLTextAreaElement;
975 this.commentText = target.value;
976 }
977
978 /**
979 * Close the comment box
980 */
981 private closeCommentBox() {
982 this.showCommentBox = false;
983 // Also hide the indicator
984 this.showCommentIndicator = false;
985 }
986
987 /**
988 * Submit the comment
989 */
990 private submitComment() {
991 try {
992 if (!this.selectedText || !this.commentText) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700993 return;
994 }
995
996 // Get the correct filename based on active editor
997 const fileContext =
998 this.activeEditor === "original"
999 ? this.originalFilename || "Original file"
1000 : this.modifiedFilename || "Modified file";
1001
1002 // Include editor info to make it clear which version was commented on
1003 const editorLabel =
1004 this.activeEditor === "original" ? "[Original]" : "[Modified]";
1005
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001006 // Add line number information if available
1007 let lineInfo = "";
1008 if (this.selectionRange) {
1009 const startLine = this.selectionRange.startLineNumber;
1010 const endLine = this.selectionRange.endLineNumber;
1011 if (startLine === endLine) {
1012 lineInfo = ` (line ${startLine})`;
1013 } else {
1014 lineInfo = ` (lines ${startLine}-${endLine})`;
1015 }
1016 }
1017
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001018 // Format the comment in a readable way
Josh Bleecher Snyderb34b8b32025-05-28 21:00:56 +00001019 const formattedComment = `\`\`\`\n${fileContext} ${editorLabel}${lineInfo}:\n${this.selectedText}\n\`\`\`\n\n${this.commentText}`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001020
1021 // Close UI before dispatching to prevent interaction conflicts
1022 this.closeCommentBox();
1023
1024 // Use setTimeout to ensure the UI has updated before sending the event
1025 setTimeout(() => {
1026 try {
1027 // Dispatch a custom event with the comment details
1028 const event = new CustomEvent("monaco-comment", {
1029 detail: {
1030 fileContext,
1031 selectedText: this.selectedText,
1032 commentText: this.commentText,
1033 formattedComment,
1034 selectionRange: this.selectionRange,
1035 activeEditor: this.activeEditor,
1036 },
1037 bubbles: true,
1038 composed: true,
1039 });
1040
1041 this.dispatchEvent(event);
1042 } catch (error) {
1043 console.error("Error dispatching comment event:", error);
1044 }
1045 }, 0);
1046 } catch (error) {
1047 console.error("Error submitting comment:", error);
1048 this.closeCommentBox();
1049 }
1050 }
1051
1052 private updateModels() {
1053 try {
1054 // Get language based on filename
1055 const originalLang = this.getLanguageForFile(this.originalFilename || "");
1056 const modifiedLang = this.getLanguageForFile(this.modifiedFilename || "");
1057
1058 // Always create new models with unique URIs based on timestamp to avoid conflicts
1059 const timestamp = new Date().getTime();
1060 // TODO: Could put filename in these URIs; unclear how they're used right now.
1061 const originalUri = monaco.Uri.parse(
1062 `file:///original-${timestamp}.${originalLang}`,
1063 );
1064 const modifiedUri = monaco.Uri.parse(
1065 `file:///modified-${timestamp}.${modifiedLang}`,
1066 );
1067
1068 // Store references to old models
1069 const oldOriginalModel = this.originalModel;
1070 const oldModifiedModel = this.modifiedModel;
1071
1072 // Nullify instance variables to prevent accidental use
1073 this.originalModel = undefined;
1074 this.modifiedModel = undefined;
1075
1076 // Clear the editor model first to release Monaco's internal references
1077 if (this.editor) {
1078 this.editor.setModel(null);
1079 }
1080
1081 // Now it's safe to dispose the old models
1082 if (oldOriginalModel) {
1083 oldOriginalModel.dispose();
1084 }
1085
1086 if (oldModifiedModel) {
1087 oldModifiedModel.dispose();
1088 }
1089
1090 // Create new models
1091 this.originalModel = monaco.editor.createModel(
1092 this.originalCode || "",
1093 originalLang,
1094 originalUri,
1095 );
1096
1097 this.modifiedModel = monaco.editor.createModel(
1098 this.modifiedCode || "",
1099 modifiedLang,
1100 modifiedUri,
1101 );
1102
1103 // Set the new models on the editor
1104 if (this.editor) {
1105 this.editor.setModel({
1106 original: this.originalModel,
1107 modified: this.modifiedModel,
1108 });
Autoformatter9abf8032025-06-14 23:24:08 +00001109
David Crawshaw26f3f342025-06-14 19:58:32 +00001110 // Set initial hideUnchangedRegions state (default to enabled/collapsed)
1111 this.editor.updateOptions({
1112 hideUnchangedRegions: {
1113 enabled: true, // Default to collapsed state
1114 contextLineCount: 3,
1115 minimumLineCount: 3,
1116 revealLineCount: 10,
1117 },
1118 });
Autoformatter9abf8032025-06-14 23:24:08 +00001119
David Crawshaw26f3f342025-06-14 19:58:32 +00001120 // Fit content after setting new models
1121 if (this.fitEditorToContent) {
1122 setTimeout(() => this.fitEditorToContent!(), 50);
1123 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001124 }
1125 this.setupContentChangeListener();
1126 } catch (error) {
1127 console.error("Error updating Monaco models:", error);
1128 }
1129 }
1130
1131 updated(changedProperties: Map<string, any>) {
1132 // If any relevant properties changed, just update the models
1133 if (
1134 changedProperties.has("originalCode") ||
1135 changedProperties.has("modifiedCode") ||
1136 changedProperties.has("originalFilename") ||
1137 changedProperties.has("modifiedFilename") ||
1138 changedProperties.has("editableRight")
1139 ) {
1140 if (this.editor) {
1141 this.updateModels();
1142
David Crawshaw26f3f342025-06-14 19:58:32 +00001143 // Force auto-sizing after model updates
1144 // Use a slightly longer delay to ensure layout is stable
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001145 setTimeout(() => {
David Crawshaw26f3f342025-06-14 19:58:32 +00001146 if (this.fitEditorToContent) {
1147 this.fitEditorToContent();
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001148 }
David Crawshaw26f3f342025-06-14 19:58:32 +00001149 }, 100);
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001150 } else {
1151 // If the editor isn't initialized yet but we received content,
1152 // initialize it now
1153 this.initializeEditor();
1154 }
1155 }
1156 }
1157
David Crawshaw26f3f342025-06-14 19:58:32 +00001158 // Set up auto-sizing for multi-file diff view
1159 private setupAutoSizing() {
1160 if (!this.editor) return;
1161
1162 const fitContent = () => {
1163 try {
1164 const originalEditor = this.editor!.getOriginalEditor();
1165 const modifiedEditor = this.editor!.getModifiedEditor();
Autoformatter9abf8032025-06-14 23:24:08 +00001166
David Crawshaw26f3f342025-06-14 19:58:32 +00001167 const originalHeight = originalEditor.getContentHeight();
1168 const modifiedHeight = modifiedEditor.getContentHeight();
Autoformatter9abf8032025-06-14 23:24:08 +00001169
David Crawshaw26f3f342025-06-14 19:58:32 +00001170 // Use the maximum height of both editors, plus some padding
1171 const maxHeight = Math.max(originalHeight, modifiedHeight) + 18; // 1 blank line bottom padding
Autoformatter9abf8032025-06-14 23:24:08 +00001172
David Crawshaw26f3f342025-06-14 19:58:32 +00001173 // Set both container and host height to enable proper scrolling
1174 if (this.container.value) {
1175 // Set explicit heights on both container and host
1176 this.container.value.style.height = `${maxHeight}px`;
1177 this.style.height = `${maxHeight}px`; // Update host element height
Autoformatter9abf8032025-06-14 23:24:08 +00001178
David Crawshaw26f3f342025-06-14 19:58:32 +00001179 // Emit the height change event BEFORE calling layout
1180 // This ensures parent containers resize first
Autoformatter9abf8032025-06-14 23:24:08 +00001181 this.dispatchEvent(
1182 new CustomEvent("monaco-height-changed", {
1183 detail: { height: maxHeight },
1184 bubbles: true,
1185 composed: true,
1186 }),
1187 );
1188
David Crawshaw26f3f342025-06-14 19:58:32 +00001189 // Layout after both this component and parents have updated
1190 setTimeout(() => {
1191 if (this.editor && this.container.value) {
1192 // Use explicit dimensions to ensure Monaco uses full available space
1193 const width = this.container.value.offsetWidth;
1194 this.editor.layout({
1195 width: width,
Autoformatter9abf8032025-06-14 23:24:08 +00001196 height: maxHeight,
David Crawshaw26f3f342025-06-14 19:58:32 +00001197 });
1198 }
1199 }, 10);
1200 }
1201 } catch (error) {
Autoformatter9abf8032025-06-14 23:24:08 +00001202 console.error("Error in fitContent:", error);
David Crawshaw26f3f342025-06-14 19:58:32 +00001203 }
1204 };
1205
1206 // Store the fit function for external access
1207 this.fitEditorToContent = fitContent;
1208
1209 // Set up listeners for content size changes
1210 this.editor.getOriginalEditor().onDidContentSizeChange(fitContent);
1211 this.editor.getModifiedEditor().onDidContentSizeChange(fitContent);
1212
1213 // Initial fit
1214 fitContent();
1215 }
1216
1217 private fitEditorToContent: (() => void) | null = null;
1218
David Crawshawe2954ce2025-06-15 00:06:34 +00001219 /**
1220 * Set up window resize handler to ensure Monaco editor adapts to browser window changes
1221 */
1222 private setupWindowResizeHandler() {
1223 // Create a debounced resize handler to avoid too many layout calls
1224 let resizeTimeout: number | null = null;
1225
1226 this._windowResizeHandler = () => {
1227 // Clear any existing timeout
1228 if (resizeTimeout) {
1229 window.clearTimeout(resizeTimeout);
1230 }
1231
1232 // Debounce the resize to avoid excessive layout calls
1233 resizeTimeout = window.setTimeout(() => {
1234 if (this.editor && this.container.value) {
1235 // Trigger layout recalculation
1236 if (this.fitEditorToContent) {
1237 this.fitEditorToContent();
1238 } else {
1239 // Fallback: just trigger a layout with current container dimensions
1240 const width = this.container.value.offsetWidth;
1241 const height = this.container.value.offsetHeight;
1242 this.editor.layout({ width, height });
1243 }
1244 }
1245 }, 100); // 100ms debounce
1246 };
1247
1248 // Add the event listener
1249 window.addEventListener('resize', this._windowResizeHandler);
1250 }
1251
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001252 // Add resize observer to ensure editor resizes when container changes
1253 firstUpdated() {
1254 // Initialize the editor
1255 this.initializeEditor();
1256
David Crawshawe2954ce2025-06-15 00:06:34 +00001257 // Set up window resize handler to ensure Monaco editor adapts to browser window changes
1258 this.setupWindowResizeHandler();
1259
David Crawshaw26f3f342025-06-14 19:58:32 +00001260 // For multi-file diff, we don't use ResizeObserver since we control the size
1261 // Instead, we rely on auto-sizing based on content
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001262
1263 // If editable, set up edit mode and content change listener
1264 if (this.editableRight && this.editor) {
1265 // Ensure the original editor is read-only
1266 this.editor.getOriginalEditor().updateOptions({ readOnly: true });
1267 // Ensure the modified editor is editable
1268 this.editor.getModifiedEditor().updateOptions({ readOnly: false });
1269 }
1270 }
1271
1272 private _resizeObserver: ResizeObserver | null = null;
David Crawshawe2954ce2025-06-15 00:06:34 +00001273 private _windowResizeHandler: (() => void) | null = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001274
philip.zeyliger7351cd92025-06-14 12:25:31 -07001275 /**
1276 * Add this Monaco editor instance to the global debug object
1277 * This allows inspection and debugging via browser console
1278 */
1279 private addToDebugGlobal() {
1280 try {
1281 // Initialize the debug global if it doesn't exist
1282 if (!(window as any).sketchDebug) {
1283 (window as any).sketchDebug = {
1284 monaco: monaco,
1285 editors: [],
1286 remeasureFonts: () => {
1287 monaco.editor.remeasureFonts();
1288 (window as any).sketchDebug.editors.forEach(
1289 (editor: any, index: number) => {
1290 if (editor && editor.layout) {
1291 editor.layout();
1292 }
1293 },
1294 );
1295 },
1296 layoutAll: () => {
1297 (window as any).sketchDebug.editors.forEach(
1298 (editor: any, index: number) => {
1299 if (editor && editor.layout) {
1300 editor.layout();
1301 }
1302 },
1303 );
1304 },
1305 getActiveEditors: () => {
1306 return (window as any).sketchDebug.editors.filter(
1307 (editor: any) => editor !== null,
1308 );
1309 },
1310 };
1311 }
1312
1313 // Add this editor to the debug collection
1314 if (this.editor) {
1315 (window as any).sketchDebug.editors.push(this.editor);
1316 }
1317 } catch (error) {
1318 console.error("Error adding Monaco editor to debug global:", error);
1319 }
1320 }
1321
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001322 disconnectedCallback() {
1323 super.disconnectedCallback();
1324
1325 try {
philip.zeyliger7351cd92025-06-14 12:25:31 -07001326 // Remove editor from debug global before disposal
1327 if (this.editor && (window as any).sketchDebug?.editors) {
1328 const index = (window as any).sketchDebug.editors.indexOf(this.editor);
1329 if (index > -1) {
1330 (window as any).sketchDebug.editors[index] = null;
1331 }
1332 }
1333
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001334 // Clean up resources when element is removed
1335 if (this.editor) {
1336 this.editor.dispose();
1337 this.editor = undefined;
1338 }
1339
1340 // Dispose models to prevent memory leaks
1341 if (this.originalModel) {
1342 this.originalModel.dispose();
1343 this.originalModel = undefined;
1344 }
1345
1346 if (this.modifiedModel) {
1347 this.modifiedModel.dispose();
1348 this.modifiedModel = undefined;
1349 }
1350
David Crawshaw26f3f342025-06-14 19:58:32 +00001351 // Clean up resize observer (if any)
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001352 if (this._resizeObserver) {
1353 this._resizeObserver.disconnect();
1354 this._resizeObserver = null;
1355 }
Autoformatter9abf8032025-06-14 23:24:08 +00001356
David Crawshaw26f3f342025-06-14 19:58:32 +00001357 // Clear the fit function reference
1358 this.fitEditorToContent = null;
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001359
1360 // Remove document click handler if set
1361 if (this._documentClickHandler) {
1362 document.removeEventListener("click", this._documentClickHandler);
1363 this._documentClickHandler = null;
1364 }
David Crawshawe2954ce2025-06-15 00:06:34 +00001365
1366 // Remove window resize handler if set
1367 if (this._windowResizeHandler) {
1368 window.removeEventListener('resize', this._windowResizeHandler);
1369 this._windowResizeHandler = null;
1370 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001371 } catch (error) {
1372 console.error("Error in disconnectedCallback:", error);
1373 }
1374 }
1375
1376 // disconnectedCallback implementation is defined below
1377}
1378
1379declare global {
1380 interface HTMLElementTagNameMap {
1381 "sketch-monaco-view": CodeDiffEditor;
1382 }
1383}