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