diff --git a/packages/editor-app/scripts/bundle-runtime.mjs b/packages/editor-app/scripts/bundle-runtime.mjs index 102cd625..604f8481 100644 --- a/packages/editor-app/scripts/bundle-runtime.mjs +++ b/packages/editor-app/scripts/bundle-runtime.mjs @@ -37,6 +37,25 @@ const filesToBundle = [ } ]; +// Type definition files for IDE intellisense +// 用于 IDE 智能感知的类型定义文件 +const typesDir = path.join(bundleDir, 'types'); +if (!fs.existsSync(typesDir)) { + fs.mkdirSync(typesDir, { recursive: true }); + console.log(`Created types directory: ${typesDir}`); +} + +const typeFilesToBundle = [ + { + src: path.join(rootPath, 'packages/core/dist/index.d.ts'), + dst: path.join(typesDir, 'ecs-framework.d.ts') + }, + { + src: path.join(rootPath, 'packages/engine-core/dist/index.d.ts'), + dst: path.join(typesDir, 'engine-core.d.ts') + } +]; + // Copy files let success = true; for (const { src, dst } of filesToBundle) { @@ -59,6 +78,24 @@ for (const { src, dst } of filesToBundle) { } } +// Copy type definition files (optional - don't fail if not found) +// 复制类型定义文件(可选 - 找不到不报错) +for (const { src, dst } of typeFilesToBundle) { + try { + if (!fs.existsSync(src)) { + console.warn(`Type definition not found: ${src}`); + console.log(' Build packages first: pnpm --filter @esengine/core build'); + continue; + } + + fs.copyFileSync(src, dst); + const stats = fs.statSync(dst); + console.log(`✓ Bundled type definition ${path.basename(dst)} (${(stats.size / 1024).toFixed(2)} KB)`); + } catch (error) { + console.warn(`Failed to bundle type definition ${path.basename(src)}: ${error.message}`); + } +} + // Update tauri.conf.json to include runtime directory if (success) { const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json'); diff --git a/packages/editor-app/src-tauri/src/commands/system.rs b/packages/editor-app/src-tauri/src/commands/system.rs index 62d64e7e..318f762b 100644 --- a/packages/editor-app/src-tauri/src/commands/system.rs +++ b/packages/editor-app/src-tauri/src/commands/system.rs @@ -142,6 +142,55 @@ pub fn get_temp_dir() -> Result { .ok_or_else(|| "Failed to get temp directory".to_string()) } +/// Open project folder with specified editor +/// 使用指定编辑器打开项目文件夹 +/// +/// @param project_path - Project folder path | 项目文件夹路径 +/// @param editor_command - Editor command (e.g., "code", "cursor") | 编辑器命令 +/// @param file_path - Optional file to open (will be opened in the editor) | 可选的要打开的文件 +#[tauri::command] +pub fn open_with_editor( + project_path: String, + editor_command: String, + file_path: Option, +) -> Result<(), String> { + use std::path::Path; + + // Normalize paths + let normalized_project = project_path.replace('/', "\\"); + let normalized_file = file_path.map(|f| f.replace('/', "\\")); + + // Verify project path exists + let project = Path::new(&normalized_project); + if !project.exists() { + return Err(format!("Project path does not exist: {}", normalized_project)); + } + + println!( + "[open_with_editor] editor: {}, project: {}, file: {:?}", + editor_command, normalized_project, normalized_file + ); + + let mut cmd = Command::new(&editor_command); + + // Add project folder as first argument + cmd.arg(&normalized_project); + + // If a specific file is provided, add it as a second argument + // Most editors support: editor + if let Some(ref file) = normalized_file { + let file_path = Path::new(file); + if file_path.exists() { + cmd.arg(file); + } + } + + cmd.spawn() + .map_err(|e| format!("Failed to open with editor '{}': {}", editor_command, e))?; + + Ok(()) +} + /// Get application resource directory #[tauri::command] pub fn get_app_resource_dir(app: AppHandle) -> Result { @@ -163,6 +212,97 @@ pub fn get_current_dir() -> Result { .map_err(|e| format!("Failed to get current directory: {}", e)) } +/// Copy type definitions to project for IDE intellisense +/// 复制类型定义文件到项目以支持 IDE 智能感知 +#[tauri::command] +pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let project = Path::new(&project_path); + if !project.exists() { + return Err(format!("Project path does not exist: {}", project_path)); + } + + // Create types directory in project + // 在项目中创建 types 目录 + let types_dir = project.join("types"); + if !types_dir.exists() { + fs::create_dir_all(&types_dir) + .map_err(|e| format!("Failed to create types directory: {}", e))?; + } + + // Get resource directory (where bundled files are) + // 获取资源目录(打包文件所在位置) + let resource_dir = app.path() + .resource_dir() + .map_err(|e| format!("Failed to get resource directory: {}", e))?; + + // Type definition files to copy + // 要复制的类型定义文件 + // Format: (resource_path, workspace_path, dest_name) + // 格式:(资源路径,工作区路径,目标文件名) + // Note: resource_path is relative to Tauri resource dir (runtime/ is mapped to .) + // 注意:resource_path 相对于 Tauri 资源目录(runtime/ 映射到 .) + let type_files = [ + ("types/ecs-framework.d.ts", "packages/core/dist/index.d.ts", "ecs-framework.d.ts"), + ("types/engine-core.d.ts", "packages/engine-core/dist/index.d.ts", "engine-core.d.ts"), + ]; + + // Try to find workspace root (for development mode) + // 尝试查找工作区根目录(用于开发模式) + let workspace_root = std::env::current_dir() + .ok() + .and_then(|cwd| { + // Look for pnpm-workspace.yaml or package.json in parent directories + // 在父目录中查找 pnpm-workspace.yaml 或 package.json + let mut dir = cwd.as_path(); + loop { + if dir.join("pnpm-workspace.yaml").exists() { + return Some(dir.to_path_buf()); + } + match dir.parent() { + Some(parent) => dir = parent, + None => return None, + } + } + }); + + let mut copied_count = 0; + for (resource_relative, workspace_relative, dest_name) in type_files { + let dest_path = types_dir.join(dest_name); + + // Try resource directory first (production mode) + // 首先尝试资源目录(生产模式) + let src_path = resource_dir.join(resource_relative); + if src_path.exists() { + fs::copy(&src_path, &dest_path) + .map_err(|e| format!("Failed to copy {}: {}", resource_relative, e))?; + println!("[copy_type_definitions] Copied {} to {}", src_path.display(), dest_path.display()); + copied_count += 1; + continue; + } + + // Try workspace directory (development mode) + // 尝试工作区目录(开发模式) + if let Some(ref ws_root) = workspace_root { + let ws_src_path = ws_root.join(workspace_relative); + if ws_src_path.exists() { + fs::copy(&ws_src_path, &dest_path) + .map_err(|e| format!("Failed to copy {}: {}", workspace_relative, e))?; + println!("[copy_type_definitions] Copied {} to {} (dev mode)", ws_src_path.display(), dest_path.display()); + copied_count += 1; + continue; + } + } + + println!("[copy_type_definitions] {} not found, skipping", dest_name); + } + + println!("[copy_type_definitions] Copied {} type definition files to {}", copied_count, types_dir.display()); + Ok(()) +} + /// Start a local HTTP server for runtime preview #[tauri::command] pub fn start_local_server(root_path: String, port: u16) -> Result { diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index db68af26..cac4365e 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -81,6 +81,8 @@ fn main() { commands::open_file_with_default_app, commands::show_in_folder, commands::get_temp_dir, + commands::open_with_editor, + commands::copy_type_definitions, commands::get_app_resource_dir, commands::get_current_dir, commands::start_local_server, diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index db306694..6537ece9 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -381,6 +381,14 @@ function App() { // 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件) await TauriAPI.setProjectBasePath(projectPath); + // 复制类型定义到项目,用于 IDE 智能感知 + // Copy type definitions to project for IDE intellisense + try { + await TauriAPI.copyTypeDefinitions(projectPath); + } catch (e) { + console.warn('[App] Failed to copy type definitions:', e); + } + const settings = SettingsService.getInstance(); settings.addRecentProject(projectPath); diff --git a/packages/editor-app/src/api/tauri.ts b/packages/editor-app/src/api/tauri.ts index 4ba68925..58dd03ff 100644 --- a/packages/editor-app/src/api/tauri.ts +++ b/packages/editor-app/src/api/tauri.ts @@ -168,6 +168,26 @@ export class TauriAPI { await invoke('show_in_folder', { filePath: path }); } + /** + * 使用指定编辑器打开项目 + * Open project with specified editor + * + * @param projectPath 项目文件夹路径 | Project folder path + * @param editorCommand 编辑器命令(如 "code", "cursor")| Editor command + * @param filePath 可选的要打开的文件路径 | Optional file path to open + */ + static async openWithEditor( + projectPath: string, + editorCommand: string, + filePath?: string + ): Promise { + await invoke('open_with_editor', { + projectPath, + editorCommand, + filePath: filePath || null + }); + } + /** * 打开行为树文件选择对话框 * @returns 用户选择的文件路径,取消则返回 null @@ -311,6 +331,16 @@ export class TauriAPI { return await invoke('generate_qrcode', { text }); } + /** + * 复制类型定义文件到项目 + * Copy type definition files to project for IDE intellisense + * + * @param projectPath 项目路径 | Project path + */ + static async copyTypeDefinitions(projectPath: string): Promise { + return await invoke('copy_type_definitions', { projectPath }); + } + /** * 将本地文件路径转换为 Tauri 可访问的 asset URL * @param filePath 本地文件路径 diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 57e3307d..c788ba01 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -40,6 +40,7 @@ import { import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; +import { SettingsService } from '../services/SettingsService'; import { ContextMenu, ContextMenuItem } from './ContextMenu'; import { PromptDialog } from './PromptDialog'; import '../styles/ContentBrowser.css'; @@ -210,8 +211,124 @@ export function ContentBrowser({ 'Shader': { en: 'Shader', zh: '着色器' }, 'Tilemap': { en: 'Tilemap', zh: '瓦片地图' }, 'Tileset': { en: 'Tileset', zh: '瓦片集' }, + 'Component': { en: 'Component', zh: '组件' }, + 'System': { en: 'System', zh: '系统' }, + 'TypeScript': { en: 'TypeScript', zh: 'TypeScript' }, }; + // 注册内置的 TypeScript 文件创建模板 + // Register built-in TypeScript file creation templates + useEffect(() => { + if (!fileActionRegistry) return; + + const builtinTemplates: FileCreationTemplate[] = [ + { + id: 'ts-component', + label: 'Component', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import { Component, ECSComponent, Property, Serialize, Serializable } from '@esengine/ecs-framework'; + +/** + * ${className} + */ +@ECSComponent('${className}') +@Serializable({ version: 1, typeId: '${className}' }) +export class ${className} extends Component { + // 在这里添加组件属性 + // Add component properties here + + @Serialize() + @Property({ type: 'number', label: 'Example Property' }) + public exampleProperty: number = 0; + + onInitialize(): void { + // 组件初始化时调用 + // Called when component is initialized + } + + onDestroy(): void { + // 组件销毁时调用 + // Called when component is destroyed + } +} +`; + } + }, + { + id: 'ts-system', + label: 'System', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'; + +/** + * ${className} + */ +export class ${className} extends EntitySystem { + // 定义系统处理的组件类型 + // Define component types this system processes + protected getMatcher(): Matcher { + // 返回匹配器,指定需要哪些组件 + // Return matcher specifying required components + // return Matcher.all(SomeComponent); + return Matcher.empty(); + } + + protected updateEntity(entity: Entity, deltaTime: number): void { + // 处理每个实体 + // Process each entity + } + + // 可选:系统初始化 + // Optional: System initialization + // onInitialize(): void { + // super.onInitialize(); + // } +} +`; + } + }, + { + id: 'ts-script', + label: 'TypeScript', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const name = fileName.replace(/\.ts$/, ''); + return `/** + * ${name} + */ + +export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void { + // 在这里编写代码 + // Write your code here +} +`; + } + } + ]; + + // 注册模板 + for (const template of builtinTemplates) { + fileActionRegistry.registerCreationTemplate(template); + } + + // 清理函数 + return () => { + for (const template of builtinTemplates) { + fileActionRegistry.unregisterCreationTemplate(template); + } + }; + }, [fileActionRegistry]); + const getTemplateLabel = (label: string): string => { const mapping = templateLabels[label]; if (mapping) { @@ -439,6 +556,24 @@ export function ContentBrowser({ return; } + // 脚本文件使用配置的编辑器打开 + // Open script files with configured editor + if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') { + const settings = SettingsService.getInstance(); + const editorCommand = settings.getScriptEditorCommand(); + + if (editorCommand && projectPath) { + try { + await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path); + return; + } catch (error) { + console.error('Failed to open with editor:', error); + // 如果失败,回退到系统默认应用 + // Fall back to system default app if failed + } + } + } + if (fileActionRegistry) { const handled = await fileActionRegistry.handleDoubleClick(asset.path); if (handled) return; @@ -450,7 +585,7 @@ export function ContentBrowser({ console.error('Failed to open file:', error); } } - }, [loadAssets, onOpenScene, fileActionRegistry]); + }, [loadAssets, onOpenScene, fileActionRegistry, projectPath]); // Handle context menu const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => { @@ -1127,10 +1262,16 @@ export function ContentBrowser({ )} {/* Create File Dialog */} - {createFileDialog && ( + {createFileDialog && (() => { + // 规范化扩展名(确保有点号前缀) + // Normalize extension (ensure dot prefix) + const ext = createFileDialog.template.extension.startsWith('.') + ? createFileDialog.template.extension + : `.${createFileDialog.template.extension}`; + return ( setCreateFileDialog(null)} /> - )} + ); + })()} ); } diff --git a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx index c949f8e7..a1f720d6 100644 --- a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx @@ -56,6 +56,32 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader { step: 1 } ] + }, + { + id: 'scriptEditor', + title: '脚本编辑器', + description: '配置用于打开脚本文件的外部编辑器', + settings: [ + { + key: 'editor.scriptEditor', + label: '脚本编辑器', + type: 'select', + defaultValue: 'system', + description: '双击脚本文件时使用的编辑器', + options: SettingsService.SCRIPT_EDITORS.map(editor => ({ + value: editor.id, + label: editor.name + })) + }, + { + key: 'editor.customScriptEditorCommand', + label: '自定义编辑器命令', + type: 'string', + defaultValue: '', + description: '当选择"自定义"时,填写编辑器的命令行命令(如 notepad++)', + placeholder: '例如:notepad++' + } + ] } ] }); diff --git a/packages/editor-app/src/services/SettingsService.ts b/packages/editor-app/src/services/SettingsService.ts index c4bba05b..b403a753 100644 --- a/packages/editor-app/src/services/SettingsService.ts +++ b/packages/editor-app/src/services/SettingsService.ts @@ -87,4 +87,64 @@ export class SettingsService { public clearRecentProjects(): void { this.set('recentProjects', []); } + + // ==================== Script Editor Settings ==================== + + /** + * 支持的脚本编辑器类型 + * Supported script editor types + */ + public static readonly SCRIPT_EDITORS = [ + { id: 'system', name: 'System Default', nameZh: '系统默认', command: '' }, + { id: 'vscode', name: 'Visual Studio Code', nameZh: 'Visual Studio Code', command: 'code' }, + { id: 'cursor', name: 'Cursor', nameZh: 'Cursor', command: 'cursor' }, + { id: 'webstorm', name: 'WebStorm', nameZh: 'WebStorm', command: 'webstorm' }, + { id: 'sublime', name: 'Sublime Text', nameZh: 'Sublime Text', command: 'subl' }, + { id: 'custom', name: 'Custom', nameZh: '自定义', command: '' } + ]; + + /** + * 获取脚本编辑器设置 + * Get script editor setting + */ + public getScriptEditor(): string { + return this.get('editor.scriptEditor', 'system'); + } + + /** + * 设置脚本编辑器 + * Set script editor + */ + public setScriptEditor(editorId: string): void { + this.set('editor.scriptEditor', editorId); + } + + /** + * 获取自定义脚本编辑器命令 + * Get custom script editor command + */ + public getCustomScriptEditorCommand(): string { + return this.get('editor.customScriptEditorCommand', ''); + } + + /** + * 设置自定义脚本编辑器命令 + * Set custom script editor command + */ + public setCustomScriptEditorCommand(command: string): void { + this.set('editor.customScriptEditorCommand', command); + } + + /** + * 获取当前脚本编辑器的命令 + * Get current script editor command + */ + public getScriptEditorCommand(): string { + const editorId = this.getScriptEditor(); + if (editorId === 'custom') { + return this.getCustomScriptEditorCommand(); + } + const editor = SettingsService.SCRIPT_EDITORS.find(e => e.id === editorId); + return editor?.command || ''; + } } diff --git a/packages/editor-core/src/Services/ProjectService.ts b/packages/editor-core/src/Services/ProjectService.ts index b585a604..d251c7e6 100644 --- a/packages/editor-core/src/Services/ProjectService.ts +++ b/packages/editor-core/src/Services/ProjectService.ts @@ -130,6 +130,40 @@ export class ProjectService implements IService { const assetsPath = `${projectPath}${sep}assets`; await this.fileAPI.createDirectory(assetsPath); + // Create types folder for type definitions + // 创建类型定义文件夹 + const typesPath = `${projectPath}${sep}types`; + await this.fileAPI.createDirectory(typesPath); + + // Create tsconfig.json for TypeScript support + // 创建 tsconfig.json 用于 TypeScript 支持 + const tsConfig = { + compilerOptions: { + target: 'ES2020', + module: 'ESNext', + moduleResolution: 'bundler', + lib: ['ES2020', 'DOM'], + strict: true, + esModuleInterop: true, + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + noEmit: true, + // Reference local type definitions + // 引用本地类型定义文件 + typeRoots: ['./types'], + paths: { + '@esengine/ecs-framework': ['./types/ecs-framework.d.ts'], + '@esengine/engine-core': ['./types/engine-core.d.ts'] + } + }, + include: ['scripts/**/*.ts'], + exclude: ['.esengine'] + }; + const tsConfigPath = `${projectPath}${sep}tsconfig.json`; + await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2)); + await this.messageHub.publish('project:created', { path: projectPath });