diff --git a/src/app/core/platform/file_dialog.cc b/src/app/core/platform/file_dialog.cc index 9ecf07ab..7d2c968d 100644 --- a/src/app/core/platform/file_dialog.cc +++ b/src/app/core/platform/file_dialog.cc @@ -7,8 +7,10 @@ #else // Linux and MacOS #include #include +#include #endif +#include #include #include #include @@ -84,6 +86,70 @@ std::string GetResourcePath(const std::string &resource_path) { #endif } +std::string ExpandHomePath(const std::string& path) { + if (path.empty() || path[0] != '~') { + return path; + } + + const char* home = nullptr; +#ifdef _WIN32 + home = std::getenv("USERPROFILE"); + if (!home) { + home = std::getenv("HOMEDRIVE"); + const char* homePath = std::getenv("HOMEPATH"); + if (home && homePath) { + static std::string full_path; + full_path = std::string(home) + std::string(homePath); + home = full_path.c_str(); + } + } +#else + home = std::getenv("HOME"); +#endif + + if (!home) { + return path; // Fallback to original path if HOME not found + } + + // Replace ~ with home directory + if (path.size() == 1 || path[1] == '/') { + return std::string(home) + path.substr(1); + } + + return path; +} + +bool EnsureConfigDirectoryExists() { + std::string config_dir = GetConfigDirectory(); + +#ifdef _WIN32 + // Windows directory creation + DWORD attr = GetFileAttributesA(config_dir.c_str()); + if (attr == INVALID_FILE_ATTRIBUTES) { + // Directory doesn't exist, create it + if (!CreateDirectoryA(config_dir.c_str(), NULL)) { + DWORD error = GetLastError(); + if (error != ERROR_ALREADY_EXISTS) { + return false; + } + } + } +#else + // Unix-like directory creation + struct stat st; + if (stat(config_dir.c_str(), &st) != 0) { + // Directory doesn't exist, create it with 0755 permissions + if (mkdir(config_dir.c_str(), 0755) != 0) { + if (errno != EEXIST) { + return false; + } + } + } +#endif + + return true; +} + std::string GetConfigDirectory() { std::string config_directory = ".yaze"; Platform platform; @@ -113,7 +179,8 @@ std::string GetConfigDirectory() { default: break; } - return config_directory; + // Expand the home directory path + return ExpandHomePath(config_directory); } #ifdef _WIN32 diff --git a/src/app/core/platform/file_dialog.h b/src/app/core/platform/file_dialog.h index f1e79967..09d5d720 100644 --- a/src/app/core/platform/file_dialog.h +++ b/src/app/core/platform/file_dialog.h @@ -56,6 +56,8 @@ std::string GetFileName(const std::string &filename); std::string LoadFile(const std::string &filename); std::string LoadConfigFile(const std::string &filename); std::string GetConfigDirectory(); +bool EnsureConfigDirectoryExists(); +std::string ExpandHomePath(const std::string& path); std::string GetResourcePath(const std::string &resource_path); void SaveFile(const std::string &filename, const std::string &data); diff --git a/src/app/core/project.cc b/src/app/core/project.cc index 93e459d7..c611fe7e 100644 --- a/src/app/core/project.cc +++ b/src/app/core/project.cc @@ -226,6 +226,18 @@ absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) { else if (key == "saved_layouts") workspace_settings.saved_layouts = ParseStringList(value); else if (key == "recent_files") workspace_settings.recent_files = ParseStringList(value); } + else if (current_section == "agent_settings") { + if (key == "ai_provider") agent_settings.ai_provider = value; + else if (key == "ai_model") agent_settings.ai_model = value; + else if (key == "ollama_host") agent_settings.ollama_host = value; + else if (key == "gemini_api_key") agent_settings.gemini_api_key = value; + else if (key == "custom_system_prompt") agent_settings.custom_system_prompt = value; + else if (key == "use_custom_prompt") agent_settings.use_custom_prompt = ParseBool(value); + else if (key == "show_reasoning") agent_settings.show_reasoning = ParseBool(value); + else if (key == "verbose") agent_settings.verbose = ParseBool(value); + else if (key == "max_tool_iterations") agent_settings.max_tool_iterations = std::stoi(value); + else if (key == "max_retry_attempts") agent_settings.max_retry_attempts = std::stoi(value); + } else if (current_section == "build") { if (key == "build_script") build_script = value; else if (key == "output_folder") output_folder = value; @@ -319,6 +331,19 @@ absl::Status YazeProject::SaveToYazeFormat() { file << "saved_layouts=" << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n"; file << "recent_files=" << absl::StrJoin(workspace_settings.recent_files, ",") << "\n\n"; + // AI Agent settings section + file << "[agent_settings]\n"; + file << "ai_provider=" << agent_settings.ai_provider << "\n"; + file << "ai_model=" << agent_settings.ai_model << "\n"; + file << "ollama_host=" << agent_settings.ollama_host << "\n"; + file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n"; + file << "custom_system_prompt=" << GetRelativePath(agent_settings.custom_system_prompt) << "\n"; + file << "use_custom_prompt=" << (agent_settings.use_custom_prompt ? "true" : "false") << "\n"; + file << "show_reasoning=" << (agent_settings.show_reasoning ? "true" : "false") << "\n"; + file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n"; + file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n"; + file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n\n"; + // Custom keybindings section if (!workspace_settings.custom_keybindings.empty()) { file << "[keybindings]\n"; @@ -1024,5 +1049,46 @@ absl::Status YazeProject::SaveToJsonFormat() { #endif // YAZE_ENABLE_JSON_PROJECT_FORMAT +// RecentFilesManager implementation +std::string RecentFilesManager::GetFilePath() const { + return GetConfigDirectory() + "/" + kRecentFilesFilename; +} + +void RecentFilesManager::Save() { + // Ensure config directory exists + if (!EnsureConfigDirectoryExists()) { + std::cerr << "Warning: Could not create config directory for recent files\n"; + return; + } + + std::string filepath = GetFilePath(); + std::ofstream file(filepath); + if (!file.is_open()) { + std::cerr << "Warning: Could not save recent files to " << filepath << "\n"; + return; + } + + for (const auto& file_path : recent_files_) { + file << file_path << std::endl; + } +} + +void RecentFilesManager::Load() { + std::string filepath = GetFilePath(); + std::ifstream file(filepath); + if (!file.is_open()) { + // File doesn't exist yet, which is fine + return; + } + + recent_files_.clear(); + std::string line; + while (std::getline(file, line)) { + if (!line.empty()) { + recent_files_.push_back(line); + } + } +} + } // namespace core } // namespace yaze diff --git a/src/app/core/project.h b/src/app/core/project.h index 5948857f..ea06f653 100644 --- a/src/app/core/project.h +++ b/src/app/core/project.h @@ -2,8 +2,6 @@ #define YAZE_APP_CORE_PROJECT_H #include -#include -#include #include #include #include @@ -113,6 +111,20 @@ struct YazeProject { std::string git_repository; bool track_changes = true; + // AI Agent Settings + struct AgentSettings { + std::string ai_provider = "auto"; // auto, gemini, ollama, mock + std::string ai_model; // e.g., "gemini-2.0-flash-exp", "llama3:latest" + std::string ollama_host = "http://localhost:11434"; + std::string gemini_api_key; // Optional, can use env var + std::string custom_system_prompt; // Path to custom prompt (relative to project) + bool use_custom_prompt = false; + bool show_reasoning = true; + bool verbose = false; + int max_tool_iterations = 4; + int max_retry_attempts = 3; + } agent_settings; + // ZScream compatibility (for importing existing projects) std::string zscream_project_file; // Path to original .zsproj if importing std::map zscream_mappings; // Field mappings @@ -228,49 +240,50 @@ const std::string kRecentFilesFilename = "recent_files.txt"; class RecentFilesManager { public: - RecentFilesManager() : RecentFilesManager(kRecentFilesFilename) {} - RecentFilesManager(const std::string& filename) : filename_(filename) {} + // Singleton pattern - get the global instance + static RecentFilesManager& GetInstance() { + static RecentFilesManager instance; + return instance; + } + + // Delete copy constructor and assignment operator + RecentFilesManager(const RecentFilesManager&) = delete; + RecentFilesManager& operator=(const RecentFilesManager&) = delete; void AddFile(const std::string& file_path) { // Add a file to the list, avoiding duplicates + // Move to front if already exists (MRU - Most Recently Used) auto it = std::find(recent_files_.begin(), recent_files_.end(), file_path); - if (it == recent_files_.end()) { - recent_files_.push_back(file_path); + if (it != recent_files_.end()) { + recent_files_.erase(it); + } + recent_files_.insert(recent_files_.begin(), file_path); + + // Limit to 20 most recent files + if (recent_files_.size() > 20) { + recent_files_.resize(20); } } - void Save() { - std::ofstream file(filename_); - if (!file.is_open()) { - return; // Handle the error appropriately - } + void Save(); - for (const auto& file_path : recent_files_) { - file << file_path << std::endl; - } - } - - void Load() { - std::ifstream file(filename_); - if (!file.is_open()) { - return; - } - - recent_files_.clear(); - std::string line; - while (std::getline(file, line)) { - if (!line.empty()) { - recent_files_.push_back(line); - } - } - } + void Load(); const std::vector& GetRecentFiles() const { return recent_files_; } + void Clear() { + recent_files_.clear(); + } + private: - std::string filename_; + RecentFilesManager() { + Load(); // Load on construction + } + + std::string GetFilePath() const; + std::vector recent_files_; }; diff --git a/src/app/editor/agent/agent_chat_widget.cc b/src/app/editor/agent/agent_chat_widget.cc index 0dd1e636..f1c17434 100644 --- a/src/app/editor/agent/agent_chat_widget.cc +++ b/src/app/editor/agent/agent_chat_widget.cc @@ -654,6 +654,18 @@ void AgentChatWidget::Draw() { ImGui::EndTabItem(); } + // File Editor Tabs + if (ImGui::BeginTabItem(ICON_MD_EDIT_NOTE " Files")) { + RenderFileEditorTabs(); + ImGui::EndTabItem(); + } + + // System Prompt Editor Tab + if (ImGui::BeginTabItem(ICON_MD_SETTINGS_SUGGEST " System Prompt")) { + RenderSystemPromptEditor(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } @@ -1628,5 +1640,284 @@ void AgentChatWidget::HandleProposalReceived( } } +void AgentChatWidget::RenderSystemPromptEditor() { + ImGui::BeginChild("SystemPromptEditor", ImVec2(0, 0), false); + + // Toolbar + if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load Default")) { + // Load system_prompt_v2.txt + std::string prompt_path = core::GetConfigDirectory() + "/../assets/agent/system_prompt_v2.txt"; + std::ifstream file(prompt_path); + if (file.is_open()) { + // Find or create system prompt tab + bool found = false; + for (auto& tab : open_files_) { + if (tab.is_system_prompt) { + std::stringstream buffer; + buffer << file.rdbuf(); + tab.editor.SetText(buffer.str()); + tab.filepath = prompt_path; + found = true; + break; + } + } + + if (!found) { + FileEditorTab tab; + tab.filename = "system_prompt_v2.txt"; + tab.filepath = prompt_path; + tab.is_system_prompt = true; + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus()); + std::stringstream buffer; + buffer << file.rdbuf(); + tab.editor.SetText(buffer.str()); + open_files_.push_back(std::move(tab)); + active_file_tab_ = static_cast(open_files_.size()) - 1; + } + + if (toast_manager_) { + toast_manager_->Show("System prompt loaded", ToastType::kSuccess); + } + } else if (toast_manager_) { + toast_manager_->Show("Could not load system prompt file", ToastType::kError); + } + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_SAVE " Save")) { + // Save the current system prompt + for (auto& tab : open_files_) { + if (tab.is_system_prompt && !tab.filepath.empty()) { + std::ofstream file(tab.filepath); + if (file.is_open()) { + file << tab.editor.GetText(); + tab.modified = false; + if (toast_manager_) { + toast_manager_->Show("System prompt saved", ToastType::kSuccess); + } + } else if (toast_manager_) { + toast_manager_->Show("Failed to save system prompt", ToastType::kError); + } + break; + } + } + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_NOTE_ADD " Create New")) { + CreateNewFileInEditor("system_prompt_custom.txt"); + if (!open_files_.empty()) { + open_files_.back().is_system_prompt = true; + open_files_.back().editor.SetText("# Custom System Prompt\n\nEnter your custom system prompt here...\n"); + } + } + + ImGui::Separator(); + + // Find and render system prompt editor + bool found_prompt = false; + for (size_t i = 0; i < open_files_.size(); ++i) { + if (open_files_[i].is_system_prompt) { + found_prompt = true; + ImVec2 editor_size = ImVec2(0, ImGui::GetContentRegionAvail().y); + open_files_[i].editor.Render("##SystemPromptEditor", editor_size); + if (open_files_[i].editor.IsTextChanged()) { + open_files_[i].modified = true; + } + break; + } + } + + if (!found_prompt) { + ImGui::TextWrapped("No system prompt loaded. Click 'Load Default' to edit the system prompt."); + } + + ImGui::EndChild(); +} + +void AgentChatWidget::RenderFileEditorTabs() { + ImGui::BeginChild("FileEditorArea", ImVec2(0, 0), false); + + // Toolbar + if (ImGui::Button(ICON_MD_NOTE_ADD " New File")) { + ImGui::OpenPopup("NewFilePopup"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open File")) { + auto filepath = core::FileDialogWrapper::ShowOpenFileDialog(); + if (!filepath.empty()) { + OpenFileInEditor(filepath); + } + } + + // New file popup + static char new_filename_buffer[256] = {}; + if (ImGui::BeginPopup("NewFilePopup")) { + ImGui::Text("Create New File"); + ImGui::Separator(); + ImGui::InputText("Filename", new_filename_buffer, sizeof(new_filename_buffer)); + if (ImGui::Button("Create")) { + if (strlen(new_filename_buffer) > 0) { + CreateNewFileInEditor(new_filename_buffer); + memset(new_filename_buffer, 0, sizeof(new_filename_buffer)); + ImGui::CloseCurrentPopup(); + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::Separator(); + + // File tabs + if (!open_files_.empty()) { + if (ImGui::BeginTabBar("FileTabs", ImGuiTabBarFlags_Reorderable | + ImGuiTabBarFlags_FittingPolicyScroll)) { + for (size_t i = 0; i < open_files_.size(); ++i) { + if (open_files_[i].is_system_prompt) continue; // Skip system prompt in file tabs + + bool open = true; + std::string tab_label = open_files_[i].filename; + if (open_files_[i].modified) { + tab_label += " *"; + } + + if (ImGui::BeginTabItem(tab_label.c_str(), &open)) { + active_file_tab_ = static_cast(i); + + // File toolbar + if (ImGui::Button(ICON_MD_SAVE " Save")) { + if (!open_files_[i].filepath.empty()) { + std::ofstream file(open_files_[i].filepath); + if (file.is_open()) { + file << open_files_[i].editor.GetText(); + open_files_[i].modified = false; + if (toast_manager_) { + toast_manager_->Show("File saved", ToastType::kSuccess); + } + } else if (toast_manager_) { + toast_manager_->Show("Failed to save file", ToastType::kError); + } + } else { + auto save_path = core::FileDialogWrapper::ShowSaveFileDialog( + open_files_[i].filename, ""); + if (!save_path.empty()) { + std::ofstream file(save_path); + if (file.is_open()) { + file << open_files_[i].editor.GetText(); + open_files_[i].filepath = save_path; + open_files_[i].modified = false; + if (toast_manager_) { + toast_manager_->Show("File saved", ToastType::kSuccess); + } + } + } + } + } + + ImGui::SameLine(); + ImGui::TextDisabled("%s", open_files_[i].filepath.empty() ? + "(unsaved)" : open_files_[i].filepath.c_str()); + + ImGui::Separator(); + + // Editor + ImVec2 editor_size = ImVec2(0, ImGui::GetContentRegionAvail().y); + open_files_[i].editor.Render("##FileEditor", editor_size); + if (open_files_[i].editor.IsTextChanged()) { + open_files_[i].modified = true; + } + + ImGui::EndTabItem(); + } + + if (!open) { + // Tab was closed + open_files_.erase(open_files_.begin() + i); + if (active_file_tab_ >= static_cast(i)) { + active_file_tab_--; + } + break; + } + } + ImGui::EndTabBar(); + } + } else { + ImGui::TextWrapped("No files open. Create a new file or open an existing one."); + } + + ImGui::EndChild(); +} + +void AgentChatWidget::OpenFileInEditor(const std::string& filepath) { + // Check if file is already open + for (size_t i = 0; i < open_files_.size(); ++i) { + if (open_files_[i].filepath == filepath) { + active_file_tab_ = static_cast(i); + return; + } + } + + // Load the file + std::ifstream file(filepath); + if (!file.is_open()) { + if (toast_manager_) { + toast_manager_->Show("Could not open file", ToastType::kError); + } + return; + } + + FileEditorTab tab; + tab.filepath = filepath; + + // Extract filename from path + size_t last_slash = filepath.find_last_of("/\\"); + tab.filename = (last_slash != std::string::npos) ? + filepath.substr(last_slash + 1) : filepath; + + // Set language based on extension + std::string ext = core::GetFileExtension(filepath); + if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus()); + } else if (ext == "c") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::C()); + } else if (ext == "lua") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::Lua()); + } + + std::stringstream buffer; + buffer << file.rdbuf(); + tab.editor.SetText(buffer.str()); + + open_files_.push_back(std::move(tab)); + active_file_tab_ = static_cast(open_files_.size()) - 1; + + if (toast_manager_) { + toast_manager_->Show("File loaded", ToastType::kSuccess); + } +} + +void AgentChatWidget::CreateNewFileInEditor(const std::string& filename) { + FileEditorTab tab; + tab.filename = filename; + tab.modified = true; + + // Set language based on extension + std::string ext = core::GetFileExtension(filename); + if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus()); + } else if (ext == "c") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::C()); + } else if (ext == "lua") { + tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::Lua()); + } + + open_files_.push_back(std::move(tab)); + active_file_tab_ = static_cast(open_files_.size()) - 1; +} + } // namespace editor } // namespace yaze diff --git a/src/app/editor/agent/agent_chat_widget.h b/src/app/editor/agent/agent_chat_widget.h index 8cf3ad8a..ea1bd406 100644 --- a/src/app/editor/agent/agent_chat_widget.h +++ b/src/app/editor/agent/agent_chat_widget.h @@ -10,6 +10,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/time/time.h" +#include "app/gui/modules/text_editor.h" #include "cli/service/agent/conversational_agent_service.h" namespace yaze { @@ -179,6 +180,10 @@ public: // Collaboration history management (public so EditorManager can call them) void SwitchToSharedHistory(const std::string& session_id); void SwitchToLocalHistory(); + + // File editing + void OpenFileInEditor(const std::string& filepath); + void CreateNewFileInEditor(const std::string& filename); private: void EnsureHistoryLoaded(); @@ -201,6 +206,8 @@ public: void RenderRomSyncPanel(); void RenderSnapshotPreviewPanel(); void RenderProposalManagerPanel(); + void RenderSystemPromptEditor(); + void RenderFileEditorTabs(); void RefreshCollaboration(); void ApplyCollaborationSession( const CollaborationCallbacks::SessionContext& context, @@ -252,12 +259,23 @@ public: size_t last_known_history_size_ = 0; // UI state - int active_tab_ = 0; // 0=Chat, 1=Config, 2=Commands, 3=Collab, 4=ROM Sync + int active_tab_ = 0; // 0=Chat, 1=Config, 2=Commands, 3=Collab, 4=ROM Sync, 5=Files, 6=Prompt bool show_agent_config_ = false; bool show_z3ed_commands_ = false; bool show_rom_sync_ = false; bool show_snapshot_preview_ = false; std::vector snapshot_preview_data_; + + // File editing state + struct FileEditorTab { + std::string filepath; + std::string filename; + TextEditor editor; + bool modified = false; + bool is_system_prompt = false; + }; + std::vector open_files_; + int active_file_tab_ = -1; }; } // namespace editor diff --git a/src/app/editor/code/project_file_editor.cc b/src/app/editor/code/project_file_editor.cc new file mode 100644 index 00000000..e2b1c2d1 --- /dev/null +++ b/src/app/editor/code/project_file_editor.cc @@ -0,0 +1,288 @@ +#include "app/editor/code/project_file_editor.h" + +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "app/core/platform/file_dialog.h" +#include "app/editor/system/toast_manager.h" +#include "app/gui/icons.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +ProjectFileEditor::ProjectFileEditor() { + text_editor_.SetLanguageDefinition(TextEditor::LanguageDefinition::C()); + text_editor_.SetTabSize(2); + text_editor_.SetShowWhitespaces(false); +} + +void ProjectFileEditor::Draw() { + if (!active_) return; + + ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(absl::StrFormat("%s Project Editor###ProjectFileEditor", + ICON_MD_EDIT_DOCUMENT).c_str(), + &active_)) { + ImGui::End(); + return; + } + + // Toolbar + if (ImGui::BeginTable("ProjectEditorToolbar", 8, ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat("%s New", ICON_MD_NOTE_ADD).c_str())) { + NewFile(); + } + + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat("%s Open", ICON_MD_FOLDER_OPEN).c_str())) { + auto file = core::FileDialogWrapper::ShowOpenFileDialog(); + if (!file.empty()) { + auto status = LoadFile(file); + if (!status.ok() && toast_manager_) { + toast_manager_->Show(std::string(status.message()), + ToastType::kError); + } + } + } + + ImGui::TableNextColumn(); + bool can_save = !filepath_.empty() && IsModified(); + if (!can_save) ImGui::BeginDisabled(); + if (ImGui::Button(absl::StrFormat("%s Save", ICON_MD_SAVE).c_str())) { + auto status = SaveFile(); + if (status.ok() && toast_manager_) { + toast_manager_->Show("Project file saved", ToastType::kSuccess); + } else if (!status.ok() && toast_manager_) { + toast_manager_->Show(std::string(status.message()), ToastType::kError); + } + } + if (!can_save) ImGui::EndDisabled(); + + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat("%s Save As", ICON_MD_SAVE_AS).c_str())) { + auto file = core::FileDialogWrapper::ShowSaveFileDialog( + filepath_.empty() ? "project" : filepath_, "yaze"); + if (!file.empty()) { + auto status = SaveFileAs(file); + if (status.ok() && toast_manager_) { + toast_manager_->Show("Project file saved", ToastType::kSuccess); + } else if (!status.ok() && toast_manager_) { + toast_manager_->Show(std::string(status.message()), ToastType::kError); + } + } + } + + ImGui::TableNextColumn(); + ImGui::Text("|"); + + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat("%s Validate", ICON_MD_CHECK_CIRCLE).c_str())) { + ValidateContent(); + show_validation_ = true; + } + + ImGui::TableNextColumn(); + ImGui::Checkbox("Show Validation", &show_validation_); + + ImGui::TableNextColumn(); + if (!filepath_.empty()) { + ImGui::TextDisabled("%s", filepath_.c_str()); + } else { + ImGui::TextDisabled("No file loaded"); + } + + ImGui::EndTable(); + } + + ImGui::Separator(); + + // Validation errors panel + if (show_validation_ && !validation_errors_.empty()) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.3f, 0.2f, 0.2f, 0.5f)); + if (ImGui::BeginChild("ValidationErrors", ImVec2(0, 100), true)) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "%s Validation Errors:", ICON_MD_ERROR); + for (const auto& error : validation_errors_) { + ImGui::BulletText("%s", error.c_str()); + } + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + // Main editor + ImVec2 editor_size = ImGui::GetContentRegionAvail(); + text_editor_.Render("##ProjectEditor", editor_size); + + ImGui::End(); +} + +absl::Status ProjectFileEditor::LoadFile(const std::string& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrFormat("Cannot open file: %s", filepath)); + } + + std::stringstream buffer; + buffer << file.rdbuf(); + file.close(); + + text_editor_.SetText(buffer.str()); + filepath_ = filepath; + modified_ = false; + + ValidateContent(); + + return absl::OkStatus(); +} + +absl::Status ProjectFileEditor::SaveFile() { + if (filepath_.empty()) { + return absl::InvalidArgumentError("No file path specified"); + } + + return SaveFileAs(filepath_); +} + +absl::Status ProjectFileEditor::SaveFileAs(const std::string& filepath) { + // Ensure .yaze extension + std::string final_path = filepath; + if (!absl::EndsWith(final_path, ".yaze")) { + final_path += ".yaze"; + } + + std::ofstream file(final_path); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrFormat("Cannot create file: %s", final_path)); + } + + file << text_editor_.GetText(); + file.close(); + + filepath_ = final_path; + modified_ = false; + + // Add to recent files + auto& recent_mgr = core::RecentFilesManager::GetInstance(); + recent_mgr.AddFile(filepath_); + recent_mgr.Save(); + + return absl::OkStatus(); +} + +void ProjectFileEditor::NewFile() { + // Create a template project file + const char* template_content = R"(# yaze Project File +# Format Version: 2.0 + +[project] +name=New Project +description= +author= +license= +version=1.0 +created_date= +last_modified= +yaze_version=0.4.0 +tags= + +[files] +rom_filename= +rom_backup_folder=backups +code_folder=asm +assets_folder=assets +patches_folder=patches +labels_filename=labels.txt +symbols_filename=symbols.txt +output_folder=build +additional_roms= + +[feature_flags] +kLogInstructions=false +kSaveDungeonMaps=true +kSaveGraphicsSheet=true +kLoadCustomOverworld=false + +[workspace_settings] +font_global_scale=1.0 +autosave_enabled=true +autosave_interval_secs=300 +theme=dark +)"; + + text_editor_.SetText(template_content); + filepath_.clear(); + modified_ = true; + validation_errors_.clear(); +} + +void ProjectFileEditor::ApplySyntaxHighlighting() { + // TODO: Implement custom syntax highlighting for INI format + // For now, use C language definition which provides some basic highlighting +} + +void ProjectFileEditor::ValidateContent() { + validation_errors_.clear(); + + std::string content = text_editor_.GetText(); + std::vector lines = absl::StrSplit(content, '\n'); + + std::string current_section; + int line_num = 0; + + for (const auto& line : lines) { + line_num++; + std::string trimmed = absl::StripAsciiWhitespace(line); + + // Skip empty lines and comments + if (trimmed.empty() || trimmed[0] == '#') continue; + + // Check for section headers + if (trimmed[0] == '[' && trimmed[trimmed.size() - 1] == ']') { + current_section = trimmed.substr(1, trimmed.size() - 2); + + // Validate known sections + if (current_section != "project" && + current_section != "files" && + current_section != "feature_flags" && + current_section != "workspace_settings" && + current_section != "build_settings") { + validation_errors_.push_back( + absl::StrFormat("Line %d: Unknown section [%s]", + line_num, current_section)); + } + continue; + } + + // Check for key=value pairs + size_t equals_pos = trimmed.find('='); + if (equals_pos == std::string::npos) { + validation_errors_.push_back( + absl::StrFormat("Line %d: Invalid format, expected key=value", line_num)); + continue; + } + } + + if (validation_errors_.empty() && show_validation_ && toast_manager_) { + toast_manager_->Show("Project file validation passed", ToastType::kSuccess); + } +} + +void ProjectFileEditor::ShowValidationErrors() { + if (validation_errors_.empty()) return; + + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Validation Errors:"); + for (const auto& error : validation_errors_) { + ImGui::BulletText("%s", error.c_str()); + } +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/code/project_file_editor.h b/src/app/editor/code/project_file_editor.h new file mode 100644 index 00000000..21b53929 --- /dev/null +++ b/src/app/editor/code/project_file_editor.h @@ -0,0 +1,95 @@ +#ifndef YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_ +#define YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_ + +#include + +#include "absl/status/status.h" +#include "app/core/project.h" +#include "app/gui/modules/text_editor.h" + +namespace yaze { +namespace editor { + +class ToastManager; + +/** + * @class ProjectFileEditor + * @brief Editor for .yaze project files with syntax highlighting and validation + * + * Provides a rich text editing experience for yaze project files with: + * - Syntax highlighting for INI-style format + * - Real-time validation + * - Auto-save capability + * - Integration with core::YazeProject + */ +class ProjectFileEditor { + public: + ProjectFileEditor(); + + void Draw(); + + /** + * @brief Load a project file into the editor + */ + absl::Status LoadFile(const std::string& filepath); + + /** + * @brief Save the current editor contents to disk + */ + absl::Status SaveFile(); + + /** + * @brief Save to a new file path + */ + absl::Status SaveFileAs(const std::string& filepath); + + /** + * @brief Get whether the file has unsaved changes + */ + bool IsModified() const { return text_editor_.IsTextChanged() || modified_; } + + /** + * @brief Get the current filepath + */ + const std::string& filepath() const { return filepath_; } + + /** + * @brief Set whether the editor window is active + */ + void set_active(bool active) { active_ = active; } + + /** + * @brief Get pointer to active state for ImGui + */ + bool* active() { return &active_; } + + /** + * @brief Set toast manager for notifications + */ + void SetToastManager(ToastManager* toast_manager) { + toast_manager_ = toast_manager; + } + + /** + * @brief Create a new empty project file + */ + void NewFile(); + + private: + void ApplySyntaxHighlighting(); + void ValidateContent(); + void ShowValidationErrors(); + + TextEditor text_editor_; + std::string filepath_; + bool active_ = false; + bool modified_ = false; + bool show_validation_ = true; + std::vector validation_errors_; + ToastManager* toast_manager_ = nullptr; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_ diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 5d7771a5..a519a912 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -385,8 +385,7 @@ void EditorManager::Initialize(const std::string& filename) { context_.shortcut_manager.RegisterShortcut( "Load Last ROM", {ImGuiKey_R, ImGuiMod_Ctrl}, [this]() { - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); if (!manager.GetRecentFiles().empty()) { auto front = manager.GetRecentFiles().front(); status_ = OpenRomOrProject(front); @@ -428,8 +427,7 @@ void EditorManager::Initialize(const std::string& filename) { // Initialize menu items std::vector recent_files; - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); if (manager.GetRecentFiles().empty()) { recent_files.emplace_back("No Recent Files", "", nullptr); } else { @@ -1414,8 +1412,7 @@ void EditorManager::DrawMenuBar() { // Recent Files Tab if (ImGui::BeginTabItem( absl::StrFormat("%s Recent Files", ICON_MD_HISTORY).c_str())) { - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); auto recent_files = manager.GetRecentFiles(); if (ImGui::BeginTable("RecentFilesTable", 3, @@ -1823,8 +1820,7 @@ absl::Status EditorManager::LoadRom() { test::TestManager::Get().SetCurrentRom(current_rom_); #endif - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); manager.AddFile(file_name); manager.Save(); RETURN_IF_ERROR(LoadAssets()); @@ -1923,8 +1919,7 @@ absl::Status EditorManager::SaveRomAs(const std::string& filename) { } // Add to recent files - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); manager.AddFile(filename); manager.Save(); @@ -2041,8 +2036,7 @@ absl::Status EditorManager::OpenProject() { ImGui::GetIO().FontGlobalScale = font_global_scale_; // Add to recent files - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); manager.AddFile(current_project_.filepath); manager.Save(); @@ -2071,8 +2065,7 @@ absl::Status EditorManager::SaveProject() { autosave_interval_secs_; // Save recent files - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); current_project_.workspace_settings.recent_files.clear(); for (const auto& file : manager.GetRecentFiles()) { current_project_.workspace_settings.recent_files.push_back(file); @@ -2106,8 +2099,7 @@ absl::Status EditorManager::SaveProjectAs() { auto save_status = current_project_.Save(); if (save_status.ok()) { // Add to recent files - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); manager.AddFile(file_path); manager.Save(); @@ -3211,8 +3203,7 @@ void EditorManager::DrawWelcomeScreen() { // Recent files section (reuse homepage logic) ImGui::Text("Recent Files:"); ImGui::BeginChild("RecentFiles", ImVec2(0, 100), true); - static core::RecentFilesManager manager("recent_files.txt"); - manager.Load(); + auto& manager = core::RecentFilesManager::GetInstance(); for (const auto& file : manager.GetRecentFiles()) { if (gui::ClickableText(file.c_str())) { status_ = OpenRomOrProject(file);