Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e229543eb | |||
| f60836eabf | |||
| 318e545435 | |||
| a823e8aaa6 |
BIN
.playwright-mcp/module-resolution-fixed.png
Normal file
BIN
.playwright-mcp/module-resolution-fixed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
21
changelog.md
21
changelog.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-31 - 3.17.0 - feat(editor)
|
||||||
|
add file explorer toolbar, empty-space context menu, editor auto-save, save-all, and keyboard save shortcuts
|
||||||
|
|
||||||
|
- Added filetree toolbar with New File / New Folder actions and toolbar styling
|
||||||
|
- Added right-click context menu for empty filetree space to create files/folders
|
||||||
|
- Implemented editor menu button with context menu (Auto Save toggle, Save, Save All)
|
||||||
|
- Added auto-save toggle with 2s interval and cleanup on disconnect
|
||||||
|
- Implemented Save and Save All APIs that persist files and update IntelliSense manager
|
||||||
|
- Added keyboard shortcuts: Cmd/Ctrl+S to save active file and Cmd/Ctrl+Shift+S to save all
|
||||||
|
- Made tabs scrollable with a tabs container and added an editor menu button
|
||||||
|
|
||||||
|
## 2025-12-30 - 3.16.0 - feat(editor)
|
||||||
|
improve TypeScript IntelliSense and module resolution for Monaco editor
|
||||||
|
|
||||||
|
- Add file cache (fileCache) and getFileContent() for synchronous access to project files
|
||||||
|
- Track and dispose Monaco extra libs (addedExtraLibs) and register project files via addExtraLib to enable TypeScript module resolution
|
||||||
|
- Add addFileAsExtraLib logic to register .ts/.tsx files also under .js/.jsx paths so ESM imports resolve to TypeScript sources
|
||||||
|
- Use ModuleResolutionKind.Bundler fallback to NodeJs and set compilerOptions (baseUrl '/', allowImportingTsExtensions, resolveJsonModule) to improve resolution
|
||||||
|
- Adapt executionEnvironment API usage to readDir/readFile and check entry.type ('directory'|'file') instead of isDirectory/isFile
|
||||||
|
- Add a debugging/screenshot asset: .playwright-mcp/module-resolution-fixed.png
|
||||||
|
|
||||||
## 2025-12-30 - 3.15.0 - feat(editor)
|
## 2025-12-30 - 3.15.0 - feat(editor)
|
||||||
enable file-backed Monaco models and add Problems panel; lazy-init project TypeScript IntelliSense
|
enable file-backed Monaco models and add Problems panel; lazy-init project TypeScript IntelliSense
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.15.0",
|
"version": "3.17.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.15.0',
|
version: '3.17.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,48 @@ export class DeesEditorFiletree extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filetree-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 8%)')};
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button {
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -231,18 +273,25 @@ export class DeesEditorFiletree extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.treeData.length === 0) {
|
|
||||||
return html`
|
|
||||||
<div class="empty">
|
|
||||||
No files found.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="tree-container">
|
<div class="filetree-toolbar">
|
||||||
${this.renderTree(this.treeData)}
|
<span class="toolbar-title">Explorer</span>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<div class="toolbar-button" @click=${() => this.createNewFile('/')} title="New File">
|
||||||
|
<dees-icon .icon=${'lucide:filePlus'} iconSize="16"></dees-icon>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-button" @click=${() => this.createNewFolder('/')} title="New Folder">
|
||||||
|
<dees-icon .icon=${'lucide:folderPlus'} iconSize="16"></dees-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this.treeData.length === 0
|
||||||
|
? html`<div class="empty">No files found.</div>`
|
||||||
|
: html`
|
||||||
|
<div class="tree-container" @contextmenu=${this.handleEmptySpaceContextMenu}>
|
||||||
|
${this.renderTree(this.treeData)}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +463,30 @@ export class DeesEditorFiletree extends DeesElement {
|
|||||||
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleEmptySpaceContextMenu(e: MouseEvent) {
|
||||||
|
// Only trigger if clicking on the container itself, not a tree item
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest('.tree-item')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
name: 'New File',
|
||||||
|
iconName: 'lucide:filePlus',
|
||||||
|
action: async () => this.createNewFile('/'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Folder',
|
||||||
|
iconName: 'lucide:folderPlus',
|
||||||
|
action: async () => this.createNewFolder('/'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
||||||
|
}
|
||||||
|
|
||||||
private async showInputModal(options: {
|
private async showInputModal(options: {
|
||||||
heading: string;
|
heading: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import '../../dees-terminal/dees-terminal.js';
|
|||||||
import '../../dees-icon/dees-icon.js';
|
import '../../dees-icon/dees-icon.js';
|
||||||
import { DeesEditorMonaco } from '../dees-editor-monaco/dees-editor-monaco.js';
|
import { DeesEditorMonaco } from '../dees-editor-monaco/dees-editor-monaco.js';
|
||||||
import { TypeScriptIntelliSenseManager } from './typescript-intellisense.js';
|
import { TypeScriptIntelliSenseManager } from './typescript-intellisense.js';
|
||||||
|
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -198,6 +199,26 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
private intelliSenseManager: TypeScriptIntelliSenseManager | null = null;
|
private intelliSenseManager: TypeScriptIntelliSenseManager | null = null;
|
||||||
private intelliSenseInitialized: boolean = false;
|
private intelliSenseInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Auto-save functionality
|
||||||
|
@state()
|
||||||
|
accessor autoSave: boolean = false;
|
||||||
|
private autoSaveInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// Keyboard shortcut handler (bound for proper cleanup)
|
||||||
|
private keydownHandler = (e: KeyboardEvent) => {
|
||||||
|
// Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 's' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveActiveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+Shift+S - Save All
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveAllFiles();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -390,6 +411,31 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-menu-button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-menu-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -611,18 +657,23 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
|
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<div class="tabs-bar">
|
<div class="tabs-bar">
|
||||||
${this.openFiles.map(file => html`
|
<div class="tabs-container">
|
||||||
<div
|
${this.openFiles.map(file => html`
|
||||||
class="tab ${file.path === this.activeFilePath ? 'active' : ''}"
|
<div
|
||||||
@click=${() => this.activateFile(file.path)}
|
class="tab ${file.path === this.activeFilePath ? 'active' : ''}"
|
||||||
>
|
@click=${() => this.activateFile(file.path)}
|
||||||
${file.modified ? html`<span class="tab-modified"></span>` : ''}
|
>
|
||||||
<span class="tab-name">${file.name}</span>
|
${file.modified ? html`<span class="tab-modified"></span>` : ''}
|
||||||
<span class="tab-close" @click=${(e: Event) => this.closeFile(e, file.path)}>
|
<span class="tab-name">${file.name}</span>
|
||||||
<dees-icon .icon=${'lucide:x'} iconSize="12"></dees-icon>
|
<span class="tab-close" @click=${(e: Event) => this.closeFile(e, file.path)}>
|
||||||
</span>
|
<dees-icon .icon=${'lucide:x'} iconSize="12"></dees-icon>
|
||||||
</div>
|
</span>
|
||||||
`)}
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
<div class="editor-menu-button" @click=${this.showEditorMenu} title="Editor options">
|
||||||
|
<dees-icon .icon=${'lucide:moreVertical'} iconSize="16"></dees-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-content">
|
<div class="editor-content">
|
||||||
${this.openFiles.length === 0 ? html`
|
${this.openFiles.length === 0 ? html`
|
||||||
@@ -690,6 +741,20 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
document.addEventListener('keydown', this.keydownHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
document.removeEventListener('keydown', this.keydownHandler);
|
||||||
|
if (this.autoSaveInterval) {
|
||||||
|
clearInterval(this.autoSaveInterval);
|
||||||
|
this.autoSaveInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
if (this.executionEnvironment) {
|
if (this.executionEnvironment) {
|
||||||
await this.initializeWorkspace();
|
await this.initializeWorkspace();
|
||||||
@@ -880,6 +945,95 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
this.isTerminalCollapsed = !this.isTerminalCollapsed;
|
this.isTerminalCollapsed = !this.isTerminalCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Save Operations ==========
|
||||||
|
|
||||||
|
public async saveActiveFile(): Promise<void> {
|
||||||
|
const file = this.openFiles.find(f => f.path === this.activeFilePath);
|
||||||
|
if (!file || !this.executionEnvironment) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executionEnvironment.writeFile(file.path, file.content);
|
||||||
|
|
||||||
|
// Update file state to mark as saved
|
||||||
|
this.openFiles = this.openFiles.map(f =>
|
||||||
|
f.path === file.path ? { ...f, modified: false } : f
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update IntelliSense manager with latest content
|
||||||
|
if (this.intelliSenseManager) {
|
||||||
|
this.intelliSenseManager.addFileModel(file.path, file.content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveAllFiles(): Promise<void> {
|
||||||
|
if (!this.executionEnvironment) return;
|
||||||
|
|
||||||
|
for (const file of this.openFiles.filter(f => f.modified)) {
|
||||||
|
try {
|
||||||
|
await this.executionEnvironment.writeFile(file.path, file.content);
|
||||||
|
|
||||||
|
// Update IntelliSense manager
|
||||||
|
if (this.intelliSenseManager) {
|
||||||
|
this.intelliSenseManager.addFileModel(file.path, file.content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to save ${file.path}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all files as saved
|
||||||
|
this.openFiles = this.openFiles.map(f => ({ ...f, modified: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Editor Menu ==========
|
||||||
|
|
||||||
|
private async showEditorMenu(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const menuItems: Parameters<typeof DeesContextmenu.openContextMenuWithOptions>[1] = [
|
||||||
|
{
|
||||||
|
name: this.autoSave ? '✓ Auto Save' : 'Auto Save',
|
||||||
|
iconName: 'lucide:save',
|
||||||
|
action: async () => this.toggleAutoSave(),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
iconName: 'lucide:save',
|
||||||
|
action: async () => this.saveActiveFile(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save All',
|
||||||
|
iconName: 'lucide:save',
|
||||||
|
action: async () => this.saveAllFiles(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleAutoSave() {
|
||||||
|
this.autoSave = !this.autoSave;
|
||||||
|
|
||||||
|
if (this.autoSave) {
|
||||||
|
// Save every 2 seconds if there are changes
|
||||||
|
this.autoSaveInterval = setInterval(() => {
|
||||||
|
const hasUnsaved = this.openFiles.some(f => f.modified);
|
||||||
|
if (hasUnsaved) {
|
||||||
|
this.saveAllFiles();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
if (this.autoSaveInterval) {
|
||||||
|
clearInterval(this.autoSaveInterval);
|
||||||
|
this.autoSaveInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getErrorCount(): number {
|
private getErrorCount(): number {
|
||||||
// Monaco MarkerSeverity: Error = 8, Warning = 4, Info = 2, Hint = 1
|
// Monaco MarkerSeverity: Error = 8, Warning = 4, Info = 2, Hint = 1
|
||||||
return this.diagnosticMarkers.filter(m => m.severity === 8).length;
|
return this.diagnosticMarkers.filter(m => m.severity === 8).length;
|
||||||
@@ -976,35 +1130,4 @@ export function createUser(firstName: string, lastName: string): IUser {
|
|||||||
resource: { path: m.resource.path },
|
resource: { path: m.resource.path },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveActiveFile(): Promise<void> {
|
|
||||||
const file = this.openFiles.find(f => f.path === this.activeFilePath);
|
|
||||||
if (!file || !this.executionEnvironment) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.executionEnvironment.writeFile(file.path, file.content);
|
|
||||||
const fileIndex = this.openFiles.findIndex(f => f.path === this.activeFilePath);
|
|
||||||
this.openFiles = [
|
|
||||||
...this.openFiles.slice(0, fileIndex),
|
|
||||||
{ ...file, modified: false },
|
|
||||||
...this.openFiles.slice(fileIndex + 1),
|
|
||||||
];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save file:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async saveAllFiles(): Promise<void> {
|
|
||||||
if (!this.executionEnvironment) return;
|
|
||||||
|
|
||||||
for (const file of this.openFiles.filter(f => f.modified)) {
|
|
||||||
try {
|
|
||||||
await this.executionEnvironment.writeFile(file.path, file.content);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to save ${file.path}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openFiles = this.openFiles.map(f => ({ ...f, modified: false }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import type * as monaco from 'monaco-editor';
|
|||||||
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
||||||
|
|
||||||
// Monaco TypeScript API types (runtime API still exists, types deprecated in 0.55+)
|
// Monaco TypeScript API types (runtime API still exists, types deprecated in 0.55+)
|
||||||
|
interface IExtraLibDisposable {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface IMonacoTypeScriptAPI {
|
interface IMonacoTypeScriptAPI {
|
||||||
typescriptDefaults: {
|
typescriptDefaults: {
|
||||||
setCompilerOptions(options: Record<string, unknown>): void;
|
setCompilerOptions(options: Record<string, unknown>): void;
|
||||||
setDiagnosticsOptions(options: Record<string, unknown>): void;
|
setDiagnosticsOptions(options: Record<string, unknown>): void;
|
||||||
addExtraLib(content: string, filePath?: string): void;
|
addExtraLib(content: string, filePath?: string): IExtraLibDisposable;
|
||||||
setEagerModelSync(value: boolean): void;
|
setEagerModelSync(value: boolean): void;
|
||||||
};
|
};
|
||||||
ScriptTarget: { ES2020: number };
|
ScriptTarget: { ES2020: number };
|
||||||
ModuleKind: { ESNext: number };
|
ModuleKind: { ESNext: number };
|
||||||
ModuleResolutionKind: { NodeJs: number };
|
ModuleResolutionKind: { NodeJs: number; Bundler?: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +27,12 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
private monacoInstance: typeof monaco | null = null;
|
private monacoInstance: typeof monaco | null = null;
|
||||||
private executionEnvironment: IExecutionEnvironment | null = null;
|
private executionEnvironment: IExecutionEnvironment | null = null;
|
||||||
|
|
||||||
|
// Cache of file contents for synchronous access and module resolution
|
||||||
|
private fileCache: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
// Track extra libs added for cleanup
|
||||||
|
private addedExtraLibs: Map<string, IExtraLibDisposable> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get TypeScript API with proper typing for Monaco 0.55+
|
* Get TypeScript API with proper typing for Monaco 0.55+
|
||||||
*/
|
*/
|
||||||
@@ -60,7 +70,7 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
if (!this.executionEnvironment) return;
|
if (!this.executionEnvironment) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = await this.executionEnvironment.readdir(dirPath);
|
const entries = await this.executionEnvironment.readDir(dirPath);
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;
|
const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;
|
||||||
@@ -68,9 +78,9 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
// Skip node_modules - too large and handled separately via addExtraLib
|
// Skip node_modules - too large and handled separately via addExtraLib
|
||||||
if (entry.name === 'node_modules') continue;
|
if (entry.name === 'node_modules') continue;
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.type === 'directory') {
|
||||||
await this.loadFilesFromDirectory(fullPath);
|
await this.loadFilesFromDirectory(fullPath);
|
||||||
} else if (entry.isFile()) {
|
} else if (entry.type === 'file') {
|
||||||
const ext = entry.name.split('.').pop()?.toLowerCase();
|
const ext = entry.name.split('.').pop()?.toLowerCase();
|
||||||
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
|
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
|
||||||
try {
|
try {
|
||||||
@@ -94,7 +104,8 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
ts.typescriptDefaults.setCompilerOptions({
|
ts.typescriptDefaults.setCompilerOptions({
|
||||||
target: ts.ScriptTarget.ES2020,
|
target: ts.ScriptTarget.ES2020,
|
||||||
module: ts.ModuleKind.ESNext,
|
module: ts.ModuleKind.ESNext,
|
||||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
// Use Bundler resolution if available (Monaco 0.45+), fallback to NodeJs
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.Bundler ?? ts.ModuleResolutionKind.NodeJs,
|
||||||
allowSyntheticDefaultImports: true,
|
allowSyntheticDefaultImports: true,
|
||||||
esModuleInterop: true,
|
esModuleInterop: true,
|
||||||
strict: true,
|
strict: true,
|
||||||
@@ -103,6 +114,12 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
checkJs: false,
|
checkJs: false,
|
||||||
allowNonTsExtensions: true,
|
allowNonTsExtensions: true,
|
||||||
lib: ['es2020', 'dom', 'dom.iterable'],
|
lib: ['es2020', 'dom', 'dom.iterable'],
|
||||||
|
// Set baseUrl to root for resolving absolute imports
|
||||||
|
baseUrl: '/',
|
||||||
|
// Allow importing .ts extensions directly (useful for some setups)
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
// Resolve JSON modules
|
||||||
|
resolveJsonModule: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
ts.typescriptDefaults.setDiagnosticsOptions({
|
ts.typescriptDefaults.setDiagnosticsOptions({
|
||||||
@@ -260,10 +277,15 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a file model to Monaco for cross-file IntelliSense
|
* Add a file model to Monaco for cross-file IntelliSense
|
||||||
|
* Also registers the file with TypeScript via addExtraLib for module resolution
|
||||||
*/
|
*/
|
||||||
public addFileModel(path: string, content: string): void {
|
public addFileModel(path: string, content: string): void {
|
||||||
if (!this.monacoInstance) return;
|
if (!this.monacoInstance) return;
|
||||||
|
|
||||||
|
// Cache the content for sync access
|
||||||
|
this.fileCache.set(path, content);
|
||||||
|
|
||||||
|
// Create/update the editor model
|
||||||
const uri = this.monacoInstance.Uri.parse(`file://${path}`);
|
const uri = this.monacoInstance.Uri.parse(`file://${path}`);
|
||||||
const existingModel = this.monacoInstance.editor.getModel(uri);
|
const existingModel = this.monacoInstance.editor.getModel(uri);
|
||||||
|
|
||||||
@@ -273,6 +295,53 @@ export class TypeScriptIntelliSenseManager {
|
|||||||
const language = this.getLanguageFromPath(path);
|
const language = this.getLanguageFromPath(path);
|
||||||
this.monacoInstance.editor.createModel(content, language, uri);
|
this.monacoInstance.editor.createModel(content, language, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also add as extra lib for TypeScript module resolution
|
||||||
|
// This is critical - TypeScript's resolver uses extra libs, not editor models
|
||||||
|
this.addFileAsExtraLib(path, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file as an extra lib for TypeScript module resolution.
|
||||||
|
* This enables TypeScript to resolve imports to project files.
|
||||||
|
*/
|
||||||
|
private addFileAsExtraLib(path: string, content: string): void {
|
||||||
|
const ts = this.tsApi;
|
||||||
|
if (!ts) return;
|
||||||
|
|
||||||
|
// Dispose existing lib if present (for updates)
|
||||||
|
const existing = this.addedExtraLibs.get(path);
|
||||||
|
if (existing) {
|
||||||
|
existing.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the file with its actual path
|
||||||
|
const filePath = `file://${path}`;
|
||||||
|
const disposable = ts.typescriptDefaults.addExtraLib(content, filePath);
|
||||||
|
this.addedExtraLibs.set(path, disposable);
|
||||||
|
|
||||||
|
// For .ts files, also add with .js extension to handle ESM imports
|
||||||
|
// (e.g., import from './utils.js' should resolve to ./utils.ts)
|
||||||
|
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
|
||||||
|
const jsPath = path.replace(/\.ts$/, '.js');
|
||||||
|
const jsFilePath = `file://${jsPath}`;
|
||||||
|
const jsDisposable = ts.typescriptDefaults.addExtraLib(content, jsFilePath);
|
||||||
|
this.addedExtraLibs.set(jsPath, jsDisposable);
|
||||||
|
this.fileCache.set(jsPath, content);
|
||||||
|
} else if (path.endsWith('.tsx')) {
|
||||||
|
const jsxPath = path.replace(/\.tsx$/, '.jsx');
|
||||||
|
const jsxFilePath = `file://${jsxPath}`;
|
||||||
|
const jsxDisposable = ts.typescriptDefaults.addExtraLib(content, jsxFilePath);
|
||||||
|
this.addedExtraLibs.set(jsxPath, jsxDisposable);
|
||||||
|
this.fileCache.set(jsxPath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached file content for synchronous access
|
||||||
|
*/
|
||||||
|
public getFileContent(path: string): string | undefined {
|
||||||
|
return this.fileCache.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLanguageFromPath(path: string): string {
|
private getLanguageFromPath(path: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user