feat(workspace): rename editor components to workspace group and move terminal & TypeScript intellisense into workspace

This commit is contained in:
2025-12-31 12:37:14 +00:00
parent 08b302bd46
commit 15bca09086
29 changed files with 517 additions and 91 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from './dees-workspace.js';
export * from './typescript-intellisense.js';

View File

@@ -0,0 +1,450 @@
import type * as monaco from 'monaco-editor';
import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
// Monaco TypeScript API types (runtime API still exists, types deprecated in 0.55+)
interface IExtraLibDisposable {
dispose(): void;
}
interface IMonacoTypeScriptAPI {
typescriptDefaults: {
setCompilerOptions(options: Record<string, unknown>): void;
setDiagnosticsOptions(options: Record<string, unknown>): void;
addExtraLib(content: string, filePath?: string): IExtraLibDisposable;
setEagerModelSync(value: boolean): void;
};
ScriptTarget: { ES2020: number };
ModuleKind: { ESNext: number };
ModuleResolutionKind: { NodeJs: number; Bundler?: number };
}
/**
* Manages TypeScript IntelliSense by loading type definitions
* from the virtual filesystem into Monaco.
*/
export class TypeScriptIntelliSenseManager {
private loadedLibs: Set<string> = new Set();
private notFoundPackages: Set<string> = new Set(); // Packages checked but not found
private monacoInstance: typeof monaco | 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+
*/
private get tsApi(): IMonacoTypeScriptAPI | null {
if (!this.monacoInstance) return null;
return (this.monacoInstance.languages as any).typescript as IMonacoTypeScriptAPI;
}
/**
* Initialize with Monaco and execution environment
*/
public async init(
monacoInst: typeof monaco,
env: IExecutionEnvironment
): Promise<void> {
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<void> {
if (!this.executionEnvironment) return;
await this.loadFilesFromDirectory('/');
}
/**
* Recursively load files from a directory
*/
private async loadFilesFromDirectory(dirPath: string): Promise<void> {
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.type === 'directory') {
await this.loadFilesFromDirectory(fullPath);
} else if (entry.type === 'file') {
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 {
const ts = this.tsApi;
if (!ts) return;
ts.typescriptDefaults.setCompilerOptions({
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ESNext,
// Use Bundler resolution if available (Monaco 0.45+), fallback to NodeJs
moduleResolution: ts.ModuleResolutionKind.Bundler ?? ts.ModuleResolutionKind.NodeJs,
allowSyntheticDefaultImports: true,
esModuleInterop: true,
strict: true,
noEmit: true,
allowJs: true,
checkJs: false,
allowNonTsExtensions: true,
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({
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);
}
/**
* 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;
if (this.notFoundPackages.has(packageName)) return; // Skip packages we already checked
try {
let typesLoaded = await this.tryLoadPackageTypes(packageName);
if (!typesLoaded) {
typesLoaded = await this.tryLoadAtTypesPackage(packageName);
}
if (typesLoaded) {
this.loadedLibs.add(packageName);
} else {
// Cache that this package wasn't found to avoid repeated filesystem checks
this.notFoundPackages.add(packageName);
}
} catch (error) {
console.warn(`Failed to load types for ${packageName}:`, error);
}
}
private async tryLoadPackageTypes(packageName: string): Promise<boolean> {
const ts = this.tsApi;
if (!this.executionEnvironment || !ts) return false;
const basePath = `/node_modules/${packageName}`;
try {
// Check package.json for types field
const packageJsonPath = `${basePath}/package.json`;
const packageJsonExists = await this.executionEnvironment.exists(packageJsonPath);
if (packageJsonExists) {
const packageJsonContent = await this.executionEnvironment.readFile(packageJsonPath);
const packageJson = JSON.parse(packageJsonContent);
// Add package.json to Monaco so TypeScript can resolve the types field
ts.typescriptDefaults.addExtraLib(packageJsonContent, `file://${packageJsonPath}`);
const typesPath = packageJson.types || packageJson.typings;
if (typesPath) {
// Load all .d.ts files from the package, not just the entry point
// Modern packages often have multiple declaration files with imports
await this.loadAllDtsFilesFromPackage(basePath);
return true;
}
}
// Try common locations - if any exist, load all .d.ts files
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)) {
await this.loadAllDtsFilesFromPackage(basePath);
return true;
}
}
return false;
} catch (error) {
console.error(`Failed to load package types for ${packageName}:`, error);
return false;
}
}
/**
* Recursively load all .d.ts files from a package directory
*/
private async loadAllDtsFilesFromPackage(basePath: string): Promise<void> {
const ts = this.tsApi;
if (!this.executionEnvironment || !ts) return;
await this.loadDtsFilesFromDirectory(basePath);
}
/**
* Recursively load .d.ts files from a directory
*/
private async loadDtsFilesFromDirectory(dirPath: string): Promise<void> {
const ts = this.tsApi;
if (!this.executionEnvironment || !ts) return;
try {
const entries = await this.executionEnvironment.readDir(dirPath);
for (const entry of entries) {
const fullPath = dirPath === '/' ? `/${entry.name}` : `${dirPath}/${entry.name}`;
// Skip nested node_modules (shouldn't happen in a package but be safe)
if (entry.name === 'node_modules') continue;
if (entry.type === 'directory') {
await this.loadDtsFilesFromDirectory(fullPath);
} else if (entry.type === 'file' && entry.name.endsWith('.d.ts')) {
try {
const content = await this.executionEnvironment.readFile(fullPath);
ts.typescriptDefaults.addExtraLib(content, `file://${fullPath}`);
} catch {
// Ignore files that can't be read
}
}
}
} catch {
// Directory might not be readable
}
}
private async tryLoadAtTypesPackage(packageName: string): Promise<boolean> {
if (!this.executionEnvironment) 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)) {
// Load all .d.ts files from the @types package
await this.loadAllDtsFilesFromPackage(basePath);
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) {
if (!this.loadedLibs.has(packageName)) {
await this.loadTypesForPackage(packageName);
}
}
}
/**
* Scan node_modules for packages and load types for any not yet loaded.
* Called when node_modules changes (e.g., after pnpm install).
*/
public async scanAndLoadNewPackageTypes(): Promise<void> {
if (!this.executionEnvironment) return;
// Clear not-found cache so newly installed packages can be detected
this.notFoundPackages.clear();
try {
// Check if node_modules exists
if (!await this.executionEnvironment.exists('/node_modules')) return;
// Read top-level node_modules
const entries = await this.executionEnvironment.readDir('/node_modules');
for (const entry of entries) {
if (entry.type !== 'directory') continue;
if (entry.name.startsWith('@')) {
// Scoped package - read subdirectories
try {
const scopedPath = `/node_modules/${entry.name}`;
const scopedEntries = await this.executionEnvironment.readDir(scopedPath);
for (const scopedEntry of scopedEntries) {
if (scopedEntry.type === 'directory') {
const packageName = `${entry.name}/${scopedEntry.name}`;
await this.loadTypesForPackage(packageName);
}
}
} catch {
// Skip if we can't read scoped directory
}
} else if (!entry.name.startsWith('.')) {
// Regular package
await this.loadTypesForPackage(entry.name);
}
}
} catch (error) {
console.warn('Failed to scan node_modules:', error);
}
}
/**
* 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 {
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 existingModel = this.monacoInstance.editor.getModel(uri);
if (existingModel) {
existingModel.setValue(content);
} else {
const language = this.getLanguageFromPath(path);
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 {
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';
}
}
}