feat: Enhance project file management and agent settings
- Added support for loading and saving agent settings in the project format, including AI provider, model, and custom prompts. - Implemented RecentFilesManager for managing recent files with functionality to save and load recent file paths. - Introduced a new ProjectFileEditor for editing .yaze project files with syntax highlighting and validation features. - Enhanced the AgentChatWidget with file editing capabilities, allowing users to open, create, and save files directly within the interface. - Improved configuration directory handling and path expansion for better cross-platform compatibility.
This commit is contained in:
@@ -7,8 +7,10 @@
|
||||
#else // Linux and MacOS
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <cerrno>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#define YAZE_APP_CORE_PROJECT_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -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<std::string, std::string> 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<std::string>& 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<std::string> recent_files_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(open_files_.size()) - 1;
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
@@ -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<uint8_t> 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<FileEditorTab> open_files_;
|
||||
int active_file_tab_ = -1;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
|
||||
288
src/app/editor/code/project_file_editor.cc
Normal file
288
src/app/editor/code/project_file_editor.cc
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "app/editor/code/project_file_editor.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#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<std::string> 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
|
||||
95
src/app/editor/code/project_file_editor.h
Normal file
95
src/app/editor/code/project_file_editor.h
Normal file
@@ -0,0 +1,95 @@
|
||||
#ifndef YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_
|
||||
#define YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#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<std::string> validation_errors_;
|
||||
ToastManager* toast_manager_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_CODE_PROJECT_FILE_EDITOR_H_
|
||||
@@ -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<gui::MenuItem> 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);
|
||||
|
||||
Reference in New Issue
Block a user