yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
assembly_editor.cc
Go to the documentation of this file.
1#include "assembly_editor.h"
2
3#include <algorithm>
4#include <array>
5#include <filesystem>
6#include <fstream>
7#include <optional>
8#include <string>
9#include <vector>
10
11#if defined(__APPLE__)
12#include <TargetConditionals.h>
13#endif
14
15#include <cctype>
16
17#include "absl/strings/ascii.h"
18#include "absl/strings/match.h"
19#include "absl/strings/str_cat.h"
23#include "app/gui/core/icons.h"
28#include "core/project.h"
30#include "util/file_util.h"
31
32namespace yaze::editor {
33
34using util::FileDialogWrapper;
35
36namespace {
37
38static const char* const kKeywords[] = {
39 "ADC", "AND", "ASL", "BCC", "BCS", "BEQ", "BIT", "BMI", "BNE", "BPL",
40 "BRA", "BRL", "BVC", "BVS", "CLC", "CLD", "CLI", "CLV", "CMP", "CPX",
41 "CPY", "DEC", "DEX", "DEY", "EOR", "INC", "INX", "INY", "JMP", "JSR",
42 "JSL", "LDA", "LDX", "LDY", "LSR", "MVN", "NOP", "ORA", "PEA", "PER",
43 "PHA", "PHB", "PHD", "PHP", "PHX", "PHY", "PLA", "PLB", "PLD", "PLP",
44 "PLX", "PLY", "REP", "ROL", "ROR", "RTI", "RTL", "RTS", "SBC", "SEC",
45 "SEI", "SEP", "STA", "STP", "STX", "STY", "STZ", "TAX", "TAY", "TCD",
46 "TCS", "TDC", "TRB", "TSB", "TSC", "TSX", "TXA", "TXS", "TXY", "TYA",
47 "TYX", "WAI", "WDM", "XBA", "XCE", "ORG", "LOROM", "HIROM"};
48
49static const char* const kIdentifiers[] = {
50 "abort", "abs", "acos", "asin", "atan", "atexit",
51 "atof", "atoi", "atol", "ceil", "clock", "cosh",
52 "ctime", "div", "exit", "fabs", "floor", "fmod",
53 "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph",
54 "ispunct", "isspace", "isupper", "kbhit", "log10", "log2",
55 "log", "memcmp", "modf", "pow", "putchar", "putenv",
56 "puts", "rand", "remove", "rename", "sinh", "sqrt",
57 "srand", "strcat", "strcmp", "strerror", "time", "tolower",
58 "toupper"};
59
61 TextEditor::LanguageDefinition language_65816;
62 for (auto& k : kKeywords)
63 language_65816.mKeywords.emplace(k);
64
65 for (auto& k : kIdentifiers) {
67 id.mDeclaration = "Built-in function";
68 language_65816.mIdentifiers.insert(std::make_pair(std::string(k), id));
69 }
70
71 language_65816.mTokenRegexStrings.push_back(
72 std::make_pair<std::string, TextEditor::PaletteIndex>(
73 "[ \\t]*#[ \\t]*[a-zA-Z_]+", TextEditor::PaletteIndex::Preprocessor));
74 language_65816.mTokenRegexStrings.push_back(
75 std::make_pair<std::string, TextEditor::PaletteIndex>(
76 "L?\\\"(\\\\.|[^\\\"])*\\\"", TextEditor::PaletteIndex::String));
77 language_65816.mTokenRegexStrings.push_back(
78 std::make_pair<std::string, TextEditor::PaletteIndex>(
79 "\\'\\\\?[^\\']\\'", TextEditor::PaletteIndex::CharLiteral));
80 language_65816.mTokenRegexStrings.push_back(
81 std::make_pair<std::string, TextEditor::PaletteIndex>(
82 "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?",
84 language_65816.mTokenRegexStrings.push_back(
85 std::make_pair<std::string, TextEditor::PaletteIndex>(
86 "[+-]?[0-9]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number));
87 language_65816.mTokenRegexStrings.push_back(
88 std::make_pair<std::string, TextEditor::PaletteIndex>(
89 "0[0-7]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number));
90 language_65816.mTokenRegexStrings.push_back(
91 std::make_pair<std::string, TextEditor::PaletteIndex>(
92 "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?",
94 language_65816.mTokenRegexStrings.push_back(
95 std::make_pair<std::string, TextEditor::PaletteIndex>(
96 "[a-zA-Z_][a-zA-Z0-9_]*", TextEditor::PaletteIndex::Identifier));
97 language_65816.mTokenRegexStrings.push_back(
98 std::make_pair<std::string, TextEditor::PaletteIndex>(
99 "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/"
100 "\\;\\,\\.]",
102
103 language_65816.mCommentStart = "/*";
104 language_65816.mCommentEnd = "*/";
105 language_65816.mSingleLineComment = ";";
106
107 language_65816.mCaseSensitive = false;
108 language_65816.mAutoIndentation = true;
109
110 language_65816.mName = "65816";
111
112 return language_65816;
113}
114
115bool HasFileExtension(const std::string& name) {
116 return absl::StrContains(name, '.');
117}
118
119bool IsHiddenName(const std::string& name) {
120 return !name.empty() && name[0] == '.';
121}
122
123bool IsIgnoredFile(const std::string& name,
124 const std::vector<std::string>& ignored_files) {
125 return std::ranges::find(ignored_files, name) != ignored_files.end();
126}
127
128bool ShouldSkipDirectory(const std::string& name) {
129 static const std::array<const char*, 11> kSkippedDirectories = {
130 ".git", ".context", ".idea", ".vscode",
131 "build", "build_ai", "build_agent", "build_test",
132 "node_modules", "dist", "out"};
133 for (const char* skipped : kSkippedDirectories) {
134 if (name == skipped) {
135 return true;
136 }
137 }
138 return false;
139}
140
142 if (!item) {
143 return;
144 }
145 std::sort(item->files.begin(), item->files.end());
146 std::sort(item->subfolders.begin(), item->subfolders.end(),
147 [](const FolderItem& lhs, const FolderItem& rhs) {
148 return lhs.name < rhs.name;
149 });
150 for (auto& subfolder : item->subfolders) {
151 SortFolderItem(&subfolder);
152 }
153}
154
155FolderItem LoadFolder(const std::string& folder) {
156 std::vector<std::string> ignored_files;
157#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
158 std::ifstream gitignore(folder + "/.gitignore");
159 if (gitignore.good()) {
160 std::string line;
161 while (std::getline(gitignore, line)) {
162 if (line.empty() || line[0] == '#' || line[0] == '!') {
163 continue;
164 }
165 ignored_files.push_back(line);
166 }
167 }
168#endif
169
170 FolderItem current_folder;
171
172 std::error_code path_ec;
173 std::filesystem::path root_path =
174 std::filesystem::weakly_canonical(folder, path_ec);
175 if (path_ec) {
176 path_ec.clear();
177 root_path = std::filesystem::absolute(folder, path_ec);
178 if (path_ec) {
179 root_path = folder;
180 }
181 }
182 current_folder.name = root_path.string();
183
184 std::error_code root_ec;
185 for (const auto& entry :
186 std::filesystem::directory_iterator(root_path, root_ec)) {
187 if (root_ec) {
188 break;
189 }
190
191 const std::string entry_name = entry.path().filename().string();
192 if (entry_name.empty() || IsHiddenName(entry_name)) {
193 continue;
194 }
195
196 std::error_code type_ec;
197 if (entry.is_regular_file(type_ec)) {
198 if (!HasFileExtension(entry_name) ||
199 IsIgnoredFile(entry_name, ignored_files)) {
200 continue;
201 }
202 current_folder.files.push_back(entry_name);
203 continue;
204 }
205
206 if (!entry.is_directory(type_ec) || ShouldSkipDirectory(entry_name)) {
207 continue;
208 }
209
210 FolderItem folder_item;
211 folder_item.name = entry_name;
212
213 std::error_code sub_ec;
214 for (const auto& sub_entry :
215 std::filesystem::directory_iterator(entry.path(), sub_ec)) {
216 if (sub_ec) {
217 break;
218 }
219
220 const std::string sub_name = sub_entry.path().filename().string();
221 if (sub_name.empty() || IsHiddenName(sub_name)) {
222 continue;
223 }
224
225 std::error_code sub_type_ec;
226 if (sub_entry.is_regular_file(sub_type_ec)) {
227 if (!HasFileExtension(sub_name) ||
228 IsIgnoredFile(sub_name, ignored_files)) {
229 continue;
230 }
231 folder_item.files.push_back(sub_name);
232 continue;
233 }
234
235 if (!sub_entry.is_directory(sub_type_ec) ||
236 ShouldSkipDirectory(sub_name)) {
237 continue;
238 }
239
240 FolderItem subfolder_item;
241 subfolder_item.name = sub_name;
242 std::error_code leaf_ec;
243 for (const auto& leaf_entry :
244 std::filesystem::directory_iterator(sub_entry.path(), leaf_ec)) {
245 if (leaf_ec) {
246 break;
247 }
248 const std::string leaf_name = leaf_entry.path().filename().string();
249 if (leaf_name.empty() || IsHiddenName(leaf_name)) {
250 continue;
251 }
252 std::error_code leaf_type_ec;
253 if (!leaf_entry.is_regular_file(leaf_type_ec)) {
254 continue;
255 }
256 if (!HasFileExtension(leaf_name) ||
257 IsIgnoredFile(leaf_name, ignored_files)) {
258 continue;
259 }
260 subfolder_item.files.push_back(leaf_name);
261 }
262 folder_item.subfolders.push_back(std::move(subfolder_item));
263 }
264
265 current_folder.subfolders.push_back(std::move(folder_item));
266 }
267
268 SortFolderItem(&current_folder);
269 return current_folder;
270}
271
272std::optional<AsmSymbolLocation> FindLabelInFile(
273 const std::filesystem::path& path, const std::string& label) {
274 std::ifstream file(path);
275 if (!file.is_open()) {
276 return std::nullopt;
277 }
278
279 std::string line;
280 int line_index = 0;
281 while (std::getline(file, line)) {
282 const size_t start = line.find_first_not_of(" \t");
283 if (start == std::string::npos) {
284 ++line_index;
285 continue;
286 }
287
288 if (line.compare(start, label.size(), label) != 0) {
289 ++line_index;
290 continue;
291 }
292
293 size_t pos = start + label.size();
294 while (pos < line.size() && (line[pos] == ' ' || line[pos] == '\t')) {
295 ++pos;
296 }
297
298 if (pos < line.size() && line[pos] == ':') {
300 loc.file = path.string();
301 loc.line = line_index;
302 loc.column = static_cast<int>(start);
303 return loc;
304 }
305
306 ++line_index;
307 }
308
309 return std::nullopt;
310}
311
312bool IsAssemblyLikeFile(const std::filesystem::path& path) {
313 const auto ext = path.extension().string();
314 return ext == ".asm" || ext == ".inc" || ext == ".s";
315}
316
318 std::string file_ref;
319 int line_one_based = 0;
320 int column_one_based = 1;
321};
322
324 std::string file_ref;
325 std::string symbol;
326};
327
328std::optional<int> ParsePositiveInt(const std::string& s) {
329 if (s.empty()) {
330 return std::nullopt;
331 }
332 for (char c : s) {
333 if (!std::isdigit(static_cast<unsigned char>(c))) {
334 return std::nullopt;
335 }
336 }
337 try {
338 const int v = std::stoi(s);
339 return v > 0 ? std::optional<int>(v) : std::nullopt;
340 } catch (...) {
341 return std::nullopt;
342 }
343}
344
345bool LooksLikeAssemblyPathRef(const std::string& file_ref) {
346 if (file_ref.empty()) {
347 return false;
348 }
349 const std::filesystem::path p(file_ref);
350 return IsAssemblyLikeFile(p);
351}
352
353std::optional<AsmFileLineRef> ParseAsmFileLineRef(
354 const std::string& reference) {
355 const std::string trimmed =
356 std::string(absl::StripAsciiWhitespace(reference));
357 if (trimmed.empty()) {
358 return std::nullopt;
359 }
360
361 // Format: "file.asm#L123"
362 if (const size_t pos = trimmed.find("#L"); pos != std::string::npos) {
363 const std::string file =
364 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, pos)));
365 const std::string line_str =
366 std::string(absl::StripAsciiWhitespace(trimmed.substr(pos + 2)));
367 if (!LooksLikeAssemblyPathRef(file)) {
368 return std::nullopt;
369 }
370 if (auto line = ParsePositiveInt(line_str); line.has_value()) {
371 return AsmFileLineRef{file, *line, /*column_one_based=*/1};
372 }
373 return std::nullopt;
374 }
375
376 // Formats:
377 // - "file.asm:123"
378 // - "file.asm:123:10"
379 const size_t last_colon = trimmed.rfind(':');
380 if (last_colon == std::string::npos) {
381 return std::nullopt;
382 }
383
384 const std::string tail =
385 std::string(absl::StripAsciiWhitespace(trimmed.substr(last_colon + 1)));
386 if (tail.empty()) {
387 return std::nullopt;
388 }
389
390 const size_t second_last_colon = (last_colon == 0)
391 ? std::string::npos
392 : trimmed.rfind(':', last_colon - 1);
393
394 if (second_last_colon != std::string::npos) {
395 const std::string file = std::string(
396 absl::StripAsciiWhitespace(trimmed.substr(0, second_last_colon)));
397 const std::string line_str =
398 std::string(absl::StripAsciiWhitespace(trimmed.substr(
399 second_last_colon + 1, last_colon - second_last_colon - 1)));
400 const std::string col_str = tail;
401 if (!LooksLikeAssemblyPathRef(file)) {
402 return std::nullopt;
403 }
404 auto line = ParsePositiveInt(line_str);
405 auto col = ParsePositiveInt(col_str);
406 if (!line.has_value() || !col.has_value()) {
407 return std::nullopt;
408 }
409 return AsmFileLineRef{file, *line, *col};
410 }
411
412 const std::string file =
413 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, last_colon)));
414 if (!LooksLikeAssemblyPathRef(file)) {
415 return std::nullopt;
416 }
417 if (auto line = ParsePositiveInt(tail); line.has_value()) {
418 return AsmFileLineRef{file, *line, /*column_one_based=*/1};
419 }
420 return std::nullopt;
421}
422
423std::optional<AsmFileSymbolRef> ParseAsmFileSymbolRef(
424 const std::string& reference) {
425 const std::string trimmed =
426 std::string(absl::StripAsciiWhitespace(reference));
427 if (trimmed.empty()) {
428 return std::nullopt;
429 }
430
431 // Format: "file.asm#Label"
432 if (const size_t pos = trimmed.rfind('#'); pos != std::string::npos) {
433 const std::string file =
434 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, pos)));
435 const std::string sym =
436 std::string(absl::StripAsciiWhitespace(trimmed.substr(pos + 1)));
437 if (!LooksLikeAssemblyPathRef(file) || sym.empty()) {
438 return std::nullopt;
439 }
440 return AsmFileSymbolRef{file, sym};
441 }
442
443 // Format: "file.asm:Label"
444 const size_t last_colon = trimmed.rfind(':');
445 if (last_colon == std::string::npos) {
446 return std::nullopt;
447 }
448
449 const std::string file =
450 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, last_colon)));
451 const std::string sym =
452 std::string(absl::StripAsciiWhitespace(trimmed.substr(last_colon + 1)));
453 if (!LooksLikeAssemblyPathRef(file) || sym.empty()) {
454 return std::nullopt;
455 }
456
457 // Avoid interpreting file:line refs as file:symbol (line parser should run
458 // first, but keep this extra guard anyway).
459 if (ParsePositiveInt(sym).has_value()) {
460 return std::nullopt;
461 }
462
463 return AsmFileSymbolRef{file, sym};
464}
465
466std::optional<std::filesystem::path> FindAsmFileInFolder(
467 const std::filesystem::path& root, const std::string& file_ref) {
468 std::filesystem::path p(file_ref);
469 if (p.is_absolute()) {
470 std::error_code ec;
471 if (std::filesystem::exists(p, ec) &&
472 std::filesystem::is_regular_file(p, ec)) {
473 return p;
474 }
475 return std::nullopt;
476 }
477
478 // Try relative to root first (supports "dir/file.asm" paths).
479 {
480 const std::filesystem::path candidate = root / p;
481 std::error_code ec;
482 if (std::filesystem::exists(candidate, ec) &&
483 std::filesystem::is_regular_file(candidate, ec)) {
484 return candidate;
485 }
486 }
487
488 // Fallback: recursive search by suffix match.
489 const std::string want_suffix = p.generic_string();
490 const std::string want_name = p.filename().string();
491
492 std::error_code ec;
493 if (!std::filesystem::exists(root, ec)) {
494 return std::nullopt;
495 }
496
497 std::filesystem::recursive_directory_iterator it(
498 root, std::filesystem::directory_options::skip_permission_denied, ec);
499 const std::filesystem::recursive_directory_iterator end;
500 for (; it != end && !ec; it.increment(ec)) {
501 const auto& entry = *it;
502 if (entry.is_directory()) {
503 const auto name = entry.path().filename().string();
504 if (!name.empty() && name.front() == '.') {
505 it.disable_recursion_pending();
506 } else if (name == "build" || name == "build_ai" || name == "build-ios" ||
507 name == "build-ios-sim" || name == "build-wasm" ||
508 name == "node_modules") {
509 it.disable_recursion_pending();
510 }
511 continue;
512 }
513
514 if (!entry.is_regular_file()) {
515 continue;
516 }
517 if (!IsAssemblyLikeFile(entry.path())) {
518 continue;
519 }
520
521 const std::string cand = entry.path().generic_string();
522 if (!want_suffix.empty() && absl::EndsWith(cand, want_suffix)) {
523 return entry.path();
524 }
525 if (!want_name.empty() && entry.path().filename() == want_name) {
526 return entry.path();
527 }
528 }
529
530 return std::nullopt;
531}
532
533std::optional<AsmSymbolLocation> FindLabelInFolder(
534 const std::filesystem::path& root, const std::string& label) {
535 std::error_code ec;
536 if (!std::filesystem::exists(root, ec)) {
537 return std::nullopt;
538 }
539
540 std::filesystem::recursive_directory_iterator it(
541 root, std::filesystem::directory_options::skip_permission_denied, ec);
542 const std::filesystem::recursive_directory_iterator end;
543 for (; it != end && !ec; it.increment(ec)) {
544 const auto& entry = *it;
545 if (entry.is_directory()) {
546 const auto name = entry.path().filename().string();
547 if (!name.empty() && name.front() == '.') {
548 it.disable_recursion_pending();
549 } else if (name == "build" || name == "build_ai" || name == "build-ios" ||
550 name == "build-ios-sim" || name == "build-wasm" ||
551 name == "node_modules") {
552 it.disable_recursion_pending();
553 }
554 continue;
555 }
556
557 if (!entry.is_regular_file()) {
558 continue;
559 }
560
561 if (!IsAssemblyLikeFile(entry.path())) {
562 continue;
563 }
564
565 if (auto loc = FindLabelInFile(entry.path(), label); loc.has_value()) {
566 return loc;
567 }
568 }
569
570 return std::nullopt;
571}
572
573} // namespace
574
577
578 // Register panels with PanelManager using EditorPanel instances
580 return;
581 auto* panel_manager = dependencies_.panel_manager;
582
583 // Register Code Editor panel - main text editing
584 panel_manager->RegisterEditorPanel(std::make_unique<AssemblyCodeEditorPanel>(
585 [this]() { DrawCodeEditor(); }));
586
587 // Register File Browser panel - project file navigation
588 panel_manager->RegisterEditorPanel(std::make_unique<AssemblyFileBrowserPanel>(
589 [this]() { DrawFileBrowser(); }));
590
591 // Register Symbols panel - symbol table viewer
592 panel_manager->RegisterEditorPanel(std::make_unique<AssemblySymbolsPanel>(
593 [this]() { DrawSymbolsContent(); }));
594
595 // Register Build Output panel - errors/warnings
596 panel_manager->RegisterEditorPanel(std::make_unique<AssemblyBuildOutputPanel>(
597 [this]() { DrawBuildOutput(); }));
598
599 // Register Toolbar panel - quick actions
600 panel_manager->RegisterEditorPanel(std::make_unique<AssemblyToolbarPanel>(
601 [this]() { DrawToolbarContent(); }));
602}
603
604absl::Status AssemblyEditor::Load() {
605 // Assembly editor doesn't require ROM data - files are loaded independently
606 return absl::OkStatus();
607}
608
609absl::Status AssemblyEditor::JumpToSymbolDefinition(const std::string& symbol) {
610 if (symbol.empty()) {
611 return absl::InvalidArgumentError("Symbol is empty");
612 }
613
614 std::filesystem::path root;
618 } else if (!current_folder_.name.empty()) {
619 root = current_folder_.name;
620 } else {
621 return absl::FailedPreconditionError(
622 "No code folder loaded (open a folder or set project code_folder)");
623 }
624
625 const std::string root_string = root.string();
626 if (symbol_jump_root_ != root_string) {
627 symbol_jump_root_ = root_string;
628 symbol_jump_cache_.clear();
630 }
631
632 if (current_folder_.name.empty()) {
633 OpenFolder(root_string);
634 }
635
636 if (auto it = symbol_jump_cache_.find(symbol);
637 it != symbol_jump_cache_.end()) {
638 const auto& cached = it->second;
639 ChangeActiveFile(cached.file);
640 if (!HasActiveFile()) {
641 return absl::InternalError("Failed to open file for symbol: " + symbol);
642 }
643
644 auto* editor = GetActiveEditor();
645 if (!editor) {
646 return absl::InternalError("No active text editor");
647 }
648
649 editor->SetCursorPosition(
650 TextEditor::Coordinates(cached.line, cached.column));
651 editor->SelectWordUnderCursor();
652 return absl::OkStatus();
653 }
654
655 if (symbol_jump_negative_cache_.contains(symbol)) {
656 return absl::NotFoundError("Symbol not found: " + symbol);
657 }
658
659 const auto loc = FindLabelInFolder(root, symbol);
660 if (!loc.has_value()) {
661 symbol_jump_negative_cache_.insert(symbol);
662 return absl::NotFoundError("Symbol not found: " + symbol);
663 }
664
665 symbol_jump_cache_[symbol] = *loc;
666 symbol_jump_negative_cache_.erase(symbol);
667
668 ChangeActiveFile(loc->file);
669 if (!HasActiveFile()) {
670 return absl::InternalError("Failed to open file for symbol: " + symbol);
671 }
672
673 auto* editor = GetActiveEditor();
674 if (!editor) {
675 return absl::InternalError("No active text editor");
676 }
677
678 editor->SetCursorPosition(TextEditor::Coordinates(loc->line, loc->column));
679 editor->SelectWordUnderCursor();
680 return absl::OkStatus();
681}
682
683absl::Status AssemblyEditor::JumpToReference(const std::string& reference) {
684 if (reference.empty()) {
685 return absl::InvalidArgumentError("Reference is empty");
686 }
687
688 if (auto file_ref = ParseAsmFileLineRef(reference); file_ref.has_value()) {
689 std::filesystem::path root;
693 } else if (!current_folder_.name.empty()) {
694 root = current_folder_.name;
695 } else {
696 return absl::FailedPreconditionError(
697 "No code folder loaded (open a folder or set project code_folder)");
698 }
699
700 if (current_folder_.name.empty()) {
701 OpenFolder(root.string());
702 }
703
704 auto path_or = FindAsmFileInFolder(root, file_ref->file_ref);
705 if (!path_or.has_value()) {
706 return absl::NotFoundError("File not found: " + file_ref->file_ref);
707 }
708
709 ChangeActiveFile(path_or->string());
710 if (!HasActiveFile()) {
711 return absl::InternalError("Failed to open file: " + path_or->string());
712 }
713
714 const int line0 = std::max(0, file_ref->line_one_based - 1);
715 const int col0 = std::max(0, file_ref->column_one_based - 1);
716 auto* editor = GetActiveEditor();
717 if (!editor) {
718 return absl::InternalError("No active text editor");
719 }
720 editor->SetCursorPosition(TextEditor::Coordinates(line0, col0));
721 editor->SelectWordUnderCursor();
722 return absl::OkStatus();
723 }
724
725 if (auto file_ref = ParseAsmFileSymbolRef(reference); file_ref.has_value()) {
726 std::filesystem::path root;
730 } else if (!current_folder_.name.empty()) {
731 root = current_folder_.name;
732 } else {
733 return absl::FailedPreconditionError(
734 "No code folder loaded (open a folder or set project code_folder)");
735 }
736
737 if (current_folder_.name.empty()) {
738 OpenFolder(root.string());
739 }
740
741 auto path_or = FindAsmFileInFolder(root, file_ref->file_ref);
742 if (!path_or.has_value()) {
743 return absl::NotFoundError("File not found: " + file_ref->file_ref);
744 }
745
746 auto loc = FindLabelInFile(*path_or, file_ref->symbol);
747 if (!loc.has_value()) {
748 return absl::NotFoundError(absl::StrCat(
749 "Symbol not found in ", file_ref->file_ref, ": ", file_ref->symbol));
750 }
751
752 ChangeActiveFile(loc->file);
753 if (!HasActiveFile()) {
754 return absl::InternalError("Failed to open file: " + loc->file);
755 }
756
757 auto* editor = GetActiveEditor();
758 if (!editor) {
759 return absl::InternalError("No active text editor");
760 }
761 editor->SetCursorPosition(TextEditor::Coordinates(loc->line, loc->column));
762 editor->SelectWordUnderCursor();
763 return absl::OkStatus();
764 }
765
766 return JumpToSymbolDefinition(reference);
767}
768
770 if (!HasActiveFile()) {
771 return "";
772 }
773 return files_[active_file_id_];
774}
775
777 if (!HasActiveFile() ||
778 active_file_id_ >= static_cast<int>(open_files_.size())) {
780 }
781 return open_files_[active_file_id_].GetCursorPosition();
782}
783
790
792 if (HasActiveFile()) {
794 }
795 return &text_editor_;
796}
797
798void AssemblyEditor::OpenFolder(const std::string& folder_path) {
799 current_folder_ = LoadFolder(folder_path);
800 if (symbol_jump_root_ != folder_path) {
801 symbol_jump_root_ = folder_path;
803 }
804}
805
810
811// =============================================================================
812// Panel Content Drawing (EditorPanel System)
813// =============================================================================
814
816 TextEditor* editor = GetActiveEditor();
817 // Menu bar for file operations
818 if (ImGui::BeginMenuBar()) {
819 DrawFileMenu();
820 DrawEditMenu();
822 ImGui::EndMenuBar();
823 }
824
825 // Status line
826 auto cpos = editor->GetCursorPosition();
827 const char* file_label =
828 current_file_.empty() ? "No file" : current_file_.c_str();
829 ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1,
830 cpos.mColumn + 1, editor->GetTotalLines(),
831 editor->IsOverwrite() ? "Ovr" : "Ins",
832 editor->CanUndo() ? "*" : " ",
833 editor->GetLanguageDefinition().mName.c_str(), file_label);
834
835 // Main text editor
836 editor->Render("##asm_editor",
837 ImVec2(0, -ImGui::GetFrameHeightWithSpacing()));
838
839 // Draw open file tabs at bottom
841}
842
844 // Lazy load project folder if not already loaded
845 if (current_folder_.name.empty() && dependencies_.project &&
849 }
850
851 // Open folder button if no folder loaded
852 if (current_folder_.name.empty()) {
853 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open Folder",
854 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
856 }
857 ImGui::Spacing();
858 ImGui::TextDisabled("No folder opened");
859 return;
860 }
861
862 // Folder path display
863 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s",
864 current_folder_.name.c_str());
865 ImGui::Separator();
866
867 // File tree
869}
870
872 if (symbols_.empty()) {
873 ImGui::TextDisabled("No symbols loaded.");
874 ImGui::Spacing();
875 ImGui::TextWrapped(
876 "Apply a patch or load external symbols to populate this list.");
877 return;
878 }
879
880 // Search filter
881 static char filter[256] = "";
882 ImGui::SetNextItemWidth(-1);
883 ImGui::InputTextWithHint("##symbol_filter",
884 ICON_MD_SEARCH " Filter symbols...", filter,
885 sizeof(filter));
886 ImGui::Separator();
887
888 // Symbol list
889 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), false)) {
890 for (const auto& [name, symbol] : symbols_) {
891 // Apply filter
892 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
893 continue;
894 }
895
896 ImGui::PushID(name.c_str());
897 if (ImGui::Selectable(name.c_str())) {
898 // Could jump to symbol definition if line info is available
899 }
900 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
901 ImGui::TextDisabled("$%06X", symbol.address);
902 ImGui::PopID();
903 }
904 }
905 ImGui::EndChild();
906}
907
909 // Error/warning counts
910 ImGui::Text("Errors: %zu Warnings: %zu", last_errors_.size(),
911 last_warnings_.size());
912 ImGui::Separator();
913
914 // Build buttons
915 bool has_active_file = HasActiveFile();
916 bool has_rom = (rom_ && rom_->is_loaded());
917
918 if (ImGui::Button(ICON_MD_CHECK_CIRCLE " Validate", ImVec2(120, 0))) {
919 if (has_active_file) {
920 auto status = ValidateCurrentFile();
921 if (status.ok() && dependencies_.toast_manager) {
922 dependencies_.toast_manager->Show("Validation passed!",
924 }
925 }
926 }
927 ImGui::SameLine();
928 bool apply_disabled = !has_rom || !has_active_file;
929 ImGui::BeginDisabled(apply_disabled);
930 if (ImGui::Button(ICON_MD_BUILD " Apply to ROM", ImVec2(140, 0))) {
931 auto status = ApplyPatchToRom();
932 if (status.ok() && dependencies_.toast_manager) {
934 }
935 }
936 ImGui::EndDisabled();
937 if (apply_disabled &&
938 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
939 if (!has_rom)
940 ImGui::SetTooltip("Load a ROM first");
941 else
942 ImGui::SetTooltip("Open an assembly file first");
943 }
944
945 ImGui::Separator();
946
947 // Output log
948 if (ImGui::BeginChild("##build_log", ImVec2(0, 0), true)) {
949 // Show errors in red
950 for (const auto& error : last_errors_) {
951 gui::StyleColorGuard err_guard(ImGuiCol_Text,
952 ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
953 ImGui::TextWrapped("%s %s", ICON_MD_ERROR, error.c_str());
954 }
955 // Show warnings in yellow
956 for (const auto& warning : last_warnings_) {
957 gui::StyleColorGuard warn_guard(ImGuiCol_Text,
958 ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
959 ImGui::TextWrapped("%s %s", ICON_MD_WARNING, warning.c_str());
960 }
961 if (last_errors_.empty() && last_warnings_.empty()) {
962 ImGui::TextDisabled("No build output");
963 }
964 }
965 ImGui::EndChild();
966}
967
969 float button_size = 32.0f;
970
971 if (ImGui::Button(ICON_MD_FOLDER_OPEN, ImVec2(button_size, button_size))) {
973 if (!folder.empty()) {
974 current_folder_ = LoadFolder(folder);
975 }
976 }
977 if (ImGui::IsItemHovered())
978 ImGui::SetTooltip("Open Folder");
979
980 ImGui::SameLine();
981 if (ImGui::Button(ICON_MD_FILE_OPEN, ImVec2(button_size, button_size))) {
983 if (!filename.empty()) {
984 ChangeActiveFile(filename);
985 }
986 }
987 if (ImGui::IsItemHovered())
988 ImGui::SetTooltip("Open File");
989
990 ImGui::SameLine();
991 bool can_save = HasActiveFile();
992 ImGui::BeginDisabled(!can_save);
993 if (ImGui::Button(ICON_MD_SAVE, ImVec2(button_size, button_size))) {
994 Save();
995 }
996 ImGui::EndDisabled();
997 if (ImGui::IsItemHovered())
998 ImGui::SetTooltip("Save File");
999
1000 ImGui::SameLine();
1001 ImGui::Text("|"); // Visual separator
1002 ImGui::SameLine();
1003
1004 // Build actions
1005 ImGui::BeginDisabled(!can_save);
1006 if (ImGui::Button(ICON_MD_CHECK_CIRCLE, ImVec2(button_size, button_size))) {
1008 }
1009 ImGui::EndDisabled();
1010 if (ImGui::IsItemHovered())
1011 ImGui::SetTooltip("Validate (Ctrl+B)");
1012
1013 ImGui::SameLine();
1014 bool can_apply = can_save && rom_ && rom_->is_loaded();
1015 ImGui::BeginDisabled(!can_apply);
1016 if (ImGui::Button(ICON_MD_BUILD, ImVec2(button_size, button_size))) {
1018 }
1019 ImGui::EndDisabled();
1020 if (ImGui::IsItemHovered())
1021 ImGui::SetTooltip("Apply to ROM (Ctrl+Shift+B)");
1022}
1023
1025 if (active_files_.empty()) {
1026 return;
1027 }
1028
1029 if (gui::BeginThemedTabBar("##OpenFileTabs",
1030 ImGuiTabBarFlags_Reorderable |
1031 ImGuiTabBarFlags_AutoSelectNewTabs |
1032 ImGuiTabBarFlags_FittingPolicyScroll)) {
1033 for (int i = 0; i < active_files_.Size; i++) {
1034 int file_id = active_files_[i];
1035 if (file_id >= files_.size()) {
1036 continue;
1037 }
1038
1039 // Extract just the filename from the path
1040 std::string filename = files_[file_id];
1041 size_t pos = filename.find_last_of("/\\");
1042 if (pos != std::string::npos) {
1043 filename = filename.substr(pos + 1);
1044 }
1045
1046 bool is_active = (active_file_id_ == file_id);
1047 ImGuiTabItemFlags flags = is_active ? ImGuiTabItemFlags_SetSelected : 0;
1048 bool tab_open = true;
1049
1050 if (ImGui::BeginTabItem(filename.c_str(), &tab_open, flags)) {
1051 // When tab is selected, update active file
1052 if (!is_active) {
1053 active_file_id_ = file_id;
1055 }
1056 ImGui::EndTabItem();
1057 }
1058
1059 // Handle tab close
1060 if (!tab_open) {
1061 active_files_.erase(active_files_.Data + i);
1062 if (active_file_id_ == file_id) {
1063 active_file_id_ = active_files_.empty() ? -1 : active_files_[0];
1064 if (active_file_id_ >= 0 &&
1065 active_file_id_ < static_cast<int>(open_files_.size())) {
1067 } else {
1068 current_file_.clear();
1069 }
1070 }
1071 i--;
1072 }
1073 }
1075 }
1076}
1077
1078// =============================================================================
1079// Legacy Update Methods (kept for backward compatibility)
1080// =============================================================================
1081
1083 if (!active_) {
1084 return absl::OkStatus();
1085 }
1086
1087 // Legacy window-based update - kept for backward compatibility
1088 // New code should use the panel system via DrawCodeEditor()
1089 ImGui::Begin("Assembly Editor", &active_, ImGuiWindowFlags_MenuBar);
1091 ImGui::End();
1092
1093 // Draw symbol panel as separate window if visible (legacy)
1095
1096 return absl::OkStatus();
1097}
1098
1100 TextEditor* editor = GetActiveEditor();
1101 auto cpos = editor->GetCursorPosition();
1102 const char* file_label =
1103 current_file_.empty() ? "No file" : current_file_.c_str();
1104 ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1,
1105 cpos.mColumn + 1, editor->GetTotalLines(),
1106 editor->IsOverwrite() ? "Ovr" : "Ins",
1107 editor->CanUndo() ? "*" : " ",
1108 editor->GetLanguageDefinition().mName.c_str(), file_label);
1109
1110 editor->Render("##asm_editor", ImVec2(0, 0));
1111}
1112
1114 // Deprecated: Use the EditorPanel system instead
1115 // This method is kept for backward compatibility during transition
1117 ImGui::Separator();
1119}
1120
1121absl::Status AssemblyEditor::Save() {
1122 if (!HasActiveFile()) {
1123 return absl::FailedPreconditionError("No active file to save.");
1124 }
1125
1126 const std::string& path = files_[active_file_id_];
1127 std::ofstream file(path);
1128 if (!file.is_open()) {
1129 return absl::InvalidArgumentError(
1130 absl::StrCat("Cannot write file: ", path));
1131 }
1132
1133 file << GetActiveEditor()->GetText();
1134 file.close();
1135 return absl::OkStatus();
1136}
1137
1139 static gui::Toolset toolbar;
1140 toolbar.Begin();
1141
1142 if (toolbar.AddAction(ICON_MD_FOLDER_OPEN, "Open Folder")) {
1144 }
1145 if (toolbar.AddAction(ICON_MD_SAVE, "Save File")) {
1146 Save();
1147 }
1148
1149 toolbar.End();
1150}
1151
1153 // Lazy load project folder if not already loaded
1154 if (current_folder_.name.empty() && dependencies_.project &&
1155 !dependencies_.project->code_folder.empty()) {
1158 }
1159
1160 if (ImGui::BeginChild("##current_folder", ImVec2(0, 0), true,
1161 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1162 if (ImGui::BeginTable("##file_table", 2,
1163 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1164 ImGuiTableFlags_Resizable |
1165 ImGuiTableFlags_Sortable)) {
1166 ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 256.0f);
1167 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch);
1168
1169 ImGui::TableHeadersRow();
1170
1171 for (const auto& file : current_folder_.files) {
1172 ImGui::TableNextRow();
1173 ImGui::TableNextColumn();
1174 if (ImGui::Selectable(file.c_str())) {
1175 ChangeActiveFile(absl::StrCat(current_folder_.name, "/", file));
1176 }
1177 ImGui::TableNextColumn();
1178 ImGui::Text("File");
1179 }
1180
1181 for (const auto& subfolder : current_folder_.subfolders) {
1182 ImGui::TableNextRow();
1183 ImGui::TableNextColumn();
1184 if (ImGui::TreeNode(subfolder.name.c_str())) {
1185 for (const auto& file : subfolder.files) {
1186 ImGui::TableNextRow();
1187 ImGui::TableNextColumn();
1188 if (ImGui::Selectable(file.c_str())) {
1189 ChangeActiveFile(absl::StrCat(current_folder_.name, "/",
1190 subfolder.name, "/", file));
1191 }
1192 ImGui::TableNextColumn();
1193 ImGui::Text("File");
1194 }
1195 ImGui::TreePop();
1196 } else {
1197 ImGui::TableNextColumn();
1198 ImGui::Text("Folder");
1199 }
1200 }
1201
1202 ImGui::EndTable();
1203 }
1204 }
1205 ImGui::EndChild();
1206}
1207
1209 if (ImGui::BeginMenu("File")) {
1210 if (ImGui::MenuItem(ICON_MD_FILE_OPEN " Open", "Ctrl+O")) {
1212 if (!filename.empty()) {
1213 ChangeActiveFile(filename);
1214 }
1215 }
1216 if (ImGui::MenuItem(ICON_MD_SAVE " Save", "Ctrl+S")) {
1217 Save();
1218 }
1219 ImGui::EndMenu();
1220 }
1221}
1222
1224 if (ImGui::BeginMenu("Edit")) {
1225 if (ImGui::MenuItem(ICON_MD_UNDO " Undo", "Ctrl+Z")) {
1226 GetActiveEditor()->Undo();
1227 }
1228 if (ImGui::MenuItem(ICON_MD_REDO " Redo", "Ctrl+Y")) {
1229 GetActiveEditor()->Redo();
1230 }
1231 ImGui::Separator();
1232 if (ImGui::MenuItem(ICON_MD_CONTENT_CUT " Cut", "Ctrl+X")) {
1233 GetActiveEditor()->Cut();
1234 }
1235 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy", "Ctrl+C")) {
1236 GetActiveEditor()->Copy();
1237 }
1238 if (ImGui::MenuItem(ICON_MD_CONTENT_PASTE " Paste", "Ctrl+V")) {
1240 }
1241 ImGui::Separator();
1242 if (ImGui::MenuItem(ICON_MD_SEARCH " Find", "Ctrl+F")) {
1243 // TODO: Implement this.
1244 }
1245 ImGui::EndMenu();
1246 }
1247}
1248
1249void AssemblyEditor::ChangeActiveFile(const std::string_view& filename) {
1250 if (filename.empty()) {
1251 return;
1252 }
1253
1254 // Check if file is already open
1255 for (int i = 0; i < active_files_.Size; ++i) {
1256 int file_id = active_files_[i];
1257 if (files_[file_id] == filename) {
1258 // Optional: Focus window
1259 active_file_id_ = file_id;
1261 return;
1262 }
1263 }
1264
1265 // Load file content using utility
1266 try {
1267 std::string content = util::LoadFile(std::string(filename));
1268 int new_file_id = files_.size();
1269 files_.push_back(std::string(filename));
1270 active_files_.push_back(new_file_id);
1271
1272 // Resize open_files_ if needed
1273 if (new_file_id >= open_files_.size()) {
1274 open_files_.resize(new_file_id + 1);
1275 }
1276
1277 open_files_[new_file_id].SetText(content);
1278 open_files_[new_file_id].SetLanguageDefinition(GetAssemblyLanguageDef());
1279 open_files_[new_file_id].SetPalette(TextEditor::GetDarkPalette());
1280 open_files_[new_file_id].SetShowWhitespaces(false);
1281 active_file_id_ = new_file_id;
1282 current_file_ = util::GetFileName(std::string(filename));
1283 } catch (const std::exception& ex) {
1284 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error opening file: %s\n",
1285 ex.what());
1286 }
1287}
1288
1289absl::Status AssemblyEditor::Cut() {
1290 GetActiveEditor()->Cut();
1291 return absl::OkStatus();
1292}
1293
1294absl::Status AssemblyEditor::Copy() {
1295 GetActiveEditor()->Copy();
1296 return absl::OkStatus();
1297}
1298
1301 return absl::OkStatus();
1302}
1303
1304absl::Status AssemblyEditor::Undo() {
1305 GetActiveEditor()->Undo();
1306 return absl::OkStatus();
1307}
1308
1309absl::Status AssemblyEditor::Redo() {
1310 GetActiveEditor()->Redo();
1311 return absl::OkStatus();
1312}
1313
1314// ============================================================================
1315// Asar Integration Implementation
1316// ============================================================================
1317
1319 if (!HasActiveFile()) {
1320 return absl::FailedPreconditionError("No file is currently active");
1321 }
1322
1323 // Initialize Asar if not already done
1324 if (!asar_initialized_) {
1325 auto status = asar_.Initialize();
1326 if (!status.ok()) {
1327 return status;
1328 }
1329 asar_initialized_ = true;
1330 }
1331
1332 // Get the file path
1333 const std::string& file_path = files_[active_file_id_];
1334
1335 // Validate the assembly
1336 auto status = asar_.ValidateAssembly(file_path);
1337
1338 // Update error markers based on result
1339 if (!status.ok()) {
1340 // Get the error messages and show them
1341 last_errors_.clear();
1342 last_errors_.push_back(std::string(status.message()));
1343 // Parse and update error markers
1345 // Asar errors typically contain line numbers we can parse
1346 for (const auto& error : last_errors_) {
1347 // Simple heuristic: look for "line X" or ":X:" pattern
1348 size_t line_pos = error.find(':');
1349 if (line_pos != std::string::npos) {
1350 size_t num_start = line_pos + 1;
1351 size_t num_end = error.find(':', num_start);
1352 if (num_end != std::string::npos) {
1353 std::string line_str = error.substr(num_start, num_end - num_start);
1354 try {
1355 int line = std::stoi(line_str);
1356 markers[line] = error;
1357 } catch (...) {
1358 // Not a line number, skip
1359 }
1360 }
1361 }
1362 }
1363 GetActiveEditor()->SetErrorMarkers(markers);
1364 return status;
1365 }
1366
1367 // Clear any previous error markers
1369 return absl::OkStatus();
1370}
1371
1373 if (!rom_ || !rom_->is_loaded()) {
1374 return absl::FailedPreconditionError("No ROM is loaded");
1375 }
1376
1377 if (!HasActiveFile()) {
1378 return absl::FailedPreconditionError("No file is currently active");
1379 }
1380
1381 // Initialize Asar if not already done
1382 if (!asar_initialized_) {
1383 auto status = asar_.Initialize();
1384 if (!status.ok()) {
1385 return status;
1386 }
1387 asar_initialized_ = true;
1388 }
1389
1390 // Get the file path
1391 const std::string& file_path = files_[active_file_id_];
1392
1393 // Get ROM data as vector for patching
1394 std::vector<uint8_t> rom_data = rom_->vector();
1395
1396 // Apply the patch
1397 auto result = asar_.ApplyPatch(file_path, rom_data);
1398
1399 if (!result.ok()) {
1400 UpdateErrorMarkers(*result);
1401 return result.status();
1402 }
1403
1404 if (result->success) {
1405 // Update the ROM with the patched data
1406 rom_->LoadFromData(rom_data);
1407
1408 // Store symbols for lookup
1410 last_errors_.clear();
1411 last_warnings_ = result->warnings;
1412
1413 // Clear error markers
1415
1416 return absl::OkStatus();
1417 } else {
1418 UpdateErrorMarkers(*result);
1419 return absl::InternalError("Patch application failed");
1420 }
1421}
1422
1424 last_errors_ = result.errors;
1425 last_warnings_ = result.warnings;
1426
1427 if (!HasActiveFile()) {
1428 return;
1429 }
1430
1432
1433 // Parse error messages to extract line numbers
1434 // Example Asar output: "asm/main.asm:42: error: Unknown command."
1435 for (const auto& error : result.errors) {
1436 try {
1437 // Simple parsing: look for first two colons numbers
1438 size_t first_colon = error.find(':');
1439 if (first_colon != std::string::npos) {
1440 size_t second_colon = error.find(':', first_colon + 1);
1441 if (second_colon != std::string::npos) {
1442 std::string line_str =
1443 error.substr(first_colon + 1, second_colon - (first_colon + 1));
1444 int line = std::stoi(line_str);
1445
1446 // Adjust for 1-based line numbers if necessary (ImGuiColorTextEdit usually uses 1-based in UI but 0-based internally? Or vice versa?)
1447 // Assuming standard compiler output 1-based, editor usually takes 1-based for markers key.
1448 markers[line] = error;
1449 }
1450 }
1451 } catch (...) {
1452 // Ignore parsing errors
1453 }
1454 }
1455
1456 GetActiveEditor()->SetErrorMarkers(markers);
1457}
1458
1460 last_errors_.clear();
1461
1462 if (!HasActiveFile()) {
1463 return;
1464 }
1465
1466 TextEditor::ErrorMarkers empty_markers;
1467 GetActiveEditor()->SetErrorMarkers(empty_markers);
1468}
1469
1471 if (ImGui::BeginMenu("Assemble")) {
1472 bool has_active_file = HasActiveFile();
1473 bool has_rom = (rom_ && rom_->is_loaded());
1474
1475 if (ImGui::MenuItem(ICON_MD_CHECK_CIRCLE " Validate", "Ctrl+B", false,
1476 has_active_file)) {
1477 auto status = ValidateCurrentFile();
1478 if (status.ok()) {
1479 // Show success notification (could add toast notification here)
1480 }
1481 }
1482
1483 if (ImGui::MenuItem(ICON_MD_BUILD " Apply to ROM", "Ctrl+Shift+B", false,
1484 has_active_file && has_rom)) {
1485 auto status = ApplyPatchToRom();
1486 if (status.ok()) {
1487 // Show success notification
1488 }
1489 }
1490
1491 if (ImGui::MenuItem(ICON_MD_FILE_UPLOAD " Load External Symbols", nullptr,
1492 false)) {
1493 if (dependencies_.project) {
1494 std::string sym_file = dependencies_.project->symbols_filename;
1495 if (!sym_file.empty()) {
1496 std::string abs_path =
1498 auto status = asar_.LoadSymbolsFromFile(abs_path);
1499 if (status.ok()) {
1500 // Copy symbols to local map for display
1504 "Successfully loaded external symbols from " + sym_file,
1506 }
1507 } else {
1510 "Failed to load symbols: " + std::string(status.message()),
1512 }
1513 }
1514 } else {
1517 "Project does not specify a symbols file.",
1519 }
1520 }
1521 }
1522 }
1523
1524 ImGui::Separator();
1525
1526 if (ImGui::MenuItem(ICON_MD_LIST " Show Symbols", nullptr,
1529 }
1530
1531 ImGui::Separator();
1532
1533 // Show last error/warning count
1534 ImGui::TextDisabled("Errors: %zu, Warnings: %zu", last_errors_.size(),
1535 last_warnings_.size());
1536
1537 ImGui::EndMenu();
1538 }
1539
1540 if (ImGui::BeginMenu("Version")) {
1541 bool has_version_manager = (dependencies_.version_manager != nullptr);
1542 if (ImGui::MenuItem(ICON_MD_CAMERA_ALT " Create Snapshot", nullptr, false,
1543 has_version_manager)) {
1544 if (has_version_manager) {
1545 ImGui::OpenPopup("Create Snapshot");
1546 }
1547 }
1548
1549 // Snapshot Dialog
1550 if (ImGui::BeginPopupModal("Create Snapshot", nullptr,
1551 ImGuiWindowFlags_AlwaysAutoResize)) {
1552 static char message[256] = "";
1553 ImGui::InputText("Message", message, sizeof(message));
1554
1555 if (ImGui::Button("Create", ImVec2(120, 0))) {
1556 auto result = dependencies_.version_manager->CreateSnapshot(message);
1557 if (result.ok() && result->success) {
1560 "Snapshot Created: " + result->commit_hash,
1562 }
1563 } else {
1565 std::string err = result.ok()
1566 ? result->message
1567 : std::string(result.status().message());
1568 dependencies_.toast_manager->Show("Snapshot Failed: " + err,
1570 }
1571 }
1572 ImGui::CloseCurrentPopup();
1573 message[0] = '\0'; // Reset
1574 }
1575 ImGui::SameLine();
1576 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
1577 ImGui::CloseCurrentPopup();
1578 }
1579 ImGui::EndPopup();
1580 }
1581
1582 ImGui::EndMenu();
1583 }
1584}
1585
1587 if (!show_symbol_panel_) {
1588 return;
1589 }
1590
1591 ImGui::SetNextWindowSize(ImVec2(350, 400), ImGuiCond_FirstUseEver);
1592 if (ImGui::Begin("Symbols", &show_symbol_panel_)) {
1593 if (symbols_.empty()) {
1594 ImGui::TextDisabled("No symbols loaded.");
1595 ImGui::TextDisabled("Apply a patch to load symbols.");
1596 } else {
1597 // Search filter
1598 static char filter[256] = "";
1599 ImGui::InputTextWithHint("##symbol_filter", "Filter symbols...", filter,
1600 sizeof(filter));
1601
1602 ImGui::Separator();
1603
1604 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), true)) {
1605 for (const auto& [name, symbol] : symbols_) {
1606 // Apply filter
1607 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
1608 continue;
1609 }
1610
1611 ImGui::PushID(name.c_str());
1612 if (ImGui::Selectable(name.c_str())) {
1613 // Could jump to symbol definition if line info is available
1614 // For now, just select it
1615 }
1616 ImGui::SameLine(200);
1617 ImGui::TextDisabled("$%06X", symbol.address);
1618 ImGui::PopID();
1619 }
1620 }
1621 ImGui::EndChild();
1622 }
1623 }
1624 ImGui::End();
1625}
1626
1627} // namespace yaze::editor
static const Palette & GetDarkPalette()
Coordinates GetCursorPosition() const
std::string GetText() const
int GetTotalLines() const
bool IsOverwrite() const
void Render(const char *aTitle, const ImVec2 &aSize=ImVec2(), bool aBorder=false)
void Undo(int aSteps=1)
std::map< int, std::string > ErrorMarkers
void SetErrorMarkers(const ErrorMarkers &aMarkers)
void Redo(int aSteps=1)
const LanguageDefinition & GetLanguageDefinition() const
bool CanUndo() const
void SetLanguageDefinition(const LanguageDefinition &aLanguageDef)
const auto & vector() const
Definition rom.h:143
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:255
bool is_loaded() const
Definition rom.h:132
absl::StatusOr< AsarPatchResult > ApplyPatch(const std::string &patch_path, std::vector< uint8_t > &rom_data, const std::vector< std::string > &include_paths={})
absl::Status ValidateAssembly(const std::string &asm_path)
absl::Status Initialize()
absl::Status LoadSymbolsFromFile(const std::string &symbol_path)
std::map< std::string, AsarSymbol > GetSymbolTable() const
absl::StatusOr< SnapshotResult > CreateSnapshot(const std::string &message)
TextEditor::Coordinates active_cursor_position() const
std::vector< std::string > last_warnings_
std::map< std::string, core::AsarSymbol > symbols_
absl::Status Save() override
std::vector< TextEditor > open_files_
absl::Status Load() override
void ChangeActiveFile(const std::string_view &filename)
absl::Status JumpToReference(const std::string &reference)
absl::Status Copy() override
absl::flat_hash_map< std::string, AsmSymbolLocation > symbol_jump_cache_
void UpdateErrorMarkers(const core::AsarPatchResult &result)
void OpenFolder(const std::string &folder_path)
absl::flat_hash_set< std::string > symbol_jump_negative_cache_
std::string active_file_path() const
absl::Status Paste() override
absl::Status Update() override
std::vector< std::string > files_
absl::Status Redo() override
absl::Status Undo() override
absl::Status Cut() override
std::vector< std::string > last_errors_
absl::Status JumpToSymbolDefinition(const std::string &symbol)
EditorDependencies dependencies_
Definition editor.h:306
void RegisterEditorPanel(std::unique_ptr< EditorPanel > panel)
Register an EditorPanel instance for central drawing.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
RAII guard for ImGui style colors.
Definition style_guard.h:27
Ultra-compact toolbar that merges mode buttons with settings.
bool AddAction(const char *icon, const char *tooltip)
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_CONTENT_CUT
Definition icons.h:466
#define ICON_MD_FILE_OPEN
Definition icons.h:747
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_CAMERA_ALT
Definition icons.h:355
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_UNDO
Definition icons.h:2039
std::optional< int > ParsePositiveInt(const std::string &s)
bool LooksLikeAssemblyPathRef(const std::string &file_ref)
std::optional< AsmFileSymbolRef > ParseAsmFileSymbolRef(const std::string &reference)
std::optional< AsmSymbolLocation > FindLabelInFolder(const std::filesystem::path &root, const std::string &label)
std::optional< AsmSymbolLocation > FindLabelInFile(const std::filesystem::path &path, const std::string &label)
std::optional< AsmFileLineRef > ParseAsmFileLineRef(const std::string &reference)
bool IsIgnoredFile(const std::string &name, const std::vector< std::string > &ignored_files)
FolderItem LoadFolder(const std::string &folder)
std::optional< std::filesystem::path > FindAsmFileInFolder(const std::filesystem::path &root, const std::string &file_ref)
bool IsAssemblyLikeFile(const std::filesystem::path &path)
Editors are the view controllers for the application.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
TextEditor::LanguageDefinition GetAssemblyLanguageDef()
Definition style.cc:193
std::string GetFileName(const std::string &filename)
Gets the filename from a full path.
Definition file_util.cc:19
std::string LoadFile(const std::string &filename)
Loads the entire contents of a file into a string.
Definition file_util.cc:23
static Coordinates Invalid()
Definition text_editor.h:69
TokenRegexStrings mTokenRegexStrings
Asar patch result information.
std::vector< std::string > errors
std::vector< std::string > warnings
project::YazeProject * project
Definition editor.h:167
core::VersionManager * version_manager
Definition editor.h:168
std::vector< FolderItem > subfolders
std::vector< std::string > files
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1287
std::string symbols_filename
Definition project.h:138