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 <cstdlib>
6#include <filesystem>
7#include <fstream>
8#include <optional>
9#include <string>
10#include <vector>
11
12#if defined(__APPLE__)
13#include <TargetConditionals.h>
14#endif
15
16#include <cctype>
17
18#include "absl/strings/ascii.h"
19#include "absl/strings/match.h"
20#include "absl/strings/str_cat.h"
21#include "absl/strings/str_format.h"
22#include "absl/strings/str_join.h"
30#include "app/gui/core/icons.h"
36#include "core/project.h"
38#include "imgui/misc/cpp/imgui_stdlib.h"
39#include "rom/snes.h"
40#include "util/file_util.h"
41#include "util/json.h"
42
43namespace yaze::editor {
44
45using util::FileDialogWrapper;
46
47namespace {
48
49static const char* const kKeywords[] = {
50 "ADC", "AND", "ASL", "BCC", "BCS", "BEQ", "BIT", "BMI", "BNE", "BPL",
51 "BRA", "BRL", "BVC", "BVS", "CLC", "CLD", "CLI", "CLV", "CMP", "CPX",
52 "CPY", "DEC", "DEX", "DEY", "EOR", "INC", "INX", "INY", "JMP", "JSR",
53 "JSL", "LDA", "LDX", "LDY", "LSR", "MVN", "NOP", "ORA", "PEA", "PER",
54 "PHA", "PHB", "PHD", "PHP", "PHX", "PHY", "PLA", "PLB", "PLD", "PLP",
55 "PLX", "PLY", "REP", "ROL", "ROR", "RTI", "RTL", "RTS", "SBC", "SEC",
56 "SEI", "SEP", "STA", "STP", "STX", "STY", "STZ", "TAX", "TAY", "TCD",
57 "TCS", "TDC", "TRB", "TSB", "TSC", "TSX", "TXA", "TXS", "TXY", "TYA",
58 "TYX", "WAI", "WDM", "XBA", "XCE", "ORG", "LOROM", "HIROM"};
59
60static const char* const kIdentifiers[] = {
61 "abort", "abs", "acos", "asin", "atan", "atexit",
62 "atof", "atoi", "atol", "ceil", "clock", "cosh",
63 "ctime", "div", "exit", "fabs", "floor", "fmod",
64 "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph",
65 "ispunct", "isspace", "isupper", "kbhit", "log10", "log2",
66 "log", "memcmp", "modf", "pow", "putchar", "putenv",
67 "puts", "rand", "remove", "rename", "sinh", "sqrt",
68 "srand", "strcat", "strcmp", "strerror", "time", "tolower",
69 "toupper"};
70
72 TextEditor::LanguageDefinition language_65816;
73 for (auto& k : kKeywords)
74 language_65816.mKeywords.emplace(k);
75
76 for (auto& k : kIdentifiers) {
78 id.mDeclaration = "Built-in function";
79 language_65816.mIdentifiers.insert(std::make_pair(std::string(k), id));
80 }
81
82 language_65816.mTokenRegexStrings.push_back(
83 std::make_pair<std::string, TextEditor::PaletteIndex>(
84 "[ \\t]*#[ \\t]*[a-zA-Z_]+", TextEditor::PaletteIndex::Preprocessor));
85 language_65816.mTokenRegexStrings.push_back(
86 std::make_pair<std::string, TextEditor::PaletteIndex>(
87 "L?\\\"(\\\\.|[^\\\"])*\\\"", TextEditor::PaletteIndex::String));
88 language_65816.mTokenRegexStrings.push_back(
89 std::make_pair<std::string, TextEditor::PaletteIndex>(
90 "\\'\\\\?[^\\']\\'", TextEditor::PaletteIndex::CharLiteral));
91 language_65816.mTokenRegexStrings.push_back(
92 std::make_pair<std::string, TextEditor::PaletteIndex>(
93 "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?",
95 language_65816.mTokenRegexStrings.push_back(
96 std::make_pair<std::string, TextEditor::PaletteIndex>(
97 "[+-]?[0-9]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number));
98 language_65816.mTokenRegexStrings.push_back(
99 std::make_pair<std::string, TextEditor::PaletteIndex>(
100 "0[0-7]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number));
101 language_65816.mTokenRegexStrings.push_back(
102 std::make_pair<std::string, TextEditor::PaletteIndex>(
103 "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?",
105 language_65816.mTokenRegexStrings.push_back(
106 std::make_pair<std::string, TextEditor::PaletteIndex>(
107 "[a-zA-Z_][a-zA-Z0-9_]*", TextEditor::PaletteIndex::Identifier));
108 language_65816.mTokenRegexStrings.push_back(
109 std::make_pair<std::string, TextEditor::PaletteIndex>(
110 "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/"
111 "\\;\\,\\.]",
113
114 language_65816.mCommentStart = "/*";
115 language_65816.mCommentEnd = "*/";
116 language_65816.mSingleLineComment = ";";
117
118 language_65816.mCaseSensitive = false;
119 language_65816.mAutoIndentation = true;
120
121 language_65816.mName = "65816";
122
123 return language_65816;
124}
125
126bool HasFileExtension(const std::string& name) {
127 return absl::StrContains(name, '.');
128}
129
130bool IsHiddenName(const std::string& name) {
131 return !name.empty() && name[0] == '.';
132}
133
134bool IsIgnoredFile(const std::string& name,
135 const std::vector<std::string>& ignored_files) {
136 return std::ranges::find(ignored_files, name) != ignored_files.end();
137}
138
139bool ShouldSkipDirectory(const std::string& name) {
140 static const std::array<const char*, 13> kSkippedDirectories = {
141 ".git", ".context", ".idea", ".vscode", "build",
142 "build_ai", "build_agent", "build_test", "build-ios", "build-ios-sim",
143 "build-wasm", "node_modules", "dist"};
144 for (const char* skipped : kSkippedDirectories) {
145 if (name == skipped) {
146 return true;
147 }
148 }
149 if (name == "out") {
150 return true;
151 }
152 return false;
153}
154
155std::string ShellQuote(const std::string& value) {
156 std::string quoted = "'";
157 for (char ch : value) {
158 if (ch == '\'') {
159 quoted += "'\\''";
160 } else {
161 quoted.push_back(ch);
162 }
163 }
164 quoted += "'";
165 return quoted;
166}
167
168std::optional<std::filesystem::path> FindUpwardPath(
169 const std::filesystem::path& start, const std::filesystem::path& relative) {
170 std::error_code ec;
171 auto current = std::filesystem::absolute(start, ec);
172 if (ec) {
173 current = start;
174 }
175 while (!current.empty()) {
176 const auto candidate = current / relative;
177 if (std::filesystem::exists(candidate, ec) && !ec) {
178 return candidate;
179 }
180 if (current == current.root_path() || current == current.parent_path()) {
181 break;
182 }
183 current = current.parent_path();
184 }
185 return std::nullopt;
186}
187
188bool IsGeneratedBankFile(const std::filesystem::path& path) {
189 if (path.extension() != ".asm") {
190 return false;
191 }
192 const std::string name = path.filename().string();
193 return absl::StartsWith(name, "bank_");
194}
195
196bool LoadJsonFile(const std::string& path, Json* out) {
197 if (path.empty()) {
198 return false;
199 }
200 std::ifstream file(path);
201 if (!file.is_open()) {
202 return false;
203 }
204 try {
205 file >> *out;
206 return true;
207 } catch (...) {
208 return false;
209 }
210}
211
212int NormalizeLoRomBankIndex(uint32_t address) {
213 if ((address & 0xFF0000u) >= 0x800000u) {
214 address &= 0x7FFFFFu;
215 }
216 return static_cast<int>((address >> 16) & 0xFFu);
217}
218
219std::optional<int> ParseGeneratedBankIndex(const std::string& path) {
220 const std::string name = std::filesystem::path(path).filename().string();
221 if (!absl::StartsWith(name, "bank_") || !absl::EndsWith(name, ".asm")) {
222 return std::nullopt;
223 }
224 const std::string hex = name.substr(5, name.size() - 9);
225 try {
226 return std::stoi(hex, nullptr, 16);
227 } catch (...) {
228 return std::nullopt;
229 }
230}
231
232std::optional<uint32_t> ParseHexAddress(const std::string& text) {
233 if (text.empty()) {
234 return std::nullopt;
235 }
236
237 std::string token = text;
238 const size_t first = token.find_first_not_of(" \t");
239 if (first == std::string::npos) {
240 return std::nullopt;
241 }
242 const size_t last = token.find_last_not_of(" \t");
243 token = token.substr(first, last - first + 1);
244 if (token.empty()) {
245 return std::nullopt;
246 }
247 if (token[0] == '$') {
248 token = token.substr(1);
249 } else if (token.size() > 2 && token[0] == '0' &&
250 (token[1] == 'x' || token[1] == 'X')) {
251 token = token.substr(2);
252 }
253 try {
254 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
255 } catch (...) {
256 return std::nullopt;
257 }
258}
259
261 if (!item) {
262 return;
263 }
264 std::sort(item->files.begin(), item->files.end());
265 std::sort(item->subfolders.begin(), item->subfolders.end(),
266 [](const FolderItem& lhs, const FolderItem& rhs) {
267 return lhs.name < rhs.name;
268 });
269 for (auto& subfolder : item->subfolders) {
270 SortFolderItem(&subfolder);
271 }
272}
273
274FolderItem LoadFolder(const std::string& folder) {
275 std::vector<std::string> ignored_files;
276#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
277 std::ifstream gitignore(folder + "/.gitignore");
278 if (gitignore.good()) {
279 std::string line;
280 while (std::getline(gitignore, line)) {
281 if (line.empty() || line[0] == '#' || line[0] == '!') {
282 continue;
283 }
284 ignored_files.push_back(line);
285 }
286 }
287#endif
288
289 FolderItem current_folder;
290
291 std::error_code path_ec;
292 std::filesystem::path root_path =
293 std::filesystem::weakly_canonical(folder, path_ec);
294 if (path_ec) {
295 path_ec.clear();
296 root_path = std::filesystem::absolute(folder, path_ec);
297 if (path_ec) {
298 root_path = folder;
299 }
300 }
301 current_folder.name = root_path.string();
302
303 std::error_code root_ec;
304 for (const auto& entry :
305 std::filesystem::directory_iterator(root_path, root_ec)) {
306 if (root_ec) {
307 break;
308 }
309
310 const std::string entry_name = entry.path().filename().string();
311 if (entry_name.empty() || IsHiddenName(entry_name)) {
312 continue;
313 }
314
315 std::error_code type_ec;
316 if (entry.is_regular_file(type_ec)) {
317 if (!HasFileExtension(entry_name) ||
318 IsIgnoredFile(entry_name, ignored_files)) {
319 continue;
320 }
321 current_folder.files.push_back(entry_name);
322 continue;
323 }
324
325 if (!entry.is_directory(type_ec) || ShouldSkipDirectory(entry_name)) {
326 continue;
327 }
328
329 FolderItem folder_item;
330 folder_item.name = entry_name;
331
332 std::error_code sub_ec;
333 for (const auto& sub_entry :
334 std::filesystem::directory_iterator(entry.path(), sub_ec)) {
335 if (sub_ec) {
336 break;
337 }
338
339 const std::string sub_name = sub_entry.path().filename().string();
340 if (sub_name.empty() || IsHiddenName(sub_name)) {
341 continue;
342 }
343
344 std::error_code sub_type_ec;
345 if (sub_entry.is_regular_file(sub_type_ec)) {
346 if (!HasFileExtension(sub_name) ||
347 IsIgnoredFile(sub_name, ignored_files)) {
348 continue;
349 }
350 folder_item.files.push_back(sub_name);
351 continue;
352 }
353
354 if (!sub_entry.is_directory(sub_type_ec) ||
355 ShouldSkipDirectory(sub_name)) {
356 continue;
357 }
358
359 FolderItem subfolder_item;
360 subfolder_item.name = sub_name;
361 std::error_code leaf_ec;
362 for (const auto& leaf_entry :
363 std::filesystem::directory_iterator(sub_entry.path(), leaf_ec)) {
364 if (leaf_ec) {
365 break;
366 }
367 const std::string leaf_name = leaf_entry.path().filename().string();
368 if (leaf_name.empty() || IsHiddenName(leaf_name)) {
369 continue;
370 }
371 std::error_code leaf_type_ec;
372 if (!leaf_entry.is_regular_file(leaf_type_ec)) {
373 continue;
374 }
375 if (!HasFileExtension(leaf_name) ||
376 IsIgnoredFile(leaf_name, ignored_files)) {
377 continue;
378 }
379 subfolder_item.files.push_back(leaf_name);
380 }
381 folder_item.subfolders.push_back(std::move(subfolder_item));
382 }
383
384 current_folder.subfolders.push_back(std::move(folder_item));
385 }
386
387 SortFolderItem(&current_folder);
388 return current_folder;
389}
390
391std::optional<AsmSymbolLocation> FindLabelInFile(
392 const std::filesystem::path& path, const std::string& label) {
393 std::ifstream file(path);
394 if (!file.is_open()) {
395 return std::nullopt;
396 }
397
398 std::string line;
399 int line_index = 0;
400 while (std::getline(file, line)) {
401 const size_t start = line.find_first_not_of(" \t");
402 if (start == std::string::npos) {
403 ++line_index;
404 continue;
405 }
406
407 if (line.compare(start, label.size(), label) != 0) {
408 ++line_index;
409 continue;
410 }
411
412 size_t pos = start + label.size();
413 while (pos < line.size() && (line[pos] == ' ' || line[pos] == '\t')) {
414 ++pos;
415 }
416
417 if (pos < line.size() && line[pos] == ':') {
419 loc.file = path.string();
420 loc.line = line_index;
421 loc.column = static_cast<int>(start);
422 return loc;
423 }
424
425 ++line_index;
426 }
427
428 return std::nullopt;
429}
430
431bool IsAssemblyLikeFile(const std::filesystem::path& path) {
432 const auto ext = path.extension().string();
433 return ext == ".asm" || ext == ".inc" || ext == ".s";
434}
435
437 std::string file_ref;
438 int line_one_based = 0;
439 int column_one_based = 1;
440};
441
443 std::string file_ref;
444 std::string symbol;
445};
446
447std::optional<int> ParsePositiveInt(const std::string& s) {
448 if (s.empty()) {
449 return std::nullopt;
450 }
451 for (char c : s) {
452 if (!std::isdigit(static_cast<unsigned char>(c))) {
453 return std::nullopt;
454 }
455 }
456 try {
457 const int v = std::stoi(s);
458 return v > 0 ? std::optional<int>(v) : std::nullopt;
459 } catch (...) {
460 return std::nullopt;
461 }
462}
463
464bool LooksLikeAssemblyPathRef(const std::string& file_ref) {
465 if (file_ref.empty()) {
466 return false;
467 }
468 const std::filesystem::path p(file_ref);
469 return IsAssemblyLikeFile(p);
470}
471
472std::optional<AsmFileLineRef> ParseAsmFileLineRef(
473 const std::string& reference) {
474 const std::string trimmed =
475 std::string(absl::StripAsciiWhitespace(reference));
476 if (trimmed.empty()) {
477 return std::nullopt;
478 }
479
480 // Format: "file.asm#L123"
481 if (const size_t pos = trimmed.find("#L"); pos != std::string::npos) {
482 const std::string file =
483 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, pos)));
484 const std::string line_str =
485 std::string(absl::StripAsciiWhitespace(trimmed.substr(pos + 2)));
486 if (!LooksLikeAssemblyPathRef(file)) {
487 return std::nullopt;
488 }
489 if (auto line = ParsePositiveInt(line_str); line.has_value()) {
490 return AsmFileLineRef{file, *line, /*column_one_based=*/1};
491 }
492 return std::nullopt;
493 }
494
495 // Formats:
496 // - "file.asm:123"
497 // - "file.asm:123:10"
498 const size_t last_colon = trimmed.rfind(':');
499 if (last_colon == std::string::npos) {
500 return std::nullopt;
501 }
502
503 const std::string tail =
504 std::string(absl::StripAsciiWhitespace(trimmed.substr(last_colon + 1)));
505 if (tail.empty()) {
506 return std::nullopt;
507 }
508
509 const size_t second_last_colon = (last_colon == 0)
510 ? std::string::npos
511 : trimmed.rfind(':', last_colon - 1);
512
513 if (second_last_colon != std::string::npos) {
514 const std::string file = std::string(
515 absl::StripAsciiWhitespace(trimmed.substr(0, second_last_colon)));
516 const std::string line_str =
517 std::string(absl::StripAsciiWhitespace(trimmed.substr(
518 second_last_colon + 1, last_colon - second_last_colon - 1)));
519 const std::string col_str = tail;
520 if (!LooksLikeAssemblyPathRef(file)) {
521 return std::nullopt;
522 }
523 auto line = ParsePositiveInt(line_str);
524 auto col = ParsePositiveInt(col_str);
525 if (!line.has_value() || !col.has_value()) {
526 return std::nullopt;
527 }
528 return AsmFileLineRef{file, *line, *col};
529 }
530
531 const std::string file =
532 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, last_colon)));
533 if (!LooksLikeAssemblyPathRef(file)) {
534 return std::nullopt;
535 }
536 if (auto line = ParsePositiveInt(tail); line.has_value()) {
537 return AsmFileLineRef{file, *line, /*column_one_based=*/1};
538 }
539 return std::nullopt;
540}
541
542std::optional<AsmFileSymbolRef> ParseAsmFileSymbolRef(
543 const std::string& reference) {
544 const std::string trimmed =
545 std::string(absl::StripAsciiWhitespace(reference));
546 if (trimmed.empty()) {
547 return std::nullopt;
548 }
549
550 // Format: "file.asm#Label"
551 if (const size_t pos = trimmed.rfind('#'); pos != std::string::npos) {
552 const std::string file =
553 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, pos)));
554 const std::string sym =
555 std::string(absl::StripAsciiWhitespace(trimmed.substr(pos + 1)));
556 if (!LooksLikeAssemblyPathRef(file) || sym.empty()) {
557 return std::nullopt;
558 }
559 return AsmFileSymbolRef{file, sym};
560 }
561
562 // Format: "file.asm:Label"
563 const size_t last_colon = trimmed.rfind(':');
564 if (last_colon == std::string::npos) {
565 return std::nullopt;
566 }
567
568 const std::string file =
569 std::string(absl::StripAsciiWhitespace(trimmed.substr(0, last_colon)));
570 const std::string sym =
571 std::string(absl::StripAsciiWhitespace(trimmed.substr(last_colon + 1)));
572 if (!LooksLikeAssemblyPathRef(file) || sym.empty()) {
573 return std::nullopt;
574 }
575
576 // Avoid interpreting file:line refs as file:symbol (line parser should run
577 // first, but keep this extra guard anyway).
578 if (ParsePositiveInt(sym).has_value()) {
579 return std::nullopt;
580 }
581
582 return AsmFileSymbolRef{file, sym};
583}
584
585std::optional<std::filesystem::path> FindAsmFileInFolder(
586 const std::filesystem::path& root, const std::string& file_ref) {
587 std::filesystem::path p(file_ref);
588 if (p.is_absolute()) {
589 std::error_code ec;
590 if (std::filesystem::exists(p, ec) &&
591 std::filesystem::is_regular_file(p, ec)) {
592 return p;
593 }
594 return std::nullopt;
595 }
596
597 // Try relative to root first (supports "dir/file.asm" paths).
598 {
599 const std::filesystem::path candidate = root / p;
600 std::error_code ec;
601 if (std::filesystem::exists(candidate, ec) &&
602 std::filesystem::is_regular_file(candidate, ec)) {
603 return candidate;
604 }
605 }
606
607 // Fallback: recursive search by suffix match.
608 const std::string want_suffix = p.generic_string();
609 const std::string want_name = p.filename().string();
610
611 std::error_code ec;
612 if (!std::filesystem::exists(root, ec)) {
613 return std::nullopt;
614 }
615
616 std::filesystem::recursive_directory_iterator it(
617 root, std::filesystem::directory_options::skip_permission_denied, ec);
618 const std::filesystem::recursive_directory_iterator end;
619 for (; it != end && !ec; it.increment(ec)) {
620 const auto& entry = *it;
621 if (entry.is_directory()) {
622 const auto name = entry.path().filename().string();
623 if (!name.empty() && name.front() == '.') {
624 it.disable_recursion_pending();
625 } else if (ShouldSkipDirectory(name)) {
626 it.disable_recursion_pending();
627 }
628 continue;
629 }
630
631 if (!entry.is_regular_file()) {
632 continue;
633 }
634 if (!IsAssemblyLikeFile(entry.path())) {
635 continue;
636 }
637
638 const std::string cand = entry.path().generic_string();
639 if (!want_suffix.empty() && absl::EndsWith(cand, want_suffix)) {
640 return entry.path();
641 }
642 if (!want_name.empty() && entry.path().filename() == want_name) {
643 return entry.path();
644 }
645 }
646
647 return std::nullopt;
648}
649
650std::optional<AsmSymbolLocation> FindLabelInFolder(
651 const std::filesystem::path& root, const std::string& label) {
652 std::error_code ec;
653 if (!std::filesystem::exists(root, ec)) {
654 return std::nullopt;
655 }
656
657 std::filesystem::recursive_directory_iterator it(
658 root, std::filesystem::directory_options::skip_permission_denied, ec);
659 const std::filesystem::recursive_directory_iterator end;
660 for (; it != end && !ec; it.increment(ec)) {
661 const auto& entry = *it;
662 if (entry.is_directory()) {
663 const auto name = entry.path().filename().string();
664 if (!name.empty() && name.front() == '.') {
665 it.disable_recursion_pending();
666 } else if (ShouldSkipDirectory(name)) {
667 it.disable_recursion_pending();
668 }
669 continue;
670 }
671
672 if (!entry.is_regular_file()) {
673 continue;
674 }
675
676 if (!IsAssemblyLikeFile(entry.path())) {
677 continue;
678 }
679
680 if (auto loc = FindLabelInFile(entry.path(), label); loc.has_value()) {
681 return loc;
682 }
683 }
684
685 return std::nullopt;
686}
687
688} // namespace
689
692
693 // Register panels with WorkspaceWindowManager using WindowContent instances
695 return;
696 auto* window_manager = dependencies_.window_manager;
697
698 // Register Code Editor panel - main text editing
699 window_manager->RegisterWindowContent(
700 std::make_unique<AssemblyCodeEditorPanel>(
701 [this]() { DrawCodeEditor(); }));
702
703 // Register File Browser panel - project file navigation
704 window_manager->RegisterWindowContent(
705 std::make_unique<AssemblyFileBrowserPanel>(
706 [this]() { DrawFileBrowser(); }));
707
708 // Register Symbols panel - symbol table viewer
709 window_manager->RegisterWindowContent(std::make_unique<AssemblySymbolsPanel>(
710 [this]() { DrawSymbolsContent(); }));
711
712 // Register Build Output panel - errors/warnings
713 window_manager->RegisterWindowContent(
714 std::make_unique<AssemblyBuildOutputPanel>(
715 [this]() { DrawBuildOutput(); }));
716
717 window_manager->RegisterWindowContent(
718 std::make_unique<AssemblyDisassemblyPanel>(
719 [this]() { DrawDisassemblyContent(); }));
720
721 // Register Toolbar panel - quick actions
722 window_manager->RegisterWindowContent(std::make_unique<AssemblyToolbarPanel>(
723 [this]() { DrawToolbarContent(); }));
724}
725
726absl::Status AssemblyEditor::Load() {
727 // Assembly editor doesn't require ROM data - files are loaded independently
728 return absl::OkStatus();
729}
730
731absl::Status AssemblyEditor::JumpToSymbolDefinition(const std::string& symbol) {
732 if (symbol.empty()) {
733 return absl::InvalidArgumentError("Symbol is empty");
734 }
735
736 std::filesystem::path root;
740 } else if (!current_folder_.name.empty()) {
741 root = current_folder_.name;
742 } else {
743 return absl::FailedPreconditionError(
744 "No code folder loaded (open a folder or set project code_folder)");
745 }
746
747 const std::string root_string = root.string();
748 if (symbol_jump_root_ != root_string) {
749 symbol_jump_root_ = root_string;
750 symbol_jump_cache_.clear();
752 }
753
754 if (current_folder_.name.empty()) {
755 OpenFolder(root_string);
756 }
757
758 if (auto it = symbol_jump_cache_.find(symbol);
759 it != symbol_jump_cache_.end()) {
760 const auto& cached = it->second;
761 ChangeActiveFile(cached.file);
762 if (!HasActiveFile()) {
763 return absl::InternalError("Failed to open file for symbol: " + symbol);
764 }
765
766 auto* editor = GetActiveEditor();
767 if (!editor) {
768 return absl::InternalError("No active text editor");
769 }
770
771 editor->SetCursorPosition(
772 TextEditor::Coordinates(cached.line, cached.column));
773 editor->SelectWordUnderCursor();
774 return absl::OkStatus();
775 }
776
777 if (symbol_jump_negative_cache_.contains(symbol)) {
778 return absl::NotFoundError("Symbol not found: " + symbol);
779 }
780
781 const auto loc = FindLabelInFolder(root, symbol);
782 if (!loc.has_value()) {
783 symbol_jump_negative_cache_.insert(symbol);
784 return absl::NotFoundError("Symbol not found: " + symbol);
785 }
786
787 symbol_jump_cache_[symbol] = *loc;
788 symbol_jump_negative_cache_.erase(symbol);
789
790 ChangeActiveFile(loc->file);
791 if (!HasActiveFile()) {
792 return absl::InternalError("Failed to open file for symbol: " + symbol);
793 }
794
795 auto* editor = GetActiveEditor();
796 if (!editor) {
797 return absl::InternalError("No active text editor");
798 }
799
800 editor->SetCursorPosition(TextEditor::Coordinates(loc->line, loc->column));
801 editor->SelectWordUnderCursor();
802 return absl::OkStatus();
803}
804
805absl::Status AssemblyEditor::JumpToReference(const std::string& reference) {
806 if (reference.empty()) {
807 return absl::InvalidArgumentError("Reference is empty");
808 }
809
810 if (auto file_ref = ParseAsmFileLineRef(reference); file_ref.has_value()) {
811 std::filesystem::path root;
815 } else if (!current_folder_.name.empty()) {
816 root = current_folder_.name;
817 } else {
818 return absl::FailedPreconditionError(
819 "No code folder loaded (open a folder or set project code_folder)");
820 }
821
822 if (current_folder_.name.empty()) {
823 OpenFolder(root.string());
824 }
825
826 auto path_or = FindAsmFileInFolder(root, file_ref->file_ref);
827 if (!path_or.has_value()) {
828 return absl::NotFoundError("File not found: " + file_ref->file_ref);
829 }
830
831 ChangeActiveFile(path_or->string());
832 if (!HasActiveFile()) {
833 return absl::InternalError("Failed to open file: " + path_or->string());
834 }
835
836 const int line0 = std::max(0, file_ref->line_one_based - 1);
837 const int col0 = std::max(0, file_ref->column_one_based - 1);
838 auto* editor = GetActiveEditor();
839 if (!editor) {
840 return absl::InternalError("No active text editor");
841 }
842 editor->SetCursorPosition(TextEditor::Coordinates(line0, col0));
843 editor->SelectWordUnderCursor();
844 return absl::OkStatus();
845 }
846
847 if (auto file_ref = ParseAsmFileSymbolRef(reference); file_ref.has_value()) {
848 std::filesystem::path root;
852 } else if (!current_folder_.name.empty()) {
853 root = current_folder_.name;
854 } else {
855 return absl::FailedPreconditionError(
856 "No code folder loaded (open a folder or set project code_folder)");
857 }
858
859 if (current_folder_.name.empty()) {
860 OpenFolder(root.string());
861 }
862
863 auto path_or = FindAsmFileInFolder(root, file_ref->file_ref);
864 if (!path_or.has_value()) {
865 return absl::NotFoundError("File not found: " + file_ref->file_ref);
866 }
867
868 auto loc = FindLabelInFile(*path_or, file_ref->symbol);
869 if (!loc.has_value()) {
870 return absl::NotFoundError(absl::StrCat(
871 "Symbol not found in ", file_ref->file_ref, ": ", file_ref->symbol));
872 }
873
874 ChangeActiveFile(loc->file);
875 if (!HasActiveFile()) {
876 return absl::InternalError("Failed to open file: " + loc->file);
877 }
878
879 auto* editor = GetActiveEditor();
880 if (!editor) {
881 return absl::InternalError("No active text editor");
882 }
883 editor->SetCursorPosition(TextEditor::Coordinates(loc->line, loc->column));
884 editor->SelectWordUnderCursor();
885 return absl::OkStatus();
886 }
887
888 return JumpToSymbolDefinition(reference);
889}
890
892 if (!HasActiveFile()) {
893 return "";
894 }
895 return files_[active_file_id_];
896}
897
899 if (!HasActiveFile() ||
900 active_file_id_ >= static_cast<int>(open_files_.size())) {
902 }
903 return open_files_[active_file_id_].GetCursorPosition();
904}
905
912
914 if (HasActiveFile()) {
916 }
917 return &text_editor_;
918}
919
920void AssemblyEditor::OpenFolder(const std::string& folder_path) {
921 current_folder_ = LoadFolder(folder_path);
922 if (symbol_jump_root_ != folder_path) {
923 symbol_jump_root_ = folder_path;
925 }
926}
927
932
933// =============================================================================
934// Panel Content Drawing (WindowContent System)
935// =============================================================================
936
938 TextEditor* editor = GetActiveEditor();
939 // Menu bar for file operations
940 if (ImGui::BeginMenuBar()) {
941 DrawFileMenu();
942 DrawEditMenu();
944 ImGui::EndMenuBar();
945 }
946
947 // Status line
948 auto cpos = editor->GetCursorPosition();
949 const char* file_label =
950 current_file_.empty() ? "No file" : current_file_.c_str();
951 ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1,
952 cpos.mColumn + 1, editor->GetTotalLines(),
953 editor->IsOverwrite() ? "Ovr" : "Ins",
954 editor->CanUndo() ? "*" : " ",
955 editor->GetLanguageDefinition().mName.c_str(), file_label);
956
957 // Main text editor
958 editor->Render("##asm_editor",
959 ImVec2(0, -ImGui::GetFrameHeightWithSpacing()));
960
961 // Draw open file tabs at bottom
963}
964
966 // Lazy load project folder if not already loaded
967 if (current_folder_.name.empty() && dependencies_.project &&
971 }
972
973 // Open folder button if no folder loaded
974 if (current_folder_.name.empty()) {
975 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open Folder",
976 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
978 }
979 ImGui::Spacing();
980 ImGui::TextDisabled("No folder opened");
981 return;
982 }
983
984 // Folder path display
985 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s",
986 current_folder_.name.c_str());
987 ImGui::Separator();
988
989 // File tree
991}
992
994 if (symbols_.empty()) {
995 ImGui::TextDisabled("No symbols loaded.");
996 ImGui::Spacing();
997 ImGui::TextWrapped(
998 "Apply a patch or load external symbols to populate this list.");
999 return;
1000 }
1001
1002 // Search filter
1003 static char filter[256] = "";
1004 ImGui::SetNextItemWidth(-1);
1005 ImGui::InputTextWithHint("##symbol_filter",
1006 ICON_MD_SEARCH " Filter symbols...", filter,
1007 sizeof(filter));
1008 ImGui::Separator();
1009
1010 // Symbol list
1011 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), false)) {
1012 for (const auto& [name, symbol] : symbols_) {
1013 // Apply filter
1014 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
1015 continue;
1016 }
1017
1018 ImGui::PushID(name.c_str());
1019 if (ImGui::Selectable(name.c_str())) {
1020 // Could jump to symbol definition if line info is available
1021 }
1022 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
1023 ImGui::TextDisabled("$%06X", symbol.address);
1024 ImGui::PopID();
1025 }
1026 }
1027 ImGui::EndChild();
1028}
1029
1031 // Error/warning counts
1032 ImGui::Text("Errors: %zu Warnings: %zu", last_errors_.size(),
1033 last_warnings_.size());
1034 ImGui::Separator();
1035
1036 // Build buttons
1037 bool has_active_file = HasActiveFile();
1038 bool has_rom = (rom_ && rom_->is_loaded());
1039
1040 if (ImGui::Button(ICON_MD_CHECK_CIRCLE " Validate", ImVec2(120, 0))) {
1041 if (has_active_file) {
1042 auto status = ValidateCurrentFile();
1043 if (status.ok() && dependencies_.toast_manager) {
1044 dependencies_.toast_manager->Show("Validation passed!",
1046 }
1047 }
1048 }
1049 ImGui::SameLine();
1050 bool apply_disabled = !has_rom || !has_active_file;
1051 ImGui::BeginDisabled(apply_disabled);
1052 if (ImGui::Button(ICON_MD_BUILD " Apply to ROM", ImVec2(140, 0))) {
1053 auto status = ApplyPatchToRom();
1054 if (status.ok() && dependencies_.toast_manager) {
1056 }
1057 }
1058 ImGui::EndDisabled();
1059 if (apply_disabled &&
1060 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1061 if (!has_rom)
1062 ImGui::SetTooltip("Load a ROM first");
1063 else
1064 ImGui::SetTooltip("Open an assembly file first");
1065 }
1066
1067 ImGui::Separator();
1068
1069 // Output log
1070 if (ImGui::BeginChild("##build_log", ImVec2(0, 0), true)) {
1071 if (!last_diagnostics_.empty()) {
1072 // Structured path — when the backend (z3dk, or future enriched Asar)
1073 // gave us file:line diagnostics, render via the dedicated panel.
1074 DiagnosticsPanelCallbacks callbacks;
1075 callbacks.on_diagnostic_activated = [this](const std::string& file,
1076 int line, int column) {
1077 if (!file.empty()) {
1078 std::string reference = file;
1079 if (line > 0) {
1080 reference = absl::StrCat(reference, ":", line);
1081 if (column > 0) {
1082 reference = absl::StrCat(reference, ":", column);
1083 }
1084 }
1085 auto status = JumpToReference(reference);
1086 if (!status.ok() && dependencies_.toast_manager) {
1088 "Failed to open diagnostic location: " +
1089 std::string(status.message()),
1091 }
1092 return;
1093 }
1094 // TextEditor coords are 0-based; diagnostics are 1-based.
1095 if (auto* editor = GetActiveEditor()) {
1096 TextEditor::Coordinates coords(line > 0 ? line - 1 : 0,
1097 column > 0 ? column - 1 : 0);
1098 editor->SetCursorPosition(coords);
1099 }
1100 };
1102 } else {
1103 // Legacy flat-string fallback (vanilla Asar without structured output).
1104 for (const auto& error : last_errors_) {
1105 gui::StyleColorGuard err_guard(ImGuiCol_Text,
1106 ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
1107 ImGui::TextWrapped("%s %s", ICON_MD_ERROR, error.c_str());
1108 }
1109 for (const auto& warning : last_warnings_) {
1110 gui::StyleColorGuard warn_guard(ImGuiCol_Text,
1111 ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
1112 ImGui::TextWrapped("%s %s", ICON_MD_WARNING, warning.c_str());
1113 }
1114 if (last_errors_.empty() && last_warnings_.empty()) {
1115 ImGui::TextDisabled("No build output");
1116 }
1117 }
1118 }
1119 ImGui::EndChild();
1120}
1121
1122std::optional<uint32_t> AssemblyEditor::CurrentDisassemblyBank() const {
1123 if (auto symbol_it = symbols_.find(disasm_query_);
1124 symbol_it != symbols_.end()) {
1125 return (symbol_it->second.address >> 16) & 0xFFu;
1126 }
1127 auto parsed = ParseHexAddress(disasm_query_);
1128 if (!parsed.has_value()) {
1129 return std::nullopt;
1130 }
1131 return (*parsed >> 16) & 0xFFu;
1132}
1133
1135 if (const char* env = std::getenv("Z3DISASM_BIN"); env && env[0] != '\0') {
1136 return std::string(env);
1137 }
1138
1139 std::error_code ec;
1140 const auto cwd = std::filesystem::current_path(ec);
1141 if (!ec) {
1142 if (auto script =
1143 FindUpwardPath(cwd, std::filesystem::path("scripts") / "z3disasm");
1144 script.has_value()) {
1145 return script->string();
1146 }
1147 }
1148
1149 return "z3disasm";
1150}
1151
1153 if (!z3disasm_output_dir_.empty()) {
1154 return z3disasm_output_dir_;
1155 }
1156 if (dependencies_.project) {
1157 return dependencies_.project->GetZ3dkArtifactPath("z3disasm");
1158 }
1159 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
1160 return (std::filesystem::path(rom_->filename()).parent_path() / "z3disasm")
1161 .string();
1162 }
1163 std::error_code ec;
1164 return (std::filesystem::current_path(ec) / "z3disasm").string();
1165}
1166
1168 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
1169 return rom_->filename();
1170 }
1171 if (dependencies_.project) {
1174 }
1176 }
1177 return {};
1178}
1179
1181 if (auto bank = ParseGeneratedBankIndex(z3disasm_selected_path_);
1182 bank.has_value()) {
1183 return *bank;
1184 }
1185 return -1;
1186}
1187
1189 const int bank = SelectedZ3DisasmBankIndex();
1190 if (bank < 0) {
1191 return {};
1192 }
1193 return absl::StrFormat("project-graph --query=bank --bank=%02X --format=json",
1194 bank);
1195}
1196
1198 uint32_t address) const {
1199 return absl::StrFormat(
1200 "project-graph --query=lookup --address=%06X --format=json", address);
1201}
1202
1204 const std::vector<std::string>& args, const std::string& title) {
1205 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
1206 if (!editor_manager || !editor_manager->right_drawer_manager()) {
1207 return absl::FailedPreconditionError(
1208 "Right drawer manager is unavailable for project-graph results");
1209 }
1210
1212 if (dependencies_.project) {
1214 }
1216 tool.SetAsarWrapper(&asar_);
1217
1218 std::string output;
1219 auto status = tool.Run(args, nullptr, &output);
1220 if (!status.ok()) {
1221 return status;
1222 }
1223
1225 actions.on_open_reference = [this](const std::string& reference) {
1226 JumpToReference(reference).IgnoreError();
1227 };
1228 actions.on_open_address = [this](uint32_t address) {
1229 disasm_query_ = absl::StrFormat("0x%06X", address);
1230 NavigateDisassemblyQuery().IgnoreError();
1231 };
1232 actions.on_open_lookup = [this](uint32_t address) {
1234 {"--query=lookup", absl::StrFormat("--address=%06X", address),
1235 "--format=json"},
1236 absl::StrFormat("Project Graph Lookup $%06X", address))
1237 .IgnoreError();
1238 };
1239
1240 editor_manager->right_drawer_manager()->SetToolOutput(
1241 title, absl::StrJoin(args, " "), output, std::move(actions));
1242 editor_manager->right_drawer_manager()->OpenDrawer(
1244 return absl::OkStatus();
1245}
1246
1248 z3disasm_source_jumps_.clear();
1249 z3disasm_hook_jumps_.clear();
1250
1251 if (!dependencies_.project) {
1252 return;
1253 }
1254
1255 const int selected_bank = SelectedZ3DisasmBankIndex();
1256 if (selected_bank < 0) {
1257 return;
1258 }
1259
1260 const auto& project = *dependencies_.project;
1261 Json sourcemap;
1262 const std::string sourcemap_path =
1263 !project.z3dk_settings.artifact_paths.sourcemap_json.empty()
1264 ? project.z3dk_settings.artifact_paths.sourcemap_json
1265 : project.GetZ3dkArtifactPath("sourcemap.json");
1266 if (LoadJsonFile(sourcemap_path, &sourcemap) &&
1267 sourcemap.contains("entries") && sourcemap["entries"].is_array() &&
1268 sourcemap.contains("files") && sourcemap["files"].is_array()) {
1269 std::map<int, std::string> files_by_id;
1270 for (const auto& file : sourcemap["files"]) {
1271 files_by_id[file.value("id", -1)] = file.value("path", "");
1272 }
1273
1274 for (const auto& entry : sourcemap["entries"]) {
1275 const std::string address_str = entry.value("address", "0x0");
1276 auto address = ParseHexAddress(address_str);
1277 if (!address.has_value() ||
1278 NormalizeLoRomBankIndex(*address) != selected_bank) {
1279 continue;
1280 }
1281
1282 Z3DisasmSourceJump jump;
1283 jump.address = *address;
1284 jump.line = entry.value("line", 0);
1285 jump.file = files_by_id[entry.value("file_id", -1)];
1286 if (!jump.file.empty()) {
1287 z3disasm_source_jumps_.push_back(std::move(jump));
1288 }
1289 }
1290 std::sort(z3disasm_source_jumps_.begin(), z3disasm_source_jumps_.end(),
1291 [](const Z3DisasmSourceJump& lhs, const Z3DisasmSourceJump& rhs) {
1292 if (lhs.address != rhs.address) {
1293 return lhs.address < rhs.address;
1294 }
1295 if (lhs.file != rhs.file) {
1296 return lhs.file < rhs.file;
1297 }
1298 return lhs.line < rhs.line;
1299 });
1300 }
1301
1302 Json hooks;
1303 const std::string hooks_path =
1304 !project.z3dk_settings.artifact_paths.hooks_json.empty()
1305 ? project.z3dk_settings.artifact_paths.hooks_json
1306 : project.GetZ3dkArtifactPath("hooks.json");
1307 if (LoadJsonFile(hooks_path, &hooks) && hooks.contains("hooks") &&
1308 hooks["hooks"].is_array()) {
1309 for (const auto& hook : hooks["hooks"]) {
1310 const std::string address_str = hook.value("address", "0x0");
1311 auto address = ParseHexAddress(address_str);
1312 if (!address.has_value() ||
1313 NormalizeLoRomBankIndex(*address) != selected_bank) {
1314 continue;
1315 }
1316
1317 Z3DisasmHookJump jump;
1318 jump.address = *address;
1319 jump.size = hook.value("size", 0);
1320 jump.kind = hook.value("kind", "patch");
1321 jump.name = hook.value("name", "");
1322 jump.source = hook.value("source", "");
1323 z3disasm_hook_jumps_.push_back(std::move(jump));
1324 }
1325 std::sort(z3disasm_hook_jumps_.begin(), z3disasm_hook_jumps_.end(),
1326 [](const Z3DisasmHookJump& lhs, const Z3DisasmHookJump& rhs) {
1327 if (lhs.address != rhs.address) {
1328 return lhs.address < rhs.address;
1329 }
1330 return lhs.name < rhs.name;
1331 });
1332 }
1333}
1334
1335void AssemblyEditor::LoadSelectedZ3DisassemblyFile() {
1336 z3disasm_selected_contents_.clear();
1337 z3disasm_selected_path_.clear();
1338 z3disasm_source_jumps_.clear();
1339 z3disasm_hook_jumps_.clear();
1340 if (z3disasm_selected_index_ < 0 ||
1341 z3disasm_selected_index_ >= static_cast<int>(z3disasm_files_.size())) {
1342 return;
1343 }
1344
1345 z3disasm_selected_path_ = z3disasm_files_[z3disasm_selected_index_];
1346 std::ifstream file(z3disasm_selected_path_);
1347 if (!file.is_open()) {
1348 z3disasm_status_ = absl::StrCat("Failed to open ", z3disasm_selected_path_);
1349 return;
1350 }
1351 z3disasm_selected_contents_.assign(std::istreambuf_iterator<char>(file),
1352 std::istreambuf_iterator<char>());
1353 RefreshSelectedZ3DisassemblyMetadata();
1354}
1355
1356void AssemblyEditor::RefreshZ3DisassemblyFiles() {
1357 z3disasm_files_.clear();
1358 const std::filesystem::path output_dir(ResolveZ3DisasmOutputDir());
1359 std::error_code ec;
1360 if (!std::filesystem::exists(output_dir, ec) || ec) {
1361 z3disasm_selected_index_ = -1;
1362 z3disasm_selected_path_.clear();
1363 z3disasm_selected_contents_.clear();
1364 z3disasm_source_jumps_.clear();
1365 z3disasm_hook_jumps_.clear();
1366 return;
1367 }
1368
1369 std::string previous_selection = z3disasm_selected_path_;
1370 for (const auto& entry :
1371 std::filesystem::directory_iterator(output_dir, ec)) {
1372 if (ec) {
1373 break;
1374 }
1375 if (!entry.is_regular_file()) {
1376 continue;
1377 }
1378 if (IsGeneratedBankFile(entry.path())) {
1379 z3disasm_files_.push_back(entry.path().string());
1380 }
1381 }
1382 std::sort(z3disasm_files_.begin(), z3disasm_files_.end());
1383
1384 if (z3disasm_files_.empty()) {
1385 z3disasm_selected_index_ = -1;
1386 z3disasm_selected_path_.clear();
1387 z3disasm_selected_contents_.clear();
1388 z3disasm_source_jumps_.clear();
1389 z3disasm_hook_jumps_.clear();
1390 return;
1391 }
1392
1393 z3disasm_selected_index_ = 0;
1394 if (!previous_selection.empty()) {
1395 const auto it = std::find(z3disasm_files_.begin(), z3disasm_files_.end(),
1396 previous_selection);
1397 if (it != z3disasm_files_.end()) {
1398 z3disasm_selected_index_ =
1399 static_cast<int>(std::distance(z3disasm_files_.begin(), it));
1400 }
1401 }
1402 LoadSelectedZ3DisassemblyFile();
1403}
1404
1405void AssemblyEditor::PollZ3DisassemblyTask() {
1406 const auto snapshot = z3disasm_task_.GetSnapshot();
1407 if (!snapshot.started) {
1408 return;
1409 }
1410 if (snapshot.running) {
1411 z3disasm_status_ =
1412 absl::StrCat("z3disasm running...\n", snapshot.output_tail);
1413 return;
1414 }
1415 if (z3disasm_task_acknowledged_ || !snapshot.finished) {
1416 return;
1417 }
1418
1419 z3disasm_task_acknowledged_ = true;
1420 if (snapshot.status.ok()) {
1421 RefreshZ3DisassemblyFiles();
1422 z3disasm_status_ = absl::StrCat(
1423 "z3disasm finished.",
1424 z3disasm_files_.empty()
1425 ? std::string(" No bank files were generated.")
1426 : absl::StrFormat(" Loaded %d bank file(s).",
1427 static_cast<int>(z3disasm_files_.size())));
1428 } else {
1429 z3disasm_status_ =
1430 absl::StrCat("z3disasm failed: ", snapshot.status.message(), "\n",
1431 snapshot.output_tail);
1432 }
1433 z3disasm_task_.Wait().IgnoreError();
1434}
1435
1436absl::Status AssemblyEditor::GenerateZ3Disassembly() {
1437 if (const auto snapshot = z3disasm_task_.GetSnapshot(); snapshot.running) {
1438 return absl::FailedPreconditionError("z3disasm is already running");
1439 }
1440
1441 const std::string rom_path = ResolveZ3DisasmRomPath();
1442 if (rom_path.empty()) {
1443 return absl::FailedPreconditionError(
1444 "No ROM path is available for z3disasm");
1445 }
1446
1447 const std::string command_path = ResolveZ3DisasmCommand();
1448 const std::string output_dir = ResolveZ3DisasmOutputDir();
1449 z3disasm_output_dir_ = output_dir;
1450
1451 std::error_code ec;
1452 std::filesystem::create_directories(output_dir, ec);
1453 for (const auto& entry :
1454 std::filesystem::directory_iterator(output_dir, ec)) {
1455 if (!ec && entry.is_regular_file() && IsGeneratedBankFile(entry.path())) {
1456 std::filesystem::remove(entry.path(), ec);
1457 }
1458 }
1459
1460 std::string command = ShellQuote(command_path);
1461 command += " --rom ";
1462 command += ShellQuote(rom_path);
1463 command += " --out ";
1464 command += ShellQuote(output_dir);
1465
1466 if (!z3disasm_all_banks_) {
1467 command += absl::StrFormat(" --bank-start %02X --bank-end %02X",
1468 std::clamp(z3disasm_bank_start_, 0, 0xFF),
1469 std::clamp(z3disasm_bank_end_, 0, 0xFF));
1470 }
1471
1472 if (dependencies_.project) {
1473 const std::string symbols_path =
1474 !dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb.empty()
1475 ? dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb
1476 : dependencies_.project->GetZ3dkArtifactPath("symbols.mlb");
1477 if (!symbols_path.empty() && std::filesystem::exists(symbols_path)) {
1478 command += " --symbols ";
1479 command += ShellQuote(symbols_path);
1480 }
1481
1482 const std::string hooks_path =
1483 !dependencies_.project->z3dk_settings.artifact_paths.hooks_json.empty()
1484 ? dependencies_.project->z3dk_settings.artifact_paths.hooks_json
1485 : dependencies_.project->GetZ3dkArtifactPath("hooks.json");
1486 if (!hooks_path.empty() && std::filesystem::exists(hooks_path)) {
1487 command += " --hooks ";
1488 command += ShellQuote(hooks_path);
1489 }
1490 }
1491
1492 z3disasm_task_acknowledged_ = false;
1493 z3disasm_selected_index_ = -1;
1494 z3disasm_selected_path_.clear();
1495 z3disasm_selected_contents_.clear();
1496 z3disasm_source_jumps_.clear();
1497 z3disasm_hook_jumps_.clear();
1498 z3disasm_files_.clear();
1499 z3disasm_status_ = "Launching z3disasm...";
1500 return z3disasm_task_.Start(command,
1501 std::filesystem::current_path().string());
1502}
1503
1504absl::Status AssemblyEditor::NavigateDisassemblyQuery() {
1505 auto symbol_it = symbols_.find(disasm_query_);
1506 if (symbol_it != symbols_.end()) {
1507 disasm_query_ = absl::StrCat("0x", absl::Hex(symbol_it->second.address));
1508 disasm_status_ = absl::StrCat("Resolved symbol ", symbol_it->first);
1509 return absl::OkStatus();
1510 }
1511
1512 auto parsed = ParseHexAddress(disasm_query_);
1513 if (!parsed.has_value()) {
1514 return absl::InvalidArgumentError("Enter a SNES address or known symbol");
1515 }
1516
1517 disasm_query_ = absl::StrCat("0x", absl::Hex(*parsed));
1518 disasm_status_ = absl::StrFormat("Showing disassembly at $%06X", *parsed);
1519 return absl::OkStatus();
1520}
1521
1522void AssemblyEditor::DrawDisassemblyContent() {
1523 PollZ3DisassemblyTask();
1524
1525 if (z3disasm_output_dir_.empty()) {
1526 z3disasm_output_dir_ = ResolveZ3DisasmOutputDir();
1527 }
1528 if (z3disasm_files_.empty()) {
1529 RefreshZ3DisassemblyFiles();
1530 }
1531
1532 ImGui::TextDisabled(
1533 "z3disasm-backed bank browser for generated `bank_XX.asm` files.");
1534 const std::string rom_path = ResolveZ3DisasmRomPath();
1535 ImGui::TextWrapped("ROM: %s",
1536 rom_path.empty() ? "<unavailable>" : rom_path.c_str());
1537 ImGui::TextWrapped("Output: %s", z3disasm_output_dir_.c_str());
1538
1539 ImGui::Checkbox("All banks", &z3disasm_all_banks_);
1540 if (!z3disasm_all_banks_) {
1541 ImGui::SameLine();
1542 ImGui::SetNextItemWidth(70.0f);
1543 ImGui::InputInt("Start", &z3disasm_bank_start_);
1544 ImGui::SameLine();
1545 ImGui::SetNextItemWidth(70.0f);
1546 ImGui::InputInt("End", &z3disasm_bank_end_);
1547 z3disasm_bank_start_ = std::clamp(z3disasm_bank_start_, 0, 0xFF);
1548 z3disasm_bank_end_ = std::clamp(z3disasm_bank_end_, 0, 0xFF);
1549 if (z3disasm_bank_end_ < z3disasm_bank_start_) {
1550 z3disasm_bank_end_ = z3disasm_bank_start_;
1551 }
1552 ImGui::SameLine();
1553 if (ImGui::SmallButton("Use Query Bank")) {
1554 if (auto bank = CurrentDisassemblyBank(); bank.has_value()) {
1555 z3disasm_bank_start_ = static_cast<int>(*bank);
1556 z3disasm_bank_end_ = static_cast<int>(*bank);
1557 }
1558 }
1559 }
1560
1561 const auto task_snapshot = z3disasm_task_.GetSnapshot();
1562 const bool can_generate = !rom_path.empty();
1563 ImGui::BeginDisabled(!can_generate || task_snapshot.running);
1564 if (ImGui::Button(ICON_MD_REFRESH " Generate / Refresh Banks")) {
1565 auto status = GenerateZ3Disassembly();
1566 if (!status.ok()) {
1567 z3disasm_status_ = std::string(status.message());
1568 if (dependencies_.toast_manager) {
1569 dependencies_.toast_manager->Show(z3disasm_status_, ToastType::kError);
1570 }
1571 }
1572 }
1573 ImGui::EndDisabled();
1574 if (task_snapshot.running) {
1575 ImGui::SameLine();
1576 if (ImGui::Button(ICON_MD_CANCEL " Cancel")) {
1577 z3disasm_task_.Cancel();
1578 }
1579 }
1580 if (!can_generate) {
1581 ImGui::SameLine();
1582 ImGui::TextDisabled("Load or configure a ROM path first.");
1583 }
1584
1585 if (!z3disasm_status_.empty()) {
1586 ImGui::Spacing();
1587 ImGui::TextWrapped("%s", z3disasm_status_.c_str());
1588 }
1589
1590 if (!task_snapshot.output_tail.empty()) {
1591 if (ImGui::CollapsingHeader("z3disasm Output")) {
1592 std::string output_tail = task_snapshot.output_tail;
1593 ImGui::InputTextMultiline("##z3disasm_output", &output_tail,
1594 ImVec2(-1.0f, 80.0f),
1595 ImGuiInputTextFlags_ReadOnly);
1596 }
1597 }
1598
1599 ImGui::SeparatorText("Bank Browser");
1600 if (z3disasm_files_.empty()) {
1601 ImGui::TextDisabled(
1602 "No generated bank files yet. Run z3disasm to populate this browser.");
1603 } else {
1604 if (ImGui::BeginTable(
1605 "##z3disasm_browser", 2,
1606 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
1607 ImGui::TableSetupColumn("Banks", ImGuiTableColumnFlags_WidthFixed,
1608 180.0f);
1609 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch);
1610 ImGui::TableNextRow();
1611
1612 ImGui::TableSetColumnIndex(0);
1613 if (ImGui::BeginChild("##z3disasm_list", ImVec2(0, 0), true)) {
1614 for (int i = 0; i < static_cast<int>(z3disasm_files_.size()); ++i) {
1615 const std::string name =
1616 std::filesystem::path(z3disasm_files_[i]).filename().string();
1617 if (ImGui::Selectable(name.c_str(), z3disasm_selected_index_ == i)) {
1618 z3disasm_selected_index_ = i;
1619 LoadSelectedZ3DisassemblyFile();
1620 }
1621 }
1622 }
1623 ImGui::EndChild();
1624
1625 ImGui::TableSetColumnIndex(1);
1626 if (ImGui::BeginChild("##z3disasm_preview", ImVec2(0, 0), true)) {
1627 if (!z3disasm_selected_path_.empty()) {
1628 const std::string selected_name =
1629 std::filesystem::path(z3disasm_selected_path_)
1630 .filename()
1631 .string();
1632 ImGui::TextUnformatted(selected_name.c_str());
1633 ImGui::SameLine();
1634 if (ImGui::SmallButton("Open in Code Editor")) {
1635 ChangeActiveFile(z3disasm_selected_path_);
1636 }
1637 const std::string bank_query = BuildProjectGraphBankQuery();
1638 if (!bank_query.empty()) {
1639 ImGui::SameLine();
1640 if (ImGui::SmallButton("Open Bank Graph")) {
1641 auto status = RunProjectGraphQueryInDrawer(
1642 {"--query=bank",
1643 absl::StrFormat("--bank=%02X", SelectedZ3DisasmBankIndex()),
1644 "--format=json"},
1645 absl::StrFormat("Project Graph Bank $%02X",
1646 SelectedZ3DisasmBankIndex()));
1647 if (!status.ok()) {
1648 z3disasm_status_ = std::string(status.message());
1649 }
1650 }
1651 ImGui::SameLine();
1652 if (ImGui::SmallButton("Copy Bank Query")) {
1653 ImGui::SetClipboardText(bank_query.c_str());
1654 }
1655 ImGui::TextDisabled("%s", bank_query.c_str());
1656 }
1657 ImGui::Separator();
1658 if (!z3disasm_source_jumps_.empty() &&
1659 ImGui::CollapsingHeader("Source Map Jumps",
1660 ImGuiTreeNodeFlags_DefaultOpen)) {
1661 for (const auto& jump : z3disasm_source_jumps_) {
1662 ImGui::PushID(static_cast<int>(jump.address) ^ jump.line);
1663 const std::string source_label =
1664 absl::StrFormat("Source##source_jump_%06X", jump.address);
1665 const std::string graph_label =
1666 absl::StrFormat("Graph##source_graph_%06X", jump.address);
1667 const std::string copy_label =
1668 absl::StrFormat("Copy##source_copy_%06X", jump.address);
1669 const std::string addr_label =
1670 absl::StrFormat("Addr##source_addr_%06X", jump.address);
1671 if (ImGui::SmallButton(source_label.c_str())) {
1672 JumpToReference(absl::StrCat(jump.file, ":", jump.line))
1673 .IgnoreError();
1674 }
1675 ImGui::SameLine();
1676 if (ImGui::SmallButton(graph_label.c_str())) {
1677 auto status = RunProjectGraphQueryInDrawer(
1678 {"--query=lookup",
1679 absl::StrFormat("--address=%06X", jump.address),
1680 "--format=json"},
1681 absl::StrFormat("Project Graph Lookup $%06X",
1682 jump.address));
1683 if (!status.ok()) {
1684 z3disasm_status_ = std::string(status.message());
1685 }
1686 }
1687 ImGui::SameLine();
1688 if (ImGui::SmallButton(copy_label.c_str())) {
1689 const std::string query =
1690 BuildProjectGraphLookupQuery(jump.address);
1691 ImGui::SetClipboardText(query.c_str());
1692 }
1693 ImGui::SameLine();
1694 if (ImGui::SmallButton(addr_label.c_str())) {
1695 disasm_query_ = absl::StrFormat("0x%06X", jump.address);
1696 NavigateDisassemblyQuery().IgnoreError();
1697 }
1698 ImGui::SameLine();
1699 ImGui::TextWrapped("$%06X %s:%d", jump.address,
1700 jump.file.c_str(), jump.line);
1701 ImGui::PopID();
1702 }
1703 }
1704 if (!z3disasm_hook_jumps_.empty() &&
1705 ImGui::CollapsingHeader("Hook Jumps",
1706 ImGuiTreeNodeFlags_DefaultOpen)) {
1707 for (const auto& hook : z3disasm_hook_jumps_) {
1708 ImGui::PushID(static_cast<int>(hook.address) ^ hook.size ^
1709 0x4000);
1710 const std::string source_label =
1711 absl::StrFormat("Source##hook_jump_%06X", hook.address);
1712 const std::string graph_label =
1713 absl::StrFormat("Graph##hook_graph_%06X", hook.address);
1714 const std::string copy_label =
1715 absl::StrFormat("Copy##hook_copy_%06X", hook.address);
1716 const std::string addr_label =
1717 absl::StrFormat("Addr##hook_addr_%06X", hook.address);
1718 if (!hook.source.empty() &&
1719 ImGui::SmallButton(source_label.c_str())) {
1720 JumpToReference(hook.source).IgnoreError();
1721 }
1722 if (!hook.source.empty()) {
1723 ImGui::SameLine();
1724 }
1725 if (ImGui::SmallButton(graph_label.c_str())) {
1726 auto status = RunProjectGraphQueryInDrawer(
1727 {"--query=lookup",
1728 absl::StrFormat("--address=%06X", hook.address),
1729 "--format=json"},
1730 absl::StrFormat("Project Graph Lookup $%06X",
1731 hook.address));
1732 if (!status.ok()) {
1733 z3disasm_status_ = std::string(status.message());
1734 }
1735 }
1736 ImGui::SameLine();
1737 if (ImGui::SmallButton(copy_label.c_str())) {
1738 const std::string query =
1739 BuildProjectGraphLookupQuery(hook.address);
1740 ImGui::SetClipboardText(query.c_str());
1741 }
1742 ImGui::SameLine();
1743 if (ImGui::SmallButton(addr_label.c_str())) {
1744 disasm_query_ = absl::StrFormat("0x%06X", hook.address);
1745 NavigateDisassemblyQuery().IgnoreError();
1746 }
1747 ImGui::SameLine();
1748 ImGui::TextWrapped("$%06X %s %s (+%d) %s", hook.address,
1749 hook.kind.c_str(), hook.name.c_str(),
1750 hook.size, hook.source.c_str());
1751 ImGui::PopID();
1752 }
1753 }
1754 ImGui::Separator();
1755 ImGui::InputTextMultiline(
1756 "##z3disasm_text", &z3disasm_selected_contents_,
1757 ImVec2(-1.0f, -1.0f), ImGuiInputTextFlags_ReadOnly);
1758 } else {
1759 ImGui::TextDisabled("Select a generated bank file to preview it.");
1760 }
1761 }
1762 ImGui::EndChild();
1763 ImGui::EndTable();
1764 }
1765 }
1766
1767 ImGui::SeparatorText("Quick ROM Slice");
1768 ImGui::TextDisabled("Inline viewer for the currently loaded ROM buffer.");
1769 ImGui::SetNextItemWidth(220.0f);
1770 ImGui::InputText("Address or Symbol", &disasm_query_);
1771 ImGui::SameLine();
1772 if (ImGui::Button("Go")) {
1773 auto status = NavigateDisassemblyQuery();
1774 if (!status.ok()) {
1775 disasm_status_ = std::string(status.message());
1776 }
1777 }
1778 ImGui::SameLine();
1779 ImGui::SetNextItemWidth(80.0f);
1780 ImGui::InputInt("Count", &disasm_instruction_count_);
1781 if (disasm_instruction_count_ < 1) {
1782 disasm_instruction_count_ = 1;
1783 }
1784 if (disasm_instruction_count_ > 128) {
1785 disasm_instruction_count_ = 128;
1786 }
1787
1788 if (!disasm_status_.empty()) {
1789 ImGui::TextDisabled("%s", disasm_status_.c_str());
1790 }
1791 ImGui::Separator();
1792
1793 if (!rom_ || !rom_->is_loaded()) {
1794 ImGui::TextDisabled("Load a ROM to browse disassembly.");
1795 return;
1796 }
1797
1798 uint32_t start_address = 0;
1799 if (auto symbol_it = symbols_.find(disasm_query_);
1800 symbol_it != symbols_.end()) {
1801 start_address = symbol_it->second.address;
1802 } else {
1803 auto parsed = ParseHexAddress(disasm_query_);
1804 if (!parsed.has_value()) {
1805 ImGui::TextDisabled(
1806 "Enter a SNES address like 0x008000 or a known label.");
1807 return;
1808 }
1809 start_address = *parsed;
1810 }
1811
1812 std::map<uint32_t, std::string> symbols_by_address;
1813 for (const auto& [name, symbol] : symbols_) {
1814 symbols_by_address.emplace(symbol.address, name);
1815 }
1816
1817 emu::debug::Disassembler65816 disassembler;
1818 disassembler.SetSymbolResolver([&symbols_by_address](uint32_t address) {
1819 auto it = symbols_by_address.find(address);
1820 return it == symbols_by_address.end() ? std::string() : it->second;
1821 });
1822
1823 auto read_byte = [this](uint32_t snes_addr) -> uint8_t {
1824 if (!rom_ || !rom_->is_loaded()) {
1825 return 0;
1826 }
1827 const uint32_t pc_addr = SnesToPc(snes_addr);
1828 if (pc_addr >= rom_->size()) {
1829 return 0;
1830 }
1831 return rom_->vector()[pc_addr];
1832 };
1833
1834 const auto instructions = disassembler.DisassembleRange(
1835 start_address, static_cast<size_t>(disasm_instruction_count_), read_byte);
1836 if (instructions.empty()) {
1837 ImGui::TextDisabled("No disassembly available for this address.");
1838 return;
1839 }
1840
1841 if (ImGui::BeginTable("##assembly_disasm", 3,
1842 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
1843 ImGuiTableFlags_Resizable)) {
1844 ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed,
1845 110.0f);
1846 ImGui::TableSetupColumn("Instruction", ImGuiTableColumnFlags_WidthStretch);
1847 ImGui::TableSetupColumn("Context", ImGuiTableColumnFlags_WidthFixed,
1848 140.0f);
1849 ImGui::TableHeadersRow();
1850
1851 for (const auto& instruction : instructions) {
1852 ImGui::PushID(static_cast<int>(instruction.address));
1853 ImGui::TableNextRow();
1854
1855 ImGui::TableSetColumnIndex(0);
1856 const std::string address_label =
1857 absl::StrFormat("$%06X", instruction.address);
1858 ImGui::TextUnformatted(address_label.c_str());
1859
1860 ImGui::TableSetColumnIndex(1);
1861 if (auto label_it = symbols_by_address.find(instruction.address);
1862 label_it != symbols_by_address.end()) {
1863 ImGui::TextColored(ImVec4(0.75f, 0.85f, 1.0f, 1.0f),
1864 "%s:", label_it->second.c_str());
1865 }
1866 ImGui::TextWrapped("%s", instruction.full_text.c_str());
1867
1868 ImGui::TableSetColumnIndex(2);
1869 if (instruction.branch_target != 0) {
1870 auto target_it = symbols_by_address.find(instruction.branch_target);
1871 if (target_it != symbols_by_address.end()) {
1872 if (ImGui::SmallButton(target_it->second.c_str())) {
1873 JumpToSymbolDefinition(target_it->second).IgnoreError();
1874 }
1875 } else {
1876 ImGui::Text("$%06X", instruction.branch_target);
1877 }
1878 } else {
1879 ImGui::TextDisabled("-");
1880 }
1881 ImGui::PopID();
1882 }
1883 ImGui::EndTable();
1884 }
1885}
1886
1887void AssemblyEditor::DrawToolbarContent() {
1888 float button_size = 32.0f;
1889
1890 if (ImGui::Button(ICON_MD_FOLDER_OPEN, ImVec2(button_size, button_size))) {
1891 auto folder = FileDialogWrapper::ShowOpenFolderDialog();
1892 if (!folder.empty()) {
1893 current_folder_ = LoadFolder(folder);
1894 }
1895 }
1896 if (ImGui::IsItemHovered())
1897 ImGui::SetTooltip("Open Folder");
1898
1899 ImGui::SameLine();
1900 if (ImGui::Button(ICON_MD_FILE_OPEN, ImVec2(button_size, button_size))) {
1901 auto filename = FileDialogWrapper::ShowOpenFileDialog();
1902 if (!filename.empty()) {
1903 ChangeActiveFile(filename);
1904 }
1905 }
1906 if (ImGui::IsItemHovered())
1907 ImGui::SetTooltip("Open File");
1908
1909 ImGui::SameLine();
1910 bool can_save = HasActiveFile();
1911 ImGui::BeginDisabled(!can_save);
1912 if (ImGui::Button(ICON_MD_SAVE, ImVec2(button_size, button_size))) {
1913 Save();
1914 }
1915 ImGui::EndDisabled();
1916 if (ImGui::IsItemHovered())
1917 ImGui::SetTooltip("Save File");
1918
1919 ImGui::SameLine();
1920 ImGui::Text("|"); // Visual separator
1921 ImGui::SameLine();
1922
1923 // Build actions
1924 ImGui::BeginDisabled(!can_save);
1925 if (ImGui::Button(ICON_MD_CHECK_CIRCLE, ImVec2(button_size, button_size))) {
1926 ValidateCurrentFile();
1927 }
1928 ImGui::EndDisabled();
1929 if (ImGui::IsItemHovered())
1930 ImGui::SetTooltip("Validate (Ctrl+B)");
1931
1932 ImGui::SameLine();
1933 bool can_apply = can_save && rom_ && rom_->is_loaded();
1934 ImGui::BeginDisabled(!can_apply);
1935 if (ImGui::Button(ICON_MD_BUILD, ImVec2(button_size, button_size))) {
1936 ApplyPatchToRom();
1937 }
1938 ImGui::EndDisabled();
1939 if (ImGui::IsItemHovered())
1940 ImGui::SetTooltip("Apply to ROM (Ctrl+Shift+B)");
1941}
1942
1943void AssemblyEditor::DrawFileTabView() {
1944 if (active_files_.empty()) {
1945 return;
1946 }
1947
1948 if (gui::BeginThemedTabBar("##OpenFileTabs",
1949 ImGuiTabBarFlags_Reorderable |
1950 ImGuiTabBarFlags_AutoSelectNewTabs |
1951 ImGuiTabBarFlags_FittingPolicyScroll)) {
1952 for (int i = 0; i < active_files_.Size; i++) {
1953 int file_id = active_files_[i];
1954 if (file_id >= files_.size()) {
1955 continue;
1956 }
1957
1958 // Extract just the filename from the path
1959 std::string filename = files_[file_id];
1960 size_t pos = filename.find_last_of("/\\");
1961 if (pos != std::string::npos) {
1962 filename = filename.substr(pos + 1);
1963 }
1964
1965 bool is_active = (active_file_id_ == file_id);
1966 ImGuiTabItemFlags flags = is_active ? ImGuiTabItemFlags_SetSelected : 0;
1967 bool tab_open = true;
1968
1969 if (ImGui::BeginTabItem(filename.c_str(), &tab_open, flags)) {
1970 // When tab is selected, update active file
1971 if (!is_active) {
1972 active_file_id_ = file_id;
1973 current_file_ = util::GetFileName(files_[file_id]);
1974 }
1975 ImGui::EndTabItem();
1976 }
1977
1978 // Handle tab close
1979 if (!tab_open) {
1980 active_files_.erase(active_files_.Data + i);
1981 if (active_file_id_ == file_id) {
1982 active_file_id_ = active_files_.empty() ? -1 : active_files_[0];
1983 if (active_file_id_ >= 0 &&
1984 active_file_id_ < static_cast<int>(open_files_.size())) {
1985 current_file_ = util::GetFileName(files_[active_file_id_]);
1986 } else {
1987 current_file_.clear();
1988 }
1989 }
1990 i--;
1991 }
1992 }
1994 }
1995}
1996
1997// =============================================================================
1998// Legacy Update Methods (kept for backward compatibility)
1999// =============================================================================
2000
2001absl::Status AssemblyEditor::Update() {
2002 if (!active_) {
2003 return absl::OkStatus();
2004 }
2005
2006 if (dependencies_.window_manager != nullptr) {
2007 return absl::OkStatus();
2008 }
2009
2010 // Legacy window-based update - kept for backward compatibility
2011 // New code should use the panel system via DrawCodeEditor()
2012 ImGui::Begin("Assembly Editor", &active_, ImGuiWindowFlags_MenuBar);
2013 DrawCodeEditor();
2014 ImGui::End();
2015
2016 // Draw symbol panel as separate window if visible (legacy)
2017 DrawSymbolPanel();
2018
2019 return absl::OkStatus();
2020}
2021
2022void AssemblyEditor::InlineUpdate() {
2023 TextEditor* editor = GetActiveEditor();
2024 auto cpos = editor->GetCursorPosition();
2025 const char* file_label =
2026 current_file_.empty() ? "No file" : current_file_.c_str();
2027 ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1,
2028 cpos.mColumn + 1, editor->GetTotalLines(),
2029 editor->IsOverwrite() ? "Ovr" : "Ins",
2030 editor->CanUndo() ? "*" : " ",
2031 editor->GetLanguageDefinition().mName.c_str(), file_label);
2032
2033 editor->Render("##asm_editor", ImVec2(0, 0));
2034}
2035
2036void AssemblyEditor::UpdateCodeView() {
2037 // Deprecated: Use the WindowContent system instead
2038 // This method is kept for backward compatibility during transition
2039 DrawToolbarContent();
2040 ImGui::Separator();
2041 DrawFileBrowser();
2042}
2043
2044absl::Status AssemblyEditor::Save() {
2045 if (!HasActiveFile()) {
2046 return absl::FailedPreconditionError("No active file to save.");
2047 }
2048
2049 const std::string& path = files_[active_file_id_];
2050 std::ofstream file(path);
2051 if (!file.is_open()) {
2052 return absl::InvalidArgumentError(
2053 absl::StrCat("Cannot write file: ", path));
2054 }
2055
2056 file << GetActiveEditor()->GetText();
2057 file.close();
2058 return absl::OkStatus();
2059}
2060
2061void AssemblyEditor::DrawToolset() {
2062 static gui::Toolset toolbar;
2063 toolbar.Begin();
2064
2065 if (toolbar.AddAction(ICON_MD_FOLDER_OPEN, "Open Folder")) {
2066 current_folder_ = LoadFolder(FileDialogWrapper::ShowOpenFolderDialog());
2067 }
2068 if (toolbar.AddAction(ICON_MD_SAVE, "Save File")) {
2069 Save();
2070 }
2071
2072 toolbar.End();
2073}
2074
2075void AssemblyEditor::DrawCurrentFolder() {
2076 // Lazy load project folder if not already loaded
2077 if (current_folder_.name.empty() && dependencies_.project &&
2078 !dependencies_.project->code_folder.empty()) {
2079 OpenFolder(dependencies_.project->GetAbsolutePath(
2080 dependencies_.project->code_folder));
2081 }
2082
2083 if (ImGui::BeginChild("##current_folder", ImVec2(0, 0), true,
2084 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
2085 if (ImGui::BeginTable("##file_table", 2,
2086 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
2087 ImGuiTableFlags_Resizable |
2088 ImGuiTableFlags_Sortable)) {
2089 ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 256.0f);
2090 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch);
2091
2092 ImGui::TableHeadersRow();
2093
2094 for (const auto& file : current_folder_.files) {
2095 ImGui::TableNextRow();
2096 ImGui::TableNextColumn();
2097 if (ImGui::Selectable(file.c_str())) {
2098 ChangeActiveFile(absl::StrCat(current_folder_.name, "/", file));
2099 }
2100 ImGui::TableNextColumn();
2101 ImGui::Text("File");
2102 }
2103
2104 for (const auto& subfolder : current_folder_.subfolders) {
2105 ImGui::TableNextRow();
2106 ImGui::TableNextColumn();
2107 if (ImGui::TreeNode(subfolder.name.c_str())) {
2108 for (const auto& file : subfolder.files) {
2109 ImGui::TableNextRow();
2110 ImGui::TableNextColumn();
2111 if (ImGui::Selectable(file.c_str())) {
2112 ChangeActiveFile(absl::StrCat(current_folder_.name, "/",
2113 subfolder.name, "/", file));
2114 }
2115 ImGui::TableNextColumn();
2116 ImGui::Text("File");
2117 }
2118 ImGui::TreePop();
2119 } else {
2120 ImGui::TableNextColumn();
2121 ImGui::Text("Folder");
2122 }
2123 }
2124
2125 ImGui::EndTable();
2126 }
2127 }
2128 ImGui::EndChild();
2129}
2130
2131void AssemblyEditor::DrawFileMenu() {
2132 if (ImGui::BeginMenu("File")) {
2133 if (ImGui::MenuItem(ICON_MD_FILE_OPEN " Open", "Ctrl+O")) {
2135 if (!filename.empty()) {
2136 ChangeActiveFile(filename);
2137 }
2138 }
2139 if (ImGui::MenuItem(ICON_MD_SAVE " Save", "Ctrl+S")) {
2140 Save();
2141 }
2142 ImGui::EndMenu();
2143 }
2144}
2145
2146void AssemblyEditor::DrawEditMenu() {
2147 if (ImGui::BeginMenu("Edit")) {
2148 if (ImGui::MenuItem(ICON_MD_UNDO " Undo", "Ctrl+Z")) {
2149 GetActiveEditor()->Undo();
2150 }
2151 if (ImGui::MenuItem(ICON_MD_REDO " Redo", "Ctrl+Y")) {
2152 GetActiveEditor()->Redo();
2153 }
2154 ImGui::Separator();
2155 if (ImGui::MenuItem(ICON_MD_CONTENT_CUT " Cut", "Ctrl+X")) {
2156 GetActiveEditor()->Cut();
2157 }
2158 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy", "Ctrl+C")) {
2159 GetActiveEditor()->Copy();
2160 }
2161 if (ImGui::MenuItem(ICON_MD_CONTENT_PASTE " Paste", "Ctrl+V")) {
2162 GetActiveEditor()->Paste();
2163 }
2164 ImGui::Separator();
2165 if (ImGui::MenuItem(ICON_MD_SEARCH " Find", "Ctrl+F")) {
2166 // TODO: Implement this.
2167 }
2168 ImGui::EndMenu();
2169 }
2170}
2171
2172void AssemblyEditor::ChangeActiveFile(const std::string_view& filename) {
2173 if (filename.empty()) {
2174 return;
2175 }
2176
2177 // Check if file is already open
2178 for (int i = 0; i < active_files_.Size; ++i) {
2179 int file_id = active_files_[i];
2180 if (files_[file_id] == filename) {
2181 // Optional: Focus window
2182 active_file_id_ = file_id;
2183 current_file_ = util::GetFileName(files_[file_id]);
2184 return;
2185 }
2186 }
2187
2188 // Load file content using utility
2189 try {
2190 std::string content = util::LoadFile(std::string(filename));
2191 int new_file_id = files_.size();
2192 files_.push_back(std::string(filename));
2193 active_files_.push_back(new_file_id);
2194
2195 // Resize open_files_ if needed
2196 if (new_file_id >= open_files_.size()) {
2197 open_files_.resize(new_file_id + 1);
2198 }
2199
2200 open_files_[new_file_id].SetText(content);
2201 open_files_[new_file_id].SetLanguageDefinition(GetAssemblyLanguageDef());
2202 open_files_[new_file_id].SetPalette(TextEditor::GetDarkPalette());
2203 open_files_[new_file_id].SetShowWhitespaces(false);
2204 active_file_id_ = new_file_id;
2205 current_file_ = util::GetFileName(std::string(filename));
2206 } catch (const std::exception& ex) {
2207 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error opening file: %s\n",
2208 ex.what());
2209 }
2210}
2211
2212absl::Status AssemblyEditor::Cut() {
2213 GetActiveEditor()->Cut();
2214 return absl::OkStatus();
2215}
2216
2217absl::Status AssemblyEditor::Copy() {
2218 GetActiveEditor()->Copy();
2219 return absl::OkStatus();
2220}
2221
2222absl::Status AssemblyEditor::Paste() {
2223 GetActiveEditor()->Paste();
2224 return absl::OkStatus();
2225}
2226
2227absl::Status AssemblyEditor::Undo() {
2228 GetActiveEditor()->Undo();
2229 return absl::OkStatus();
2230}
2231
2232absl::Status AssemblyEditor::Redo() {
2233 GetActiveEditor()->Redo();
2234 return absl::OkStatus();
2235}
2236
2237#ifdef YAZE_WITH_Z3DK
2238core::Z3dkAssembleOptions AssemblyEditor::BuildZ3dkAssembleOptions() const {
2240 if (!dependencies_.project) {
2241 return options;
2242 }
2243
2244 const auto& z3dk = dependencies_.project->z3dk_settings;
2245 options.include_paths = z3dk.include_paths;
2246 options.defines = z3dk.defines;
2247 options.std_includes_path = z3dk.std_includes_path;
2248 options.std_defines_path = z3dk.std_defines_path;
2249 options.mapper = z3dk.mapper;
2250 options.rom_size = z3dk.rom_size;
2251 options.capture_nocash_symbols = (z3dk.symbols_format == "nocash");
2252 options.warn_unused_symbols = z3dk.warn_unused_symbols;
2253 options.warn_branch_outside_bank = z3dk.warn_branch_outside_bank;
2254 options.warn_unknown_width = z3dk.warn_unknown_width;
2255 options.warn_org_collision = z3dk.warn_org_collision;
2256 options.warn_unauthorized_hook = z3dk.warn_unauthorized_hook;
2257 options.warn_stack_balance = z3dk.warn_stack_balance;
2258 options.warn_hook_return = z3dk.warn_hook_return;
2259 for (const auto& range : z3dk.prohibited_memory_ranges) {
2260 options.prohibited_memory_ranges.push_back(
2261 {.start = range.start, .end = range.end, .reason = range.reason});
2262 }
2263
2264 auto append_unique = [&options](const std::string& path) {
2265 if (path.empty()) {
2266 return;
2267 }
2268 if (std::find(options.include_paths.begin(), options.include_paths.end(),
2269 path) == options.include_paths.end()) {
2270 options.include_paths.push_back(path);
2271 }
2272 };
2273
2274 append_unique(dependencies_.project->code_folder);
2275 if (HasActiveFile()) {
2276 append_unique(
2277 std::filesystem::path(files_[active_file_id_]).parent_path().string());
2278 }
2279
2280 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
2281 options.hooks_rom_path = rom_->filename();
2282 } else if (!z3dk.rom_path.empty()) {
2283 options.hooks_rom_path = z3dk.rom_path;
2284 }
2285
2286 return options;
2287}
2288
2289void AssemblyEditor::ExportZ3dkArtifacts(const core::AsarPatchResult& result,
2290 bool sync_mesen_symbols) {
2291 if (!dependencies_.project) {
2292 return;
2293 }
2294
2295 const auto& project = *dependencies_.project;
2296 const auto& configured = project.z3dk_settings.artifact_paths;
2297 const std::string symbols_path =
2298 configured.symbols_mlb.empty()
2299 ? project.GetZ3dkArtifactPath("symbols.mlb")
2300 : configured.symbols_mlb;
2301 const std::string sourcemap_path =
2302 configured.sourcemap_json.empty()
2303 ? project.GetZ3dkArtifactPath("sourcemap.json")
2304 : configured.sourcemap_json;
2305 const std::string annotations_path =
2306 configured.annotations_json.empty()
2307 ? project.GetZ3dkArtifactPath("annotations.json")
2308 : configured.annotations_json;
2309 const std::string hooks_path = configured.hooks_json.empty()
2310 ? project.GetZ3dkArtifactPath("hooks.json")
2311 : configured.hooks_json;
2312 const std::string lint_path = configured.lint_json.empty()
2313 ? project.GetZ3dkArtifactPath("lint.json")
2314 : configured.lint_json;
2315 auto write_artifact = [](const std::string& path,
2316 const std::string& content) {
2317 if (path.empty() || content.empty()) {
2318 return;
2319 }
2320 std::error_code ec;
2321 std::filesystem::create_directories(
2322 std::filesystem::path(path).parent_path(), ec);
2323 std::ofstream file(path, std::ios::binary | std::ios::trunc);
2324 if (!file.is_open()) {
2325 return;
2326 }
2327 file << content;
2328 };
2329
2330 write_artifact(symbols_path, result.symbols_mlb);
2331 write_artifact(sourcemap_path, result.sourcemap_json);
2332 write_artifact(annotations_path, result.annotations_json);
2333 write_artifact(hooks_path, result.hooks_json);
2334 write_artifact(lint_path, result.lint_json);
2335
2336 if (!sync_mesen_symbols || symbols_path.empty() ||
2337 result.symbols_mlb.empty()) {
2338 return;
2339 }
2340
2342 if (!client->IsConnected()) {
2343 client->Connect().IgnoreError();
2344 }
2345 if (!client->IsConnected()) {
2346 return;
2347 }
2348
2349 auto status = client->LoadSymbolsFile(symbols_path);
2350 if (!status.ok()) {
2351 if (dependencies_.toast_manager) {
2352 dependencies_.toast_manager->Show(
2353 "Failed to sync Mesen2 symbols: " + std::string(status.message()),
2354 ToastType::kWarning);
2355 }
2356 } else if (dependencies_.toast_manager) {
2357 dependencies_.toast_manager->Show(
2358 "Synced symbols to Mesen2 from " + symbols_path, ToastType::kSuccess);
2359 }
2360}
2361#endif
2362
2363// ============================================================================
2364// Asar Integration Implementation
2365// ============================================================================
2366
2367absl::Status AssemblyEditor::ValidateCurrentFile() {
2368 if (!HasActiveFile()) {
2369 return absl::FailedPreconditionError("No file is currently active");
2370 }
2371
2372 const std::string& file_path = files_[active_file_id_];
2373
2374#ifdef YAZE_WITH_Z3DK
2375 const auto options = BuildZ3dkAssembleOptions();
2376 // z3dk path: validate via scratch assemble; structured diagnostics are
2377 // populated natively. We reuse ApplyPatch against a throwaway buffer so
2378 // we get the full AsarPatchResult (including symbols) back.
2379 std::vector<uint8_t> scratch;
2380 auto result_or = z3dk_.ApplyPatch(file_path, scratch, options);
2381 if (!result_or.ok()) {
2382 last_errors_.clear();
2383 last_errors_.push_back(std::string(result_or.status().message()));
2384 last_warnings_.clear();
2385 last_diagnostics_.clear();
2386 TextEditor::ErrorMarkers empty_markers;
2387 GetActiveEditor()->SetErrorMarkers(empty_markers);
2388 return result_or.status();
2389 }
2390 UpdateErrorMarkers(*result_or);
2391 if (!result_or->success) {
2392 return absl::InternalError("Assembly validation failed");
2393 }
2394 ExportZ3dkArtifacts(*result_or, false);
2395 ClearErrorMarkers();
2396 return absl::OkStatus();
2397#else
2398 // Initialize Asar if not already done
2399 if (!asar_initialized_) {
2400 auto status = asar_.Initialize();
2401 if (!status.ok()) {
2402 return status;
2403 }
2404 asar_initialized_ = true;
2405 }
2406
2407 // Validate the assembly
2408 auto status = asar_.ValidateAssembly(file_path);
2409
2410 // Update error markers based on result
2411 if (!status.ok()) {
2412 // Get the error messages and show them
2413 last_errors_.clear();
2414 last_errors_.push_back(std::string(status.message()));
2415 // Parse and update error markers
2417 // Asar errors typically contain line numbers we can parse
2418 for (const auto& error : last_errors_) {
2419 // Simple heuristic: look for "line X" or ":X:" pattern
2420 size_t line_pos = error.find(':');
2421 if (line_pos != std::string::npos) {
2422 size_t num_start = line_pos + 1;
2423 size_t num_end = error.find(':', num_start);
2424 if (num_end != std::string::npos) {
2425 std::string line_str = error.substr(num_start, num_end - num_start);
2426 try {
2427 int line = std::stoi(line_str);
2428 markers[line] = error;
2429 } catch (...) {
2430 // Not a line number, skip
2431 }
2432 }
2433 }
2434 }
2435 GetActiveEditor()->SetErrorMarkers(markers);
2436 return status;
2437 }
2438
2439 // Clear any previous error markers
2440 ClearErrorMarkers();
2441 return absl::OkStatus();
2442#endif
2443}
2444
2445absl::Status AssemblyEditor::ApplyPatchToRom() {
2446 if (!rom_ || !rom_->is_loaded()) {
2447 return absl::FailedPreconditionError("No ROM is loaded");
2448 }
2449
2450 if (!HasActiveFile()) {
2451 return absl::FailedPreconditionError("No file is currently active");
2452 }
2453
2454 const std::string& file_path = files_[active_file_id_];
2455 std::vector<uint8_t> rom_data = rom_->vector();
2456
2457#ifdef YAZE_WITH_Z3DK
2458 const auto options = BuildZ3dkAssembleOptions();
2459 auto result = z3dk_.ApplyPatch(file_path, rom_data, options);
2460 if (!result.ok()) {
2461 last_errors_.clear();
2462 last_errors_.push_back(std::string(result.status().message()));
2463 last_warnings_.clear();
2464 last_diagnostics_.clear();
2465 TextEditor::ErrorMarkers empty_markers;
2466 GetActiveEditor()->SetErrorMarkers(empty_markers);
2467 return result.status();
2468 }
2469 UpdateErrorMarkers(*result);
2470 if (!result->success) {
2471 return absl::InternalError("Patch application failed");
2472 }
2473 rom_->LoadFromData(rom_data);
2474 symbols_ = z3dk_.GetSymbolTable();
2475 ExportZ3dkArtifacts(*result, true);
2476 ClearErrorMarkers();
2477 return absl::OkStatus();
2478#else
2479 // Initialize Asar if not already done
2480 if (!asar_initialized_) {
2481 auto status = asar_.Initialize();
2482 if (!status.ok()) {
2483 return status;
2484 }
2485 asar_initialized_ = true;
2486 }
2487
2488 // Apply the patch
2489 auto result = asar_.ApplyPatch(file_path, rom_data);
2490
2491 if (!result.ok()) {
2492 UpdateErrorMarkers(*result);
2493 return result.status();
2494 }
2495
2496 if (result->success) {
2497 // Update the ROM with the patched data
2498 rom_->LoadFromData(rom_data);
2499
2500 // Store symbols for lookup
2501 symbols_ = asar_.GetSymbolTable();
2502 last_errors_.clear();
2503 last_warnings_ = result->warnings;
2504
2505 // Clear error markers
2506 ClearErrorMarkers();
2507
2508 return absl::OkStatus();
2509 } else {
2510 UpdateErrorMarkers(*result);
2511 return absl::InternalError("Patch application failed");
2512 }
2513#endif
2514}
2515
2516void AssemblyEditor::UpdateErrorMarkers(const core::AsarPatchResult& result) {
2517 last_errors_ = result.errors;
2518 last_warnings_ = result.warnings;
2519 last_diagnostics_ = result.structured_diagnostics;
2520
2521 if (!HasActiveFile()) {
2522 return;
2523 }
2524
2526
2527 // Prefer the native structured diagnostics when available — they carry
2528 // line numbers directly, no string parsing required. Fall back to the
2529 // flat error strings only when the backend did not populate structured
2530 // diagnostics (e.g., pre-M3 Asar path with a malformed error line).
2531 if (!result.structured_diagnostics.empty()) {
2532 for (const auto& d : result.structured_diagnostics) {
2533 if (d.line > 0 &&
2535 markers[d.line] = d.message;
2536 }
2537 }
2538 } else {
2539 // Legacy fallback: parse "file:line:..." out of the flat strings.
2540 for (const auto& error : result.errors) {
2541 try {
2542 size_t first_colon = error.find(':');
2543 if (first_colon != std::string::npos) {
2544 size_t second_colon = error.find(':', first_colon + 1);
2545 if (second_colon != std::string::npos) {
2546 std::string line_str =
2547 error.substr(first_colon + 1, second_colon - (first_colon + 1));
2548 int line = std::stoi(line_str);
2549 markers[line] = error;
2550 }
2551 }
2552 } catch (...) {
2553 // Ignore parsing errors
2554 }
2555 }
2556 }
2557
2558 GetActiveEditor()->SetErrorMarkers(markers);
2559}
2560
2561void AssemblyEditor::ClearErrorMarkers() {
2562 last_errors_.clear();
2563 last_diagnostics_.clear();
2564
2565 if (!HasActiveFile()) {
2566 return;
2567 }
2568
2569 TextEditor::ErrorMarkers empty_markers;
2570 GetActiveEditor()->SetErrorMarkers(empty_markers);
2571}
2572
2573void AssemblyEditor::DrawAssembleMenu() {
2574 if (ImGui::BeginMenu("Assemble")) {
2575 bool has_active_file = HasActiveFile();
2576 bool has_rom = (rom_ && rom_->is_loaded());
2577
2578 if (ImGui::MenuItem(ICON_MD_CHECK_CIRCLE " Validate", "Ctrl+B", false,
2579 has_active_file)) {
2580 auto status = ValidateCurrentFile();
2581 if (status.ok()) {
2582 // Show success notification (could add toast notification here)
2583 }
2584 }
2585
2586 if (ImGui::MenuItem(ICON_MD_BUILD " Apply to ROM", "Ctrl+Shift+B", false,
2587 has_active_file && has_rom)) {
2588 auto status = ApplyPatchToRom();
2589 if (status.ok()) {
2590 // Show success notification
2591 }
2592 }
2593
2594 if (ImGui::MenuItem(ICON_MD_FILE_UPLOAD " Load External Symbols", nullptr,
2595 false)) {
2596 if (dependencies_.project) {
2597 std::string sym_file = dependencies_.project->symbols_filename;
2598 if (dependencies_.project->HasZ3dkConfig() &&
2599 !dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb
2600 .empty()) {
2601 sym_file =
2602 dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb;
2603 } else if (sym_file.empty() || !std::filesystem::exists(sym_file)) {
2604 sym_file = dependencies_.project->GetZ3dkArtifactPath("symbols.mlb");
2605 }
2606 if (!sym_file.empty()) {
2607 std::string abs_path = sym_file;
2608 if (!std::filesystem::path(abs_path).is_absolute()) {
2609 abs_path = dependencies_.project->GetAbsolutePath(sym_file);
2610 }
2611 auto status = asar_.LoadSymbolsFromFile(abs_path);
2612 if (status.ok()) {
2613 // Copy symbols to local map for display
2614 symbols_ = asar_.GetSymbolTable();
2615 if (dependencies_.toast_manager) {
2616 dependencies_.toast_manager->Show(
2617 "Successfully loaded external symbols from " + sym_file,
2618 ToastType::kSuccess);
2619 }
2620 } else {
2621 if (dependencies_.toast_manager) {
2622 dependencies_.toast_manager->Show(
2623 "Failed to load symbols: " + std::string(status.message()),
2624 ToastType::kError);
2625 }
2626 }
2627 } else {
2628 if (dependencies_.toast_manager) {
2629 dependencies_.toast_manager->Show(
2630 "Project does not specify a symbols file.",
2631 ToastType::kWarning);
2632 }
2633 }
2634 }
2635 }
2636
2637 ImGui::Separator();
2638
2639 if (ImGui::MenuItem(ICON_MD_LIST " Show Symbols", nullptr,
2640 show_symbol_panel_)) {
2641 show_symbol_panel_ = !show_symbol_panel_;
2642 }
2643
2644 ImGui::Separator();
2645
2646 // Show last error/warning count
2647 ImGui::TextDisabled("Errors: %zu, Warnings: %zu", last_errors_.size(),
2648 last_warnings_.size());
2649
2650 ImGui::EndMenu();
2651 }
2652
2653 if (ImGui::BeginMenu("Version")) {
2654 bool has_version_manager = (dependencies_.version_manager != nullptr);
2655 if (ImGui::MenuItem(ICON_MD_CAMERA_ALT " Create Snapshot", nullptr, false,
2656 has_version_manager)) {
2657 if (has_version_manager) {
2658 ImGui::OpenPopup("Create Snapshot");
2659 }
2660 }
2661
2662 // Snapshot Dialog
2663 if (ImGui::BeginPopupModal("Create Snapshot", nullptr,
2664 ImGuiWindowFlags_AlwaysAutoResize)) {
2665 static char message[256] = "";
2666 ImGui::InputText("Message", message, sizeof(message));
2667
2668 if (ImGui::Button("Create", ImVec2(120, 0))) {
2669 auto result = dependencies_.version_manager->CreateSnapshot(message);
2670 if (result.ok() && result->success) {
2671 if (dependencies_.toast_manager) {
2672 dependencies_.toast_manager->Show(
2673 "Snapshot Created: " + result->commit_hash,
2674 ToastType::kSuccess);
2675 }
2676 } else {
2677 if (dependencies_.toast_manager) {
2678 std::string err = result.ok()
2679 ? result->message
2680 : std::string(result.status().message());
2681 dependencies_.toast_manager->Show("Snapshot Failed: " + err,
2682 ToastType::kError);
2683 }
2684 }
2685 ImGui::CloseCurrentPopup();
2686 message[0] = '\0'; // Reset
2687 }
2688 ImGui::SameLine();
2689 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
2690 ImGui::CloseCurrentPopup();
2691 }
2692 ImGui::EndPopup();
2693 }
2694
2695 ImGui::EndMenu();
2696 }
2697}
2698
2699void AssemblyEditor::DrawSymbolPanel() {
2700 if (!show_symbol_panel_) {
2701 return;
2702 }
2703
2704 ImGui::SetNextWindowSize(ImVec2(350, 400), ImGuiCond_FirstUseEver);
2705 if (ImGui::Begin("Symbols", &show_symbol_panel_)) {
2706 if (symbols_.empty()) {
2707 ImGui::TextDisabled("No symbols loaded.");
2708 ImGui::TextDisabled("Apply a patch to load symbols.");
2709 } else {
2710 // Search filter
2711 static char filter[256] = "";
2712 ImGui::InputTextWithHint("##symbol_filter", "Filter symbols...", filter,
2713 sizeof(filter));
2714
2715 ImGui::Separator();
2716
2717 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), true)) {
2718 for (const auto& [name, symbol] : symbols_) {
2719 // Apply filter
2720 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
2721 continue;
2722 }
2723
2724 ImGui::PushID(name.c_str());
2725 if (ImGui::Selectable(name.c_str())) {
2726 // Could jump to symbol definition if line info is available
2727 // For now, just select it
2728 }
2729 ImGui::SameLine(200);
2730 ImGui::TextDisabled("$%06X", symbol.address);
2731 ImGui::PopID();
2732 }
2733 }
2734 ImGui::EndChild();
2735 }
2736 }
2737 ImGui::End();
2738}
2739
2740} // namespace yaze::editor
static const Palette & GetDarkPalette()
Coordinates GetCursorPosition() const
int GetTotalLines() const
bool IsOverwrite() const
void Render(const char *aTitle, const ImVec2 &aSize=ImVec2(), bool aBorder=false)
std::map< int, std::string > ErrorMarkers
const LanguageDefinition & GetLanguageDefinition() const
bool CanUndo() const
void SetLanguageDefinition(const LanguageDefinition &aLanguageDef)
bool is_array() const
Definition json.h:58
bool empty() const
Definition json.h:62
bool contains(const std::string &) const
Definition json.h:53
auto filename() const
Definition rom.h:145
bool is_loaded() const
Definition rom.h:132
Provides the AI agent with structured information about the project.
absl::Status Run(const std::vector< std::string > &args, Rom *rom_context, std::string *captured_output=nullptr)
Execute the command.
virtual void SetAsarWrapper(core::AsarWrapper *asar_wrapper)
Set the AsarWrapper context. Default implementation does nothing, override if tool needs Asar access.
virtual void SetProjectContext(project::YazeProject *project)
Set the YazeProject context. Default implementation does nothing, override if tool needs project info...
virtual void SetAssemblySymbolTable(const std::map< std::string, core::AsarSymbol > *table)
Optional Asar symbol table for assembly-aware tools.
TextEditor::Coordinates active_cursor_position() const
std::vector< std::string > last_warnings_
std::map< std::string, core::AsarSymbol > symbols_
std::optional< uint32_t > CurrentDisassemblyBank() const
std::vector< TextEditor > open_files_
absl::Status Load() override
std::string ResolveZ3DisasmOutputDir() const
std::string BuildProjectGraphLookupQuery(uint32_t address) const
std::vector< Z3DisasmHookJump > z3disasm_hook_jumps_
void ChangeActiveFile(const std::string_view &filename)
absl::Status JumpToReference(const std::string &reference)
absl::flat_hash_map< std::string, AsmSymbolLocation > symbol_jump_cache_
std::string ResolveZ3DisasmRomPath() const
std::vector< core::AssemblyDiagnostic > last_diagnostics_
void OpenFolder(const std::string &folder_path)
absl::flat_hash_set< std::string > symbol_jump_negative_cache_
std::string active_file_path() const
std::string BuildProjectGraphBankQuery() const
std::string ResolveZ3DisasmCommand() const
std::vector< std::string > files_
absl::Status RunProjectGraphQueryInDrawer(const std::vector< std::string > &args, const std::string &title)
std::vector< std::string > last_errors_
std::vector< Z3DisasmSourceJump > z3disasm_source_jumps_
absl::Status JumpToSymbolDefinition(const std::string &symbol)
The EditorManager controls the main editor window and manages the various editor classes.
EditorDependencies dependencies_
Definition editor.h:316
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
65816 CPU disassembler for debugging and ROM hacking
std::vector< DisassembledInstruction > DisassembleRange(uint32_t start_address, size_t count, MemoryReader read_byte, bool m_flag=true, bool x_flag=true) const
Disassemble multiple instructions.
void SetSymbolResolver(SymbolResolver resolver)
Set optional symbol resolver for address lookups.
static std::shared_ptr< MesenSocketClient > GetOrCreate()
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_CANCEL
Definition icons.h:364
#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_REFRESH
Definition icons.h:1572
#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)
bool LoadJsonFile(const std::string &path, Json *out)
std::optional< std::filesystem::path > FindUpwardPath(const std::filesystem::path &start, const std::filesystem::path &relative)
std::optional< AsmFileSymbolRef > ParseAsmFileSymbolRef(const std::string &reference)
std::optional< AsmSymbolLocation > FindLabelInFolder(const std::filesystem::path &root, const std::string &label)
std::optional< uint32_t > ParseHexAddress(const std::string &text)
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< int > ParseGeneratedBankIndex(const std::string &path)
std::optional< std::filesystem::path > FindAsmFileInFolder(const std::filesystem::path &root, const std::string &file_ref)
bool IsAssemblyLikeFile(const std::filesystem::path &path)
bool IsGeneratedBankFile(const std::filesystem::path &path)
std::string ShellQuote(const std::string &value)
Editors are the view controllers for the application.
void DrawDiagnosticsPanel(std::span< const core::AssemblyDiagnostic > diagnostics, const DiagnosticsPanelCallbacks &callbacks)
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
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
static Coordinates Invalid()
Definition text_editor.h:69
TokenRegexStrings mTokenRegexStrings
Asar patch result information.
std::vector< AssemblyDiagnostic > structured_diagnostics
std::vector< std::string > errors
std::vector< std::string > warnings
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
std::vector< std::string > include_paths
std::vector< std::pair< std::string, std::string > > defines
std::function< void(const std::string &file, int line, int column) on_diagnostic_activated)
project::YazeProject * project
Definition editor.h:168
WorkspaceWindowManager * window_manager
Definition editor.h:176
std::vector< FolderItem > subfolders
std::vector< std::string > files
std::function< void(const std::string &) on_open_reference)
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:1398
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1319
Z3dkSettings z3dk_settings
Definition project.h:207