feat(editor): add modal prompts for file/folder creation, improve Monaco editor reactivity and add TypeScript IntelliSense support
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-30 - 3.14.0 - feat(editor)
|
||||||
|
add modal prompts for file/folder creation, improve Monaco editor reactivity and add TypeScript IntelliSense support
|
||||||
|
|
||||||
|
- Replace window.prompt for new file/folder with DeesModal + DeesInputText (showInputModal) to provide a focused modal input UX.
|
||||||
|
- Monaco editor: add language property, handle external content updates without emitting change events (isUpdatingFromExternal), dispatch 'content-change' events, and apply language changes at runtime.
|
||||||
|
- Add TypeScriptIntelliSenseManager to load .d.ts/type packages from the virtual filesystem (/node_modules), parse imports, load @types fallbacks, and add file models to Monaco for cross-file IntelliSense.
|
||||||
|
- Workspace demo now mounts an initial TypeScript project and exposes initializationPromise to wait for external setup; workspace initializes IntelliSense and processes content changes to keep types up to date.
|
||||||
|
- Export typescript-intellisense from workspace index so the manager is available to consumers.
|
||||||
|
|
||||||
## 2025-12-30 - 3.13.1 - fix(webcontainer)
|
## 2025-12-30 - 3.13.1 - fix(webcontainer)
|
||||||
prevent double initialization and race conditions when booting WebContainer and loading editor workspace/file tree
|
prevent double initialization and race conditions when booting WebContainer and loading editor workspace/file tree
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.13.1',
|
version: '3.14.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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import type { IExecutionEnvironment, IFileEntry } from '../../00group-runtime/in
|
|||||||
import '../../dees-icon/dees-icon.js';
|
import '../../dees-icon/dees-icon.js';
|
||||||
import '../../dees-contextmenu/dees-contextmenu.js';
|
import '../../dees-contextmenu/dees-contextmenu.js';
|
||||||
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
||||||
|
import { DeesModal } from '../../dees-modal/dees-modal.js';
|
||||||
|
import '../../00group-input/dees-input-text/dees-input-text.js';
|
||||||
|
import { DeesInputText } from '../../00group-input/dees-input-text/dees-input-text.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -411,8 +414,60 @@ export class DeesEditorFiletree extends DeesElement {
|
|||||||
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
await DeesContextmenu.openContextMenuWithOptions(e, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async showInputModal(options: {
|
||||||
|
heading: string;
|
||||||
|
label: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
let inputValue = '';
|
||||||
|
|
||||||
|
const modal = await DeesModal.createAndShow({
|
||||||
|
heading: options.heading,
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<dees-input-text
|
||||||
|
.label=${options.label}
|
||||||
|
@changeSubject=${(e: CustomEvent) => {
|
||||||
|
inputValue = (e.target as DeesInputText).value;
|
||||||
|
}}
|
||||||
|
></dees-input-text>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalRef) => {
|
||||||
|
await modalRef.destroy();
|
||||||
|
resolve(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
action: async (modalRef) => {
|
||||||
|
await modalRef.destroy();
|
||||||
|
resolve(inputValue.trim() || null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus the input after modal renders
|
||||||
|
await modal.updateComplete;
|
||||||
|
const contentEl = modal.shadowRoot?.querySelector('.modal .content');
|
||||||
|
if (contentEl) {
|
||||||
|
const inputElement = contentEl.querySelector('dees-input-text') as DeesInputText | null;
|
||||||
|
if (inputElement) {
|
||||||
|
await inputElement.updateComplete;
|
||||||
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async createNewFile(parentPath: string) {
|
private async createNewFile(parentPath: string) {
|
||||||
const fileName = prompt('Enter file name:');
|
const fileName = await this.showInputModal({
|
||||||
|
heading: 'New File',
|
||||||
|
label: 'File name',
|
||||||
|
});
|
||||||
if (!fileName || !this.executionEnvironment) return;
|
if (!fileName || !this.executionEnvironment) return;
|
||||||
|
|
||||||
const newPath = parentPath === '/' ? `/${fileName}` : `${parentPath}/${fileName}`;
|
const newPath = parentPath === '/' ? `/${fileName}` : `${parentPath}/${fileName}`;
|
||||||
@@ -432,7 +487,10 @@ export class DeesEditorFiletree extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createNewFolder(parentPath: string) {
|
private async createNewFolder(parentPath: string) {
|
||||||
const folderName = prompt('Enter folder name:');
|
const folderName = await this.showInputModal({
|
||||||
|
heading: 'New Folder',
|
||||||
|
label: 'Folder name',
|
||||||
|
});
|
||||||
if (!folderName || !this.executionEnvironment) return;
|
if (!folderName || !this.executionEnvironment) return;
|
||||||
|
|
||||||
const newPath = parentPath === '/' ? `/${folderName}` : `${parentPath}/${folderName}`;
|
const newPath = parentPath === '/' ? `/${folderName}` : `${parentPath}/${folderName}`;
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ export class DeesEditorMonaco extends DeesElement {
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
public editorDeferred = domtools.plugins.smartpromise.defer<monaco.editor.IStandaloneCodeEditor>();
|
public editorDeferred = domtools.plugins.smartpromise.defer<monaco.editor.IStandaloneCodeEditor>();
|
||||||
public language = 'typescript';
|
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: String
|
type: String
|
||||||
})
|
})
|
||||||
accessor content = "function hello() {\n\talert('Hello world!');\n}";
|
accessor content = "function hello() {\n\talert('Hello world!');\n}";
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
accessor language = 'typescript';
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: Object
|
type: Object
|
||||||
})
|
})
|
||||||
@@ -47,6 +51,7 @@ export class DeesEditorMonaco extends DeesElement {
|
|||||||
accessor wordWrap: monaco.editor.IStandaloneEditorConstructionOptions['wordWrap'] = 'off';
|
accessor wordWrap: monaco.editor.IStandaloneEditorConstructionOptions['wordWrap'] = 'off';
|
||||||
|
|
||||||
private monacoThemeSubscription: domtools.plugins.smartrx.rxjs.Subscription | null = null;
|
private monacoThemeSubscription: domtools.plugins.smartrx.rxjs.Subscription | null = null;
|
||||||
|
private isUpdatingFromExternal: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -138,11 +143,47 @@ export class DeesEditorMonaco extends DeesElement {
|
|||||||
// editor is setup let do the rest
|
// editor is setup let do the rest
|
||||||
const editor = await this.editorDeferred.promise;
|
const editor = await this.editorDeferred.promise;
|
||||||
editor.onDidChangeModelContent(async eventArg => {
|
editor.onDidChangeModelContent(async eventArg => {
|
||||||
this.contentSubject.next(editor.getValue());
|
// Don't emit events when we're programmatically updating the content
|
||||||
|
if (this.isUpdatingFromExternal) return;
|
||||||
|
|
||||||
|
const value = editor.getValue();
|
||||||
|
this.contentSubject.next(value);
|
||||||
|
this.dispatchEvent(new CustomEvent('content-change', {
|
||||||
|
detail: value,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
this.contentSubject.next(editor.getValue());
|
this.contentSubject.next(editor.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string, any>): Promise<void> {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// Handle content changes
|
||||||
|
if (changedProperties.has('content')) {
|
||||||
|
const editor = await this.editorDeferred.promise;
|
||||||
|
const currentValue = editor.getValue();
|
||||||
|
if (currentValue !== this.content) {
|
||||||
|
this.isUpdatingFromExternal = true;
|
||||||
|
editor.setValue(this.content);
|
||||||
|
this.isUpdatingFromExternal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle language changes
|
||||||
|
if (changedProperties.has('language')) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async disconnectedCallback(): Promise<void> {
|
public async disconnectedCallback(): Promise<void> {
|
||||||
await super.disconnectedCallback();
|
await super.disconnectedCallback();
|
||||||
if (this.monacoThemeSubscription) {
|
if (this.monacoThemeSubscription) {
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import * as domtools from '@design.estate/dees-domtools';
|
|||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
||||||
import { WebContainerEnvironment } from '../../00group-runtime/index.js';
|
import { WebContainerEnvironment } from '../../00group-runtime/index.js';
|
||||||
|
import type { FileSystemTree } from '@webcontainer/api';
|
||||||
import '../dees-editor-monaco/dees-editor-monaco.js';
|
import '../dees-editor-monaco/dees-editor-monaco.js';
|
||||||
import '../dees-editor-filetree/dees-editor-filetree.js';
|
import '../dees-editor-filetree/dees-editor-filetree.js';
|
||||||
|
import { DeesEditorFiletree } from '../dees-editor-filetree/dees-editor-filetree.js';
|
||||||
import '../../dees-terminal/dees-terminal.js';
|
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';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -35,9 +38,108 @@ interface IOpenFile {
|
|||||||
export class DeesEditorWorkspace extends DeesElement {
|
export class DeesEditorWorkspace extends DeesElement {
|
||||||
public static demo = () => {
|
public static demo = () => {
|
||||||
const env = new WebContainerEnvironment();
|
const env = new WebContainerEnvironment();
|
||||||
|
|
||||||
|
// Mount initial TypeScript project files
|
||||||
|
const mountPromise = (async () => {
|
||||||
|
await env.init();
|
||||||
|
|
||||||
|
const fileTree: FileSystemTree = {
|
||||||
|
'package.json': {
|
||||||
|
file: {
|
||||||
|
contents: JSON.stringify(
|
||||||
|
{
|
||||||
|
name: 'demo-project',
|
||||||
|
version: '1.0.0',
|
||||||
|
type: 'module',
|
||||||
|
scripts: {
|
||||||
|
build: 'tsc',
|
||||||
|
dev: 'tsc --watch',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
typescript: '^5.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'tsconfig.json': {
|
||||||
|
file: {
|
||||||
|
contents: JSON.stringify(
|
||||||
|
{
|
||||||
|
compilerOptions: {
|
||||||
|
target: 'ES2022',
|
||||||
|
module: 'NodeNext',
|
||||||
|
moduleResolution: 'NodeNext',
|
||||||
|
strict: true,
|
||||||
|
outDir: './dist',
|
||||||
|
rootDir: './src',
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
include: ['src/**/*'],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
src: {
|
||||||
|
directory: {
|
||||||
|
'index.ts': {
|
||||||
|
file: {
|
||||||
|
contents: `// Main entry point
|
||||||
|
import { greet, formatName } from './utils.js';
|
||||||
|
|
||||||
|
const name = formatName('World');
|
||||||
|
console.log(greet(name));
|
||||||
|
|
||||||
|
// Example async function
|
||||||
|
async function main() {
|
||||||
|
const result = await Promise.resolve('Hello from async!');
|
||||||
|
console.log(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utils.ts': {
|
||||||
|
file: {
|
||||||
|
contents: `// Utility functions
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function greet(name: string): string {
|
||||||
|
return \`Hello, \${name}!\`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatName(name: string): string {
|
||||||
|
return name.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUser(firstName: string, lastName: string): IUser {
|
||||||
|
return { firstName, lastName };
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await env.mount(fileTree);
|
||||||
|
})();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div style="width: 100%; height: 600px; position: relative;">
|
<div style="width: 100%; height: 600px; position: relative;">
|
||||||
<dees-editor-workspace .executionEnvironment=${env}></dees-editor-workspace>
|
<dees-editor-workspace
|
||||||
|
.executionEnvironment=${env}
|
||||||
|
.initializationPromise=${mountPromise}
|
||||||
|
></dees-editor-workspace>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
@@ -46,6 +148,9 @@ export class DeesEditorWorkspace extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
accessor executionEnvironment: IExecutionEnvironment | null = null;
|
accessor executionEnvironment: IExecutionEnvironment | null = null;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor initializationPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor showFileTree: boolean = true;
|
accessor showFileTree: boolean = true;
|
||||||
|
|
||||||
@@ -75,6 +180,7 @@ export class DeesEditorWorkspace extends DeesElement {
|
|||||||
|
|
||||||
private editorElement: DeesEditorMonaco | null = null;
|
private editorElement: DeesEditorMonaco | null = null;
|
||||||
private initializationStarted: boolean = false;
|
private initializationStarted: boolean = false;
|
||||||
|
private intelliSenseManager: TypeScriptIntelliSenseManager | null = null;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
@@ -450,9 +556,14 @@ export class DeesEditorWorkspace extends DeesElement {
|
|||||||
this.isInitializing = true;
|
this.isInitializing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.executionEnvironment.ready) {
|
// Wait for any external initialization (e.g., file mounting)
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
await this.initializationPromise;
|
||||||
|
} else if (!this.executionEnvironment.ready) {
|
||||||
await this.executionEnvironment.init();
|
await this.executionEnvironment.init();
|
||||||
}
|
}
|
||||||
|
// Initialize IntelliSense after workspace is ready
|
||||||
|
await this.initializeIntelliSense();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize workspace:', error);
|
console.error('Failed to initialize workspace:', error);
|
||||||
// Reset flag to allow retry
|
// Reset flag to allow retry
|
||||||
@@ -462,6 +573,20 @@ export class DeesEditorWorkspace extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initializeIntelliSense(): Promise<void> {
|
||||||
|
if (!this.executionEnvironment) return;
|
||||||
|
|
||||||
|
// Wait for Monaco to be available globally
|
||||||
|
const monacoInstance = (window as any).monaco;
|
||||||
|
if (!monacoInstance) {
|
||||||
|
console.warn('Monaco not loaded, IntelliSense disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intelliSenseManager = new TypeScriptIntelliSenseManager();
|
||||||
|
await this.intelliSenseManager.init(monacoInstance, this.executionEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
private async handleFileSelect(e: CustomEvent<{ path: string; name: string }>) {
|
private async handleFileSelect(e: CustomEvent<{ path: string; name: string }>) {
|
||||||
const { path, name } = e.detail;
|
const { path, name } = e.detail;
|
||||||
await this.openFile(path, name);
|
await this.openFile(path, name);
|
||||||
@@ -537,6 +662,12 @@ export class DeesEditorWorkspace extends DeesElement {
|
|||||||
{ ...file, content: newContent, modified: true },
|
{ ...file, content: newContent, modified: true },
|
||||||
...this.openFiles.slice(fileIndex + 1),
|
...this.openFiles.slice(fileIndex + 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Process content for IntelliSense (TypeScript/JavaScript files)
|
||||||
|
const language = this.getLanguageFromPath(this.activeFilePath);
|
||||||
|
if (this.intelliSenseManager && (language === 'typescript' || language === 'javascript')) {
|
||||||
|
this.intelliSenseManager.processContentChange(newContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './dees-editor-workspace.js';
|
export * from './dees-editor-workspace.js';
|
||||||
|
export * from './typescript-intellisense.js';
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import type * as monaco from 'monaco-editor';
|
||||||
|
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages TypeScript IntelliSense by loading type definitions
|
||||||
|
* from the virtual filesystem into Monaco.
|
||||||
|
*/
|
||||||
|
export class TypeScriptIntelliSenseManager {
|
||||||
|
private loadedLibs: Set<string> = new Set();
|
||||||
|
private monacoInstance: typeof monaco | null = null;
|
||||||
|
private executionEnvironment: IExecutionEnvironment | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize with Monaco and execution environment
|
||||||
|
*/
|
||||||
|
public async init(
|
||||||
|
monacoInst: typeof monaco,
|
||||||
|
env: IExecutionEnvironment
|
||||||
|
): Promise<void> {
|
||||||
|
this.monacoInstance = monacoInst;
|
||||||
|
this.executionEnvironment = env;
|
||||||
|
this.configureCompilerOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private configureCompilerOptions(): void {
|
||||||
|
if (!this.monacoInstance) return;
|
||||||
|
|
||||||
|
this.monacoInstance.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||||
|
target: this.monacoInstance.languages.typescript.ScriptTarget.ES2020,
|
||||||
|
module: this.monacoInstance.languages.typescript.ModuleKind.ESNext,
|
||||||
|
moduleResolution: this.monacoInstance.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||||
|
allowSyntheticDefaultImports: true,
|
||||||
|
esModuleInterop: true,
|
||||||
|
strict: true,
|
||||||
|
noEmit: true,
|
||||||
|
allowJs: true,
|
||||||
|
checkJs: false,
|
||||||
|
lib: ['es2020', 'dom', 'dom.iterable'],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.monacoInstance.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||||
|
noSemanticValidation: false,
|
||||||
|
noSyntaxValidation: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse imports from TypeScript/JavaScript content
|
||||||
|
*/
|
||||||
|
public parseImports(content: string): string[] {
|
||||||
|
const imports: string[] = [];
|
||||||
|
|
||||||
|
// Match ES6 imports: import { x } from 'package' or import 'package'
|
||||||
|
const importRegex = /import\s+(?:[\w*{}\s,]+from\s+)?['"]([^'"]+)['"]/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = importRegex.exec(content)) !== null) {
|
||||||
|
const importPath = match[1];
|
||||||
|
// Only process non-relative imports (npm packages)
|
||||||
|
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
||||||
|
const packageName = importPath.startsWith('@')
|
||||||
|
? importPath.split('/').slice(0, 2).join('/') // @scope/package
|
||||||
|
: importPath.split('/')[0]; // package
|
||||||
|
imports.push(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match require calls: require('package')
|
||||||
|
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
||||||
|
while ((match = requireRegex.exec(content)) !== null) {
|
||||||
|
const importPath = match[1];
|
||||||
|
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
||||||
|
const packageName = importPath.startsWith('@')
|
||||||
|
? importPath.split('/').slice(0, 2).join('/')
|
||||||
|
: importPath.split('/')[0];
|
||||||
|
imports.push(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(imports)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load type definitions for a package from virtual FS
|
||||||
|
*/
|
||||||
|
public async loadTypesForPackage(packageName: string): Promise<void> {
|
||||||
|
if (!this.monacoInstance || !this.executionEnvironment) return;
|
||||||
|
if (this.loadedLibs.has(packageName)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const typesLoaded = await this.tryLoadPackageTypes(packageName);
|
||||||
|
if (!typesLoaded) {
|
||||||
|
await this.tryLoadAtTypesPackage(packageName);
|
||||||
|
}
|
||||||
|
this.loadedLibs.add(packageName);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load types for ${packageName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryLoadPackageTypes(packageName: string): Promise<boolean> {
|
||||||
|
if (!this.executionEnvironment || !this.monacoInstance) return false;
|
||||||
|
|
||||||
|
const basePath = `/node_modules/${packageName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check package.json for types field
|
||||||
|
const packageJsonPath = `${basePath}/package.json`;
|
||||||
|
if (await this.executionEnvironment.exists(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
await this.executionEnvironment.readFile(packageJsonPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
const typesPath = packageJson.types || packageJson.typings;
|
||||||
|
if (typesPath) {
|
||||||
|
const fullTypesPath = `${basePath}/${typesPath}`;
|
||||||
|
if (await this.executionEnvironment.exists(fullTypesPath)) {
|
||||||
|
const content = await this.executionEnvironment.readFile(fullTypesPath);
|
||||||
|
this.monacoInstance.languages.typescript.typescriptDefaults.addExtraLib(
|
||||||
|
content,
|
||||||
|
`file://${fullTypesPath}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try common locations
|
||||||
|
const commonPaths = [
|
||||||
|
`${basePath}/index.d.ts`,
|
||||||
|
`${basePath}/dist/index.d.ts`,
|
||||||
|
`${basePath}/lib/index.d.ts`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dtsPath of commonPaths) {
|
||||||
|
if (await this.executionEnvironment.exists(dtsPath)) {
|
||||||
|
const content = await this.executionEnvironment.readFile(dtsPath);
|
||||||
|
this.monacoInstance.languages.typescript.typescriptDefaults.addExtraLib(
|
||||||
|
content,
|
||||||
|
`file://${dtsPath}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryLoadAtTypesPackage(packageName: string): Promise<boolean> {
|
||||||
|
if (!this.executionEnvironment || !this.monacoInstance) return false;
|
||||||
|
|
||||||
|
// Handle scoped packages: @scope/package -> @types/scope__package
|
||||||
|
const typesPackageName = packageName.startsWith('@')
|
||||||
|
? `@types/${packageName.slice(1).replace('/', '__')}`
|
||||||
|
: `@types/${packageName}`;
|
||||||
|
|
||||||
|
const basePath = `/node_modules/${typesPackageName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const indexPath = `${basePath}/index.d.ts`;
|
||||||
|
if (await this.executionEnvironment.exists(indexPath)) {
|
||||||
|
const content = await this.executionEnvironment.readFile(indexPath);
|
||||||
|
this.monacoInstance.languages.typescript.typescriptDefaults.addExtraLib(
|
||||||
|
content,
|
||||||
|
`file://${indexPath}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process content change and load types for any new imports
|
||||||
|
*/
|
||||||
|
public async processContentChange(content: string): Promise<void> {
|
||||||
|
const imports = this.parseImports(content);
|
||||||
|
for (const packageName of imports) {
|
||||||
|
await this.loadTypesForPackage(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file model to Monaco for cross-file IntelliSense
|
||||||
|
*/
|
||||||
|
public addFileModel(path: string, content: string): void {
|
||||||
|
if (!this.monacoInstance) return;
|
||||||
|
|
||||||
|
const uri = this.monacoInstance.Uri.parse(`file://${path}`);
|
||||||
|
const existingModel = this.monacoInstance.editor.getModel(uri);
|
||||||
|
|
||||||
|
if (existingModel) {
|
||||||
|
existingModel.setValue(content);
|
||||||
|
} else {
|
||||||
|
const language = this.getLanguageFromPath(path);
|
||||||
|
this.monacoInstance.editor.createModel(content, language, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLanguageFromPath(path: string): string {
|
||||||
|
const ext = path.split('.').pop()?.toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
return 'typescript';
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
return 'javascript';
|
||||||
|
case 'json':
|
||||||
|
return 'json';
|
||||||
|
default:
|
||||||
|
return 'plaintext';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user