diff --git a/.playwright-mcp/intellisense-test.png b/.playwright-mcp/intellisense-test.png new file mode 100644 index 0000000..4e9e81f Binary files /dev/null and b/.playwright-mcp/intellisense-test.png differ diff --git a/.playwright-mcp/workspace-full.png b/.playwright-mcp/workspace-full.png new file mode 100644 index 0000000..e4cb380 Binary files /dev/null and b/.playwright-mcp/workspace-full.png differ diff --git a/.playwright-mcp/workspace-with-problems-panel.png b/.playwright-mcp/workspace-with-problems-panel.png new file mode 100644 index 0000000..e4cb380 Binary files /dev/null and b/.playwright-mcp/workspace-with-problems-panel.png differ diff --git a/changelog.md b/changelog.md index ace2ec3..2b1c335 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-12-30 - 3.15.0 - feat(editor) +enable file-backed Monaco models and add Problems panel; lazy-init project TypeScript IntelliSense + +- dees-editor-monaco: add `filePath` property and create/get Monaco models with file:// URIs so editors are backed by real models; sync content into models and handle model switching when filePath changes; enable hover config and improved lifecycle handling. +- dees-editor-workspace: add bottom 'Problems' panel and panel tabs (terminal/problems), diagnosticMarkers state, marker listener, UI for problem list, and navigation to file/position when a problem is clicked; initialize IntelliSense lazily when a file is opened. +- typescript-intellisense: index project .ts/.js files from the virtual filesystem into Monaco models for cross-file resolution, enable allowNonTsExtensions and set eager model sync so TypeScript processes models eagerly. +- General: improved handling for language changes, model language switching, and deferred initialization of the IntelliSense manager. +- Add Playwright test images (workspace screenshots) used by CI/tests. + ## 2025-12-30 - 3.14.2 - fix(editor) bump monaco-editor to 0.55.1 and adapt TypeScript intellisense integration to the updated Monaco API diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 6ee9f78..78e4cac 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.14.2', + version: '3.15.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-editor/dees-editor-monaco/dees-editor-monaco.ts b/ts_web/elements/00group-editor/dees-editor-monaco/dees-editor-monaco.ts index 51c134c..25c1fa6 100644 --- a/ts_web/elements/00group-editor/dees-editor-monaco/dees-editor-monaco.ts +++ b/ts_web/elements/00group-editor/dees-editor-monaco/dees-editor-monaco.ts @@ -40,6 +40,11 @@ export class DeesEditorMonaco extends DeesElement { }) accessor language = 'typescript'; + @property({ + type: String + }) + accessor filePath: string = ''; + @property({ type: Object }) @@ -114,14 +119,35 @@ export class DeesEditorMonaco extends DeesElement { const isBright = domtoolsInstance.themeManager.goBrightBoolean; const initialTheme = isBright ? 'vs' : 'vs-dark'; - const editor = ((window as any).monaco.editor as typeof monaco.editor).create(container, { - value: this.content, - language: this.language, + const monacoInstance = (window as any).monaco as typeof monaco; + + // Create or get model with proper file URI for TypeScript IntelliSense + let model: monaco.editor.ITextModel | null = null; + if (this.filePath) { + const uri = monacoInstance.Uri.parse(`file://${this.filePath}`); + model = monacoInstance.editor.getModel(uri); + if (!model) { + model = monacoInstance.editor.createModel(this.content, this.language, uri); + } else { + model.setValue(this.content); + } + } + + const editor = (monacoInstance.editor as typeof monaco.editor).create(container, { + model: model || undefined, + value: model ? undefined : this.content, + language: model ? undefined : this.language, theme: initialTheme, useShadowDOM: true, fontSize: 16, automaticLayout: true, - wordWrap: this.wordWrap + wordWrap: this.wordWrap, + hover: { + enabled: true, + delay: 300, + sticky: true, + above: false, + }, }); // Subscribe to theme changes @@ -160,7 +186,35 @@ export class DeesEditorMonaco extends DeesElement { public async updated(changedProperties: Map): Promise { super.updated(changedProperties); - // Handle content changes + const monacoInstance = (window as any).monaco as typeof monaco; + if (!monacoInstance) return; + + // Handle filePath changes - switch to different model + if (changedProperties.has('filePath') && this.filePath) { + const editor = await this.editorDeferred.promise; + const uri = monacoInstance.Uri.parse(`file://${this.filePath}`); + let model = monacoInstance.editor.getModel(uri); + + if (!model) { + model = monacoInstance.editor.createModel(this.content, this.language, uri); + } else { + // Update model content if different + if (model.getValue() !== this.content) { + this.isUpdatingFromExternal = true; + model.setValue(this.content); + this.isUpdatingFromExternal = false; + } + } + + // Switch editor to use this model + const currentModel = editor.getModel(); + if (currentModel?.uri.toString() !== uri.toString()) { + editor.setModel(model); + } + return; // filePath change handles content too + } + + // Handle content changes (when no filePath or filePath unchanged) if (changedProperties.has('content')) { const editor = await this.editorDeferred.promise; const currentValue = editor.getValue(); @@ -176,10 +230,7 @@ export class DeesEditorMonaco extends DeesElement { const editor = await this.editorDeferred.promise; const model = editor.getModel(); if (model) { - const monacoInstance = (window as any).monaco; - if (monacoInstance) { - monacoInstance.editor.setModelLanguage(model, this.language); - } + monacoInstance.editor.setModelLanguage(model, this.language); } } } diff --git a/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts b/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts index ba237b8..f068da7 100644 --- a/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts +++ b/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts @@ -178,9 +178,25 @@ export function createUser(firstName: string, lastName: string): IUser { @state() accessor isInitializing: boolean = true; + @state() + accessor activeBottomPanel: 'terminal' | 'problems' = 'terminal'; + + @state() + accessor diagnosticMarkers: Array<{ + message: string; + severity: number; + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + source?: string; + resource: { path: string }; + }> = []; + private editorElement: DeesEditorMonaco | null = null; private initializationStarted: boolean = false; private intelliSenseManager: TypeScriptIntelliSenseManager | null = null; + private intelliSenseInitialized: boolean = false; public static styles = [ themeDefaultStyles, @@ -387,6 +403,127 @@ export function createUser(firstName: string, lastName: string): IUser { bottom: 0; } + .problems-content { + position: absolute; + top: 32px; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; + } + + .panel-tabs { + display: flex; + align-items: center; + gap: 0; + } + + .panel-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 0 12px; + height: 32px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + border-bottom: 2px solid transparent; + transition: all 0.15s ease; + } + + .panel-tab:hover { + color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 75%)')}; + background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 12%)')}; + } + + .panel-tab.active { + color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; + border-bottom-color: ${cssManager.bdTheme('hsl(210 100% 50%)', 'hsl(210 100% 60%)')}; + } + + .panel-tab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + font-size: 11px; + font-weight: 600; + background: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 45%)')}; + color: white; + } + + .panel-tab-badge.warning { + background: ${cssManager.bdTheme('hsl(40 70% 50%)', 'hsl(40 70% 45%)')}; + } + + .panel-tab-badge.none { + display: none; + } + + .problems-list { + padding: 4px 0; + } + + .problem-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 12px; + cursor: pointer; + font-size: 12px; + line-height: 1.4; + color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; + transition: background 0.1s ease; + } + + .problem-item:hover { + background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')}; + } + + .problem-icon { + flex-shrink: 0; + margin-top: 2px; + } + + .problem-icon.error { + color: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 60%)')}; + } + + .problem-icon.warning { + color: ${cssManager.bdTheme('hsl(40 70% 50%)', 'hsl(40 70% 60%)')}; + } + + .problem-details { + flex: 1; + min-width: 0; + } + + .problem-message { + word-break: break-word; + } + + .problem-location { + margin-top: 2px; + font-size: 11px; + color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; + } + + .problems-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')}; + font-size: 13px; + gap: 8px; + } + .empty-state { display: flex; flex-direction: column; @@ -495,6 +632,7 @@ export function createUser(firstName: string, lastName: string): IUser { ` : html`
-
- - Terminal +
+
this.activeBottomPanel = 'terminal'} + > + + Terminal +
+
this.activeBottomPanel = 'problems'} + > + + Problems + ${this.diagnosticMarkers.length > 0 ? html` + ${this.diagnosticMarkers.length} + ` : ''} +
@@ -522,12 +675,15 @@ export function createUser(firstName: string, lastName: string): IUser {
-
+
+
+ ${this.renderProblemsPanel()} +
` : ''}
@@ -562,8 +718,7 @@ export function createUser(firstName: string, lastName: string): IUser { } else if (!this.executionEnvironment.ready) { await this.executionEnvironment.init(); } - // Initialize IntelliSense after workspace is ready - await this.initializeIntelliSense(); + // IntelliSense is initialized lazily when first file is opened (Monaco loads on demand) } catch (error) { console.error('Failed to initialize workspace:', error); // Reset flag to allow retry @@ -575,16 +730,27 @@ export function createUser(firstName: string, lastName: string): IUser { private async initializeIntelliSense(): Promise { if (!this.executionEnvironment) return; + if (this.intelliSenseInitialized) return; - // Wait for Monaco to be available globally - const monacoInstance = (window as any).monaco; + // Wait for Monaco to be available globally (with retry for timing) + let monacoInstance = (window as any).monaco; if (!monacoInstance) { - console.warn('Monaco not loaded, IntelliSense disabled'); + // Monaco loads asynchronously when the editor mounts, wait a bit + await new Promise(resolve => setTimeout(resolve, 100)); + monacoInstance = (window as any).monaco; + } + + if (!monacoInstance) { + console.warn('Monaco not yet loaded, IntelliSense will be initialized later'); return; } + this.intelliSenseInitialized = true; this.intelliSenseManager = new TypeScriptIntelliSenseManager(); await this.intelliSenseManager.init(monacoInstance, this.executionEnvironment); + + // Set up marker listener for Problems panel + this.setupMarkerListener(); } private async handleFileSelect(e: CustomEvent<{ path: string; name: string }>) { @@ -610,6 +776,21 @@ export function createUser(firstName: string, lastName: string): IUser { { path, name, content, modified: false }, ]; this.activeFilePath = path; + + // Initialize IntelliSense lazily after first file opens (Monaco loads on demand) + if (!this.intelliSenseInitialized) { + // Wait for Monaco editor to mount and load Monaco from CDN + await this.updateComplete; + // Give Monaco time to load via require.js + await new Promise(resolve => setTimeout(resolve, 500)); + await this.initializeIntelliSense(); + + // Process the initial file content for IntelliSense + const language = this.getLanguageFromPath(path); + if (this.intelliSenseManager && (language === 'typescript' || language === 'javascript')) { + await this.intelliSenseManager.processContentChange(content); + } + } } catch (error) { console.error(`Failed to open file ${path}:`, error); } @@ -699,6 +880,103 @@ export function createUser(firstName: string, lastName: string): IUser { this.isTerminalCollapsed = !this.isTerminalCollapsed; } + private getErrorCount(): number { + // Monaco MarkerSeverity: Error = 8, Warning = 4, Info = 2, Hint = 1 + return this.diagnosticMarkers.filter(m => m.severity === 8).length; + } + + private renderProblemsPanel(): TemplateResult { + if (this.diagnosticMarkers.length === 0) { + return html` +
+ + No problems detected +
+ `; + } + + return html` +
+ ${this.diagnosticMarkers.map(marker => html` +
this.navigateToProblem(marker)}> + +
+
${marker.message}
+
+ ${marker.resource.path.split('/').pop()} (${marker.startLineNumber}, ${marker.startColumn}) + ${marker.source ? `[${marker.source}]` : ''} +
+
+
+ `)} +
+ `; + } + + private async navigateToProblem(marker: typeof this.diagnosticMarkers[0]) { + // Extract file path from resource + const filePath = marker.resource.path; + const fileName = filePath.split('/').pop() || ''; + + // Open the file if not already open + const existingFile = this.openFiles.find(f => f.path === filePath); + if (!existingFile) { + await this.openFile(filePath, fileName); + } else { + this.activeFilePath = filePath; + } + + // Wait for editor to be ready, then navigate to the line + await this.updateComplete; + const editorElement = this.shadowRoot?.querySelector('dees-editor-monaco') as DeesEditorMonaco; + if (editorElement) { + const editor = await editorElement.editorDeferred.promise; + editor.revealLineInCenter(marker.startLineNumber); + editor.setPosition({ + lineNumber: marker.startLineNumber, + column: marker.startColumn, + }); + editor.focus(); + } + } + + private setupMarkerListener() { + const monacoInstance = (window as any).monaco; + if (!monacoInstance) return; + + // Listen for marker changes + monacoInstance.editor.onDidChangeMarkers((uris: any[]) => { + this.updateDiagnosticMarkers(); + }); + + // Initial load + this.updateDiagnosticMarkers(); + } + + private updateDiagnosticMarkers() { + const monacoInstance = (window as any).monaco; + if (!monacoInstance) return; + + // Get all markers from Monaco + const allMarkers = monacoInstance.editor.getModelMarkers({}); + + // Transform to our format + this.diagnosticMarkers = allMarkers.map((m: any) => ({ + message: m.message, + severity: m.severity, + startLineNumber: m.startLineNumber, + startColumn: m.startColumn, + endLineNumber: m.endLineNumber, + endColumn: m.endColumn, + source: m.source, + resource: { path: m.resource.path }, + })); + } + public async saveActiveFile(): Promise { const file = this.openFiles.find(f => f.path === this.activeFilePath); if (!file || !this.executionEnvironment) return; diff --git a/ts_web/elements/00group-editor/dees-editor-workspace/typescript-intellisense.ts b/ts_web/elements/00group-editor/dees-editor-workspace/typescript-intellisense.ts index c82fb01..721acef 100644 --- a/ts_web/elements/00group-editor/dees-editor-workspace/typescript-intellisense.ts +++ b/ts_web/elements/00group-editor/dees-editor-workspace/typescript-intellisense.ts @@ -7,6 +7,7 @@ interface IMonacoTypeScriptAPI { setCompilerOptions(options: Record): void; setDiagnosticsOptions(options: Record): void; addExtraLib(content: string, filePath?: string): void; + setEagerModelSync(value: boolean): void; }; ScriptTarget: { ES2020: number }; ModuleKind: { ESNext: number }; @@ -40,6 +41,50 @@ export class TypeScriptIntelliSenseManager { this.monacoInstance = monacoInst; this.executionEnvironment = env; this.configureCompilerOptions(); + // Load all project TypeScript/JavaScript files into Monaco for cross-file resolution + await this.loadAllProjectFiles(); + } + + /** + * Recursively load all .ts/.js files from the virtual filesystem into Monaco + */ + private async loadAllProjectFiles(): Promise { + if (!this.executionEnvironment) return; + await this.loadFilesFromDirectory('/'); + } + + /** + * Recursively load files from a directory + */ + private async loadFilesFromDirectory(dirPath: string): Promise { + if (!this.executionEnvironment) return; + + try { + const entries = await this.executionEnvironment.readdir(dirPath); + + for (const entry of entries) { + const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`; + + // Skip node_modules - too large and handled separately via addExtraLib + if (entry.name === 'node_modules') continue; + + if (entry.isDirectory()) { + await this.loadFilesFromDirectory(fullPath); + } else if (entry.isFile()) { + const ext = entry.name.split('.').pop()?.toLowerCase(); + if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') { + try { + const content = await this.executionEnvironment.readFile(fullPath); + this.addFileModel(fullPath, content); + } catch { + // Ignore files that can't be read + } + } + } + } + } catch { + // Directory might not exist or not be readable + } } private configureCompilerOptions(): void { @@ -56,6 +101,7 @@ export class TypeScriptIntelliSenseManager { noEmit: true, allowJs: true, checkJs: false, + allowNonTsExtensions: true, lib: ['es2020', 'dom', 'dom.iterable'], }); @@ -63,6 +109,10 @@ export class TypeScriptIntelliSenseManager { noSemanticValidation: false, noSyntaxValidation: false, }); + + // Enable eager model sync so TypeScript immediately processes all models + // This is critical for cross-file IntelliSense to work without requiring edits + ts.typescriptDefaults.setEagerModelSync(true); } /**