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 (ImGui::Button(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::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s",
987 current_folder_.name.c_str());
988 ImGui::Separator();
989
990 // File tree
992}
993
995 if (symbols_.empty()) {
996 ImGui::TextDisabled(tr("No symbols loaded."));
997 ImGui::Spacing();
998 ImGui::TextWrapped(
999 tr("Apply a patch or load external symbols to populate this list."));
1000 return;
1001 }
1002
1003 // Search filter
1004 static char filter[256] = "";
1005 ImGui::SetNextItemWidth(-1);
1006 ImGui::InputTextWithHint("##symbol_filter",
1007 ICON_MD_SEARCH " Filter symbols...", filter,
1008 sizeof(filter));
1009 ImGui::Separator();
1010
1011 // Symbol list
1012 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), false)) {
1013 for (const auto& [name, symbol] : symbols_) {
1014 // Apply filter
1015 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
1016 continue;
1017 }
1018
1019 ImGui::PushID(name.c_str());
1020 if (ImGui::Selectable(name.c_str())) {
1021 // Could jump to symbol definition if line info is available
1022 }
1023 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
1024 ImGui::TextDisabled("$%06X", symbol.address);
1025 ImGui::PopID();
1026 }
1027 }
1028 ImGui::EndChild();
1029}
1030
1032 // Error/warning counts
1033 ImGui::Text(tr("Errors: %zu Warnings: %zu"), last_errors_.size(),
1034 last_warnings_.size());
1035 ImGui::Separator();
1036
1037 // Build buttons
1038 bool has_active_file = HasActiveFile();
1039 bool has_rom = (rom_ && rom_->is_loaded());
1040
1041 if (ImGui::Button(ICON_MD_CHECK_CIRCLE " Validate", ImVec2(120, 0))) {
1042 if (has_active_file) {
1043 auto status = ValidateCurrentFile();
1044 if (status.ok() && dependencies_.toast_manager) {
1045 dependencies_.toast_manager->Show("Validation passed!",
1047 }
1048 }
1049 }
1050 ImGui::SameLine();
1051 bool apply_disabled = !has_rom || !has_active_file;
1052 ImGui::BeginDisabled(apply_disabled);
1053 if (ImGui::Button(ICON_MD_BUILD " Apply to ROM", ImVec2(140, 0))) {
1054 auto status = ApplyPatchToRom();
1055 if (status.ok() && dependencies_.toast_manager) {
1057 }
1058 }
1059 ImGui::EndDisabled();
1060 if (apply_disabled &&
1061 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1062 if (!has_rom)
1063 ImGui::SetTooltip(tr("Load a ROM first"));
1064 else
1065 ImGui::SetTooltip(tr("Open an assembly file first"));
1066 }
1067
1068 ImGui::Separator();
1069
1070 // Output log
1071 if (ImGui::BeginChild("##build_log", ImVec2(0, 0), true)) {
1072 if (!last_diagnostics_.empty()) {
1073 // Structured path — when the backend (z3dk, or future enriched Asar)
1074 // gave us file:line diagnostics, render via the dedicated panel.
1075 DiagnosticsPanelCallbacks callbacks;
1076 callbacks.on_diagnostic_activated = [this](const std::string& file,
1077 int line, int column) {
1078 if (!file.empty()) {
1079 std::string reference = file;
1080 if (line > 0) {
1081 reference = absl::StrCat(reference, ":", line);
1082 if (column > 0) {
1083 reference = absl::StrCat(reference, ":", column);
1084 }
1085 }
1086 auto status = JumpToReference(reference);
1087 if (!status.ok() && dependencies_.toast_manager) {
1089 "Failed to open diagnostic location: " +
1090 std::string(status.message()),
1092 }
1093 return;
1094 }
1095 // TextEditor coords are 0-based; diagnostics are 1-based.
1096 if (auto* editor = GetActiveEditor()) {
1097 TextEditor::Coordinates coords(line > 0 ? line - 1 : 0,
1098 column > 0 ? column - 1 : 0);
1099 editor->SetCursorPosition(coords);
1100 }
1101 };
1103 } else {
1104 // Legacy flat-string fallback (vanilla Asar without structured output).
1105 for (const auto& error : last_errors_) {
1106 gui::StyleColorGuard err_guard(ImGuiCol_Text,
1107 ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
1108 ImGui::TextWrapped("%s %s", ICON_MD_ERROR, error.c_str());
1109 }
1110 for (const auto& warning : last_warnings_) {
1111 gui::StyleColorGuard warn_guard(ImGuiCol_Text,
1112 ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
1113 ImGui::TextWrapped("%s %s", ICON_MD_WARNING, warning.c_str());
1114 }
1115 if (last_errors_.empty() && last_warnings_.empty()) {
1116 ImGui::TextDisabled(tr("No build output"));
1117 }
1118 }
1119 }
1120 ImGui::EndChild();
1121}
1122
1123std::optional<uint32_t> AssemblyEditor::CurrentDisassemblyBank() const {
1124 if (auto symbol_it = symbols_.find(disasm_query_);
1125 symbol_it != symbols_.end()) {
1126 return (symbol_it->second.address >> 16) & 0xFFu;
1127 }
1128 auto parsed = ParseHexAddress(disasm_query_);
1129 if (!parsed.has_value()) {
1130 return std::nullopt;
1131 }
1132 return (*parsed >> 16) & 0xFFu;
1133}
1134
1136 if (const char* env = std::getenv("Z3DISASM_BIN"); env && env[0] != '\0') {
1137 return std::string(env);
1138 }
1139
1140 std::error_code ec;
1141 const auto cwd = std::filesystem::current_path(ec);
1142 if (!ec) {
1143 if (auto script =
1144 FindUpwardPath(cwd, std::filesystem::path("scripts") / "z3disasm");
1145 script.has_value()) {
1146 return script->string();
1147 }
1148 }
1149
1150 return "z3disasm";
1151}
1152
1154 if (!z3disasm_output_dir_.empty()) {
1155 return z3disasm_output_dir_;
1156 }
1157 if (dependencies_.project) {
1158 return dependencies_.project->GetZ3dkArtifactPath("z3disasm");
1159 }
1160 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
1161 return (std::filesystem::path(rom_->filename()).parent_path() / "z3disasm")
1162 .string();
1163 }
1164 std::error_code ec;
1165 return (std::filesystem::current_path(ec) / "z3disasm").string();
1166}
1167
1169 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
1170 return rom_->filename();
1171 }
1172 if (dependencies_.project) {
1175 }
1177 }
1178 return {};
1179}
1180
1182 if (auto bank = ParseGeneratedBankIndex(z3disasm_selected_path_);
1183 bank.has_value()) {
1184 return *bank;
1185 }
1186 return -1;
1187}
1188
1190 const int bank = SelectedZ3DisasmBankIndex();
1191 if (bank < 0) {
1192 return {};
1193 }
1194 return absl::StrFormat("project-graph --query=bank --bank=%02X --format=json",
1195 bank);
1196}
1197
1199 uint32_t address) const {
1200 return absl::StrFormat(
1201 "project-graph --query=lookup --address=%06X --format=json", address);
1202}
1203
1205 const std::vector<std::string>& args, const std::string& title) {
1206 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
1207 if (!editor_manager || !editor_manager->right_drawer_manager()) {
1208 return absl::FailedPreconditionError(
1209 "Right drawer manager is unavailable for project-graph results");
1210 }
1211
1213 if (dependencies_.project) {
1215 }
1217 tool.SetAsarWrapper(&asar_);
1218
1219 std::string output;
1220 auto status = tool.Run(args, nullptr, &output);
1221 if (!status.ok()) {
1222 return status;
1223 }
1224
1226 actions.on_open_reference = [this](const std::string& reference) {
1227 JumpToReference(reference).IgnoreError();
1228 };
1229 actions.on_open_address = [this](uint32_t address) {
1230 disasm_query_ = absl::StrFormat("0x%06X", address);
1231 NavigateDisassemblyQuery().IgnoreError();
1232 };
1233 actions.on_open_lookup = [this](uint32_t address) {
1235 {"--query=lookup", absl::StrFormat("--address=%06X", address),
1236 "--format=json"},
1237 absl::StrFormat("Project Graph Lookup $%06X", address))
1238 .IgnoreError();
1239 };
1240
1241 editor_manager->right_drawer_manager()->SetToolOutput(
1242 title, absl::StrJoin(args, " "), output, std::move(actions));
1243 editor_manager->right_drawer_manager()->OpenDrawer(
1245 return absl::OkStatus();
1246}
1247
1249 z3disasm_source_jumps_.clear();
1250 z3disasm_hook_jumps_.clear();
1251
1252 if (!dependencies_.project) {
1253 return;
1254 }
1255
1256 const int selected_bank = SelectedZ3DisasmBankIndex();
1257 if (selected_bank < 0) {
1258 return;
1259 }
1260
1261 const auto& project = *dependencies_.project;
1262 Json sourcemap;
1263 const std::string sourcemap_path =
1266 : project.GetZ3dkArtifactPath("sourcemap.json");
1267 if (LoadJsonFile(sourcemap_path, &sourcemap) &&
1268 sourcemap.contains("entries") && sourcemap["entries"].is_array() &&
1269 sourcemap.contains("files") && sourcemap["files"].is_array()) {
1270 std::map<int, std::string> files_by_id;
1271 for (const auto& file : sourcemap["files"]) {
1272 files_by_id[file.value("id", -1)] = file.value("path", "");
1273 }
1274
1275 for (const auto& entry : sourcemap["entries"]) {
1276 const std::string address_str = entry.value("address", "0x0");
1277 auto address = ParseHexAddress(address_str);
1278 if (!address.has_value() ||
1279 NormalizeLoRomBankIndex(*address) != selected_bank) {
1280 continue;
1281 }
1282
1283 Z3DisasmSourceJump jump;
1284 jump.address = *address;
1285 jump.line = entry.value("line", 0);
1286 jump.file = files_by_id[entry.value("file_id", -1)];
1287 if (!jump.file.empty()) {
1288 z3disasm_source_jumps_.push_back(std::move(jump));
1289 }
1290 }
1291 std::sort(z3disasm_source_jumps_.begin(), z3disasm_source_jumps_.end(),
1292 [](const Z3DisasmSourceJump& lhs, const Z3DisasmSourceJump& rhs) {
1293 if (lhs.address != rhs.address) {
1294 return lhs.address < rhs.address;
1295 }
1296 if (lhs.file != rhs.file) {
1297 return lhs.file < rhs.file;
1298 }
1299 return lhs.line < rhs.line;
1300 });
1301 }
1302
1303 Json hooks;
1304 const std::string hooks_path =
1305 !project.z3dk_settings.artifact_paths.hooks_json.empty()
1306 ? project.z3dk_settings.artifact_paths.hooks_json
1307 : project.GetZ3dkArtifactPath("hooks.json");
1308 if (LoadJsonFile(hooks_path, &hooks) && hooks.contains("hooks") &&
1309 hooks["hooks"].is_array()) {
1310 for (const auto& hook : hooks["hooks"]) {
1311 const std::string address_str = hook.value("address", "0x0");
1312 auto address = ParseHexAddress(address_str);
1313 if (!address.has_value() ||
1314 NormalizeLoRomBankIndex(*address) != selected_bank) {
1315 continue;
1316 }
1317
1318 Z3DisasmHookJump jump;
1319 jump.address = *address;
1320 jump.size = hook.value("size", 0);
1321 jump.kind = hook.value("kind", "patch");
1322 jump.name = hook.value("name", "");
1323 jump.source = hook.value("source", "");
1324 z3disasm_hook_jumps_.push_back(std::move(jump));
1325 }
1326 std::sort(z3disasm_hook_jumps_.begin(), z3disasm_hook_jumps_.end(),
1327 [](const Z3DisasmHookJump& lhs, const Z3DisasmHookJump& rhs) {
1328 if (lhs.address != rhs.address) {
1329 return lhs.address < rhs.address;
1330 }
1331 return lhs.name < rhs.name;
1332 });
1333 }
1334}
1335
1336void AssemblyEditor::LoadSelectedZ3DisassemblyFile() {
1337 z3disasm_selected_contents_.clear();
1338 z3disasm_selected_path_.clear();
1339 z3disasm_source_jumps_.clear();
1340 z3disasm_hook_jumps_.clear();
1341 if (z3disasm_selected_index_ < 0 ||
1342 z3disasm_selected_index_ >= static_cast<int>(z3disasm_files_.size())) {
1343 return;
1344 }
1345
1346 z3disasm_selected_path_ = z3disasm_files_[z3disasm_selected_index_];
1347 std::ifstream file(z3disasm_selected_path_);
1348 if (!file.is_open()) {
1349 z3disasm_status_ = absl::StrCat("Failed to open ", z3disasm_selected_path_);
1350 return;
1351 }
1352 z3disasm_selected_contents_.assign(std::istreambuf_iterator<char>(file),
1353 std::istreambuf_iterator<char>());
1354 RefreshSelectedZ3DisassemblyMetadata();
1355}
1356
1357void AssemblyEditor::RefreshZ3DisassemblyFiles() {
1358 z3disasm_files_.clear();
1359 const std::filesystem::path output_dir(ResolveZ3DisasmOutputDir());
1360 std::error_code ec;
1361 if (!std::filesystem::exists(output_dir, ec) || ec) {
1362 z3disasm_selected_index_ = -1;
1363 z3disasm_selected_path_.clear();
1364 z3disasm_selected_contents_.clear();
1365 z3disasm_source_jumps_.clear();
1366 z3disasm_hook_jumps_.clear();
1367 return;
1368 }
1369
1370 std::string previous_selection = z3disasm_selected_path_;
1371 for (const auto& entry :
1372 std::filesystem::directory_iterator(output_dir, ec)) {
1373 if (ec) {
1374 break;
1375 }
1376 if (!entry.is_regular_file()) {
1377 continue;
1378 }
1379 if (IsGeneratedBankFile(entry.path())) {
1380 z3disasm_files_.push_back(entry.path().string());
1381 }
1382 }
1383 std::sort(z3disasm_files_.begin(), z3disasm_files_.end());
1384
1385 if (z3disasm_files_.empty()) {
1386 z3disasm_selected_index_ = -1;
1387 z3disasm_selected_path_.clear();
1388 z3disasm_selected_contents_.clear();
1389 z3disasm_source_jumps_.clear();
1390 z3disasm_hook_jumps_.clear();
1391 return;
1392 }
1393
1394 z3disasm_selected_index_ = 0;
1395 if (!previous_selection.empty()) {
1396 const auto it = std::find(z3disasm_files_.begin(), z3disasm_files_.end(),
1397 previous_selection);
1398 if (it != z3disasm_files_.end()) {
1399 z3disasm_selected_index_ =
1400 static_cast<int>(std::distance(z3disasm_files_.begin(), it));
1401 }
1402 }
1403 LoadSelectedZ3DisassemblyFile();
1404}
1405
1406void AssemblyEditor::PollZ3DisassemblyTask() {
1407 const auto snapshot = z3disasm_task_.GetSnapshot();
1408 if (!snapshot.started) {
1409 return;
1410 }
1411 if (snapshot.running) {
1412 z3disasm_status_ =
1413 absl::StrCat("z3disasm running...\n", snapshot.output_tail);
1414 return;
1415 }
1416 if (z3disasm_task_acknowledged_ || !snapshot.finished) {
1417 return;
1418 }
1419
1420 z3disasm_task_acknowledged_ = true;
1421 if (snapshot.status.ok()) {
1422 RefreshZ3DisassemblyFiles();
1423 z3disasm_status_ = absl::StrCat(
1424 "z3disasm finished.",
1425 z3disasm_files_.empty()
1426 ? std::string(" No bank files were generated.")
1427 : absl::StrFormat(" Loaded %d bank file(s).",
1428 static_cast<int>(z3disasm_files_.size())));
1429 } else {
1430 z3disasm_status_ =
1431 absl::StrCat("z3disasm failed: ", snapshot.status.message(), "\n",
1432 snapshot.output_tail);
1433 }
1434 z3disasm_task_.Wait().IgnoreError();
1435}
1436
1437absl::Status AssemblyEditor::GenerateZ3Disassembly() {
1438 if (const auto snapshot = z3disasm_task_.GetSnapshot(); snapshot.running) {
1439 return absl::FailedPreconditionError("z3disasm is already running");
1440 }
1441
1442 const std::string rom_path = ResolveZ3DisasmRomPath();
1443 if (rom_path.empty()) {
1444 return absl::FailedPreconditionError(
1445 "No ROM path is available for z3disasm");
1446 }
1447
1448 const std::string command_path = ResolveZ3DisasmCommand();
1449 const std::string output_dir = ResolveZ3DisasmOutputDir();
1450 z3disasm_output_dir_ = output_dir;
1451
1452 std::error_code ec;
1453 std::filesystem::create_directories(output_dir, ec);
1454 for (const auto& entry :
1455 std::filesystem::directory_iterator(output_dir, ec)) {
1456 if (!ec && entry.is_regular_file() && IsGeneratedBankFile(entry.path())) {
1457 std::filesystem::remove(entry.path(), ec);
1458 }
1459 }
1460
1461 std::string command = ShellQuote(command_path);
1462 command += " --rom ";
1463 command += ShellQuote(rom_path);
1464 command += " --out ";
1465 command += ShellQuote(output_dir);
1466
1467 if (!z3disasm_all_banks_) {
1468 command += absl::StrFormat(" --bank-start %02X --bank-end %02X",
1469 std::clamp(z3disasm_bank_start_, 0, 0xFF),
1470 std::clamp(z3disasm_bank_end_, 0, 0xFF));
1471 }
1472
1473 if (dependencies_.project) {
1474 const std::string symbols_path =
1475 !dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb.empty()
1476 ? dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb
1477 : dependencies_.project->GetZ3dkArtifactPath("symbols.mlb");
1478 if (!symbols_path.empty() && std::filesystem::exists(symbols_path)) {
1479 command += " --symbols ";
1480 command += ShellQuote(symbols_path);
1481 }
1482
1483 const std::string hooks_path =
1484 !dependencies_.project->z3dk_settings.artifact_paths.hooks_json.empty()
1485 ? dependencies_.project->z3dk_settings.artifact_paths.hooks_json
1486 : dependencies_.project->GetZ3dkArtifactPath("hooks.json");
1487 if (!hooks_path.empty() && std::filesystem::exists(hooks_path)) {
1488 command += " --hooks ";
1489 command += ShellQuote(hooks_path);
1490 }
1491 }
1492
1493 z3disasm_task_acknowledged_ = false;
1494 z3disasm_selected_index_ = -1;
1495 z3disasm_selected_path_.clear();
1496 z3disasm_selected_contents_.clear();
1497 z3disasm_source_jumps_.clear();
1498 z3disasm_hook_jumps_.clear();
1499 z3disasm_files_.clear();
1500 z3disasm_status_ = "Launching z3disasm...";
1501 return z3disasm_task_.Start(command,
1502 std::filesystem::current_path().string());
1503}
1504
1505absl::Status AssemblyEditor::NavigateDisassemblyQuery() {
1506 auto symbol_it = symbols_.find(disasm_query_);
1507 if (symbol_it != symbols_.end()) {
1508 disasm_query_ = absl::StrCat("0x", absl::Hex(symbol_it->second.address));
1509 disasm_status_ = absl::StrCat("Resolved symbol ", symbol_it->first);
1510 return absl::OkStatus();
1511 }
1512
1513 auto parsed = ParseHexAddress(disasm_query_);
1514 if (!parsed.has_value()) {
1515 return absl::InvalidArgumentError("Enter a SNES address or known symbol");
1516 }
1517
1518 disasm_query_ = absl::StrCat("0x", absl::Hex(*parsed));
1519 disasm_status_ = absl::StrFormat("Showing disassembly at $%06X", *parsed);
1520 return absl::OkStatus();
1521}
1522
1523void AssemblyEditor::DrawDisassemblyContent() {
1524 PollZ3DisassemblyTask();
1525
1526 if (z3disasm_output_dir_.empty()) {
1527 z3disasm_output_dir_ = ResolveZ3DisasmOutputDir();
1528 }
1529 if (z3disasm_files_.empty()) {
1530 RefreshZ3DisassemblyFiles();
1531 }
1532
1533 ImGui::TextDisabled(
1534 tr("z3disasm-backed bank browser for generated `bank_XX.asm` files."));
1535 const std::string rom_path = ResolveZ3DisasmRomPath();
1536 ImGui::TextWrapped(tr("ROM: %s"),
1537 rom_path.empty() ? "<unavailable>" : rom_path.c_str());
1538 ImGui::TextWrapped(tr("Output: %s"), z3disasm_output_dir_.c_str());
1539
1540 ImGui::Checkbox(tr("All banks"), &z3disasm_all_banks_);
1541 if (!z3disasm_all_banks_) {
1542 ImGui::SameLine();
1543 ImGui::SetNextItemWidth(70.0f);
1544 ImGui::InputInt(tr("Start"), &z3disasm_bank_start_);
1545 ImGui::SameLine();
1546 ImGui::SetNextItemWidth(70.0f);
1547 ImGui::InputInt(tr("End"), &z3disasm_bank_end_);
1548 z3disasm_bank_start_ = std::clamp(z3disasm_bank_start_, 0, 0xFF);
1549 z3disasm_bank_end_ = std::clamp(z3disasm_bank_end_, 0, 0xFF);
1550 if (z3disasm_bank_end_ < z3disasm_bank_start_) {
1551 z3disasm_bank_end_ = z3disasm_bank_start_;
1552 }
1553 ImGui::SameLine();
1554 if (ImGui::SmallButton(tr("Use Query Bank"))) {
1555 if (auto bank = CurrentDisassemblyBank(); bank.has_value()) {
1556 z3disasm_bank_start_ = static_cast<int>(*bank);
1557 z3disasm_bank_end_ = static_cast<int>(*bank);
1558 }
1559 }
1560 }
1561
1562 const auto task_snapshot = z3disasm_task_.GetSnapshot();
1563 const bool can_generate = !rom_path.empty();
1564 ImGui::BeginDisabled(!can_generate || task_snapshot.running);
1565 if (ImGui::Button(ICON_MD_REFRESH " Generate / Refresh Banks")) {
1566 auto status = GenerateZ3Disassembly();
1567 if (!status.ok()) {
1568 z3disasm_status_ = std::string(status.message());
1569 if (dependencies_.toast_manager) {
1570 dependencies_.toast_manager->Show(z3disasm_status_, ToastType::kError);
1571 }
1572 }
1573 }
1574 ImGui::EndDisabled();
1575 if (task_snapshot.running) {
1576 ImGui::SameLine();
1577 if (ImGui::Button(ICON_MD_CANCEL " Cancel")) {
1578 z3disasm_task_.Cancel();
1579 }
1580 }
1581 if (!can_generate) {
1582 ImGui::SameLine();
1583 ImGui::TextDisabled(tr("Load or configure a ROM path first."));
1584 }
1585
1586 if (!z3disasm_status_.empty()) {
1587 ImGui::Spacing();
1588 ImGui::TextWrapped("%s", z3disasm_status_.c_str());
1589 }
1590
1591 if (!task_snapshot.output_tail.empty()) {
1592 if (ImGui::CollapsingHeader(tr("z3disasm Output"))) {
1593 std::string output_tail = task_snapshot.output_tail;
1594 ImGui::InputTextMultiline("##z3disasm_output", &output_tail,
1595 ImVec2(-1.0f, 80.0f),
1596 ImGuiInputTextFlags_ReadOnly);
1597 }
1598 }
1599
1600 ImGui::SeparatorText(tr("Bank Browser"));
1601 if (z3disasm_files_.empty()) {
1602 ImGui::TextDisabled(tr(
1603 "No generated bank files yet. Run z3disasm to populate this browser."));
1604 } else {
1605 if (ImGui::BeginTable(
1606 "##z3disasm_browser", 2,
1607 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
1608 ImGui::TableSetupColumn("Banks", ImGuiTableColumnFlags_WidthFixed,
1609 180.0f);
1610 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch);
1611 ImGui::TableNextRow();
1612
1613 ImGui::TableSetColumnIndex(0);
1614 if (ImGui::BeginChild("##z3disasm_list", ImVec2(0, 0), true)) {
1615 for (int i = 0; i < static_cast<int>(z3disasm_files_.size()); ++i) {
1616 const std::string name =
1617 std::filesystem::path(z3disasm_files_[i]).filename().string();
1618 if (ImGui::Selectable(name.c_str(), z3disasm_selected_index_ == i)) {
1619 z3disasm_selected_index_ = i;
1620 LoadSelectedZ3DisassemblyFile();
1621 }
1622 }
1623 }
1624 ImGui::EndChild();
1625
1626 ImGui::TableSetColumnIndex(1);
1627 if (ImGui::BeginChild("##z3disasm_preview", ImVec2(0, 0), true)) {
1628 if (!z3disasm_selected_path_.empty()) {
1629 const std::string selected_name =
1630 std::filesystem::path(z3disasm_selected_path_)
1631 .filename()
1632 .string();
1633 ImGui::TextUnformatted(selected_name.c_str());
1634 ImGui::SameLine();
1635 if (ImGui::SmallButton(tr("Open in Code Editor"))) {
1636 ChangeActiveFile(z3disasm_selected_path_);
1637 }
1638 const std::string bank_query = BuildProjectGraphBankQuery();
1639 if (!bank_query.empty()) {
1640 ImGui::SameLine();
1641 if (ImGui::SmallButton(tr("Open Bank Graph"))) {
1642 auto status = RunProjectGraphQueryInDrawer(
1643 {"--query=bank",
1644 absl::StrFormat("--bank=%02X", SelectedZ3DisasmBankIndex()),
1645 "--format=json"},
1646 absl::StrFormat("Project Graph Bank $%02X",
1647 SelectedZ3DisasmBankIndex()));
1648 if (!status.ok()) {
1649 z3disasm_status_ = std::string(status.message());
1650 }
1651 }
1652 ImGui::SameLine();
1653 if (ImGui::SmallButton(tr("Copy Bank Query"))) {
1654 ImGui::SetClipboardText(bank_query.c_str());
1655 }
1656 ImGui::TextDisabled("%s", bank_query.c_str());
1657 }
1658 ImGui::Separator();
1659 if (!z3disasm_source_jumps_.empty() &&
1660 ImGui::CollapsingHeader(tr("Source Map Jumps"),
1661 ImGuiTreeNodeFlags_DefaultOpen)) {
1662 for (const auto& jump : z3disasm_source_jumps_) {
1663 ImGui::PushID(static_cast<int>(jump.address) ^ jump.line);
1664 const std::string source_label =
1665 absl::StrFormat("Source##source_jump_%06X", jump.address);
1666 const std::string graph_label =
1667 absl::StrFormat("Graph##source_graph_%06X", jump.address);
1668 const std::string copy_label =
1669 absl::StrFormat("Copy##source_copy_%06X", jump.address);
1670 const std::string addr_label =
1671 absl::StrFormat("Addr##source_addr_%06X", jump.address);
1672 if (ImGui::SmallButton(source_label.c_str())) {
1673 JumpToReference(absl::StrCat(jump.file, ":", jump.line))
1674 .IgnoreError();
1675 }
1676 ImGui::SameLine();
1677 if (ImGui::SmallButton(graph_label.c_str())) {
1678 auto status = RunProjectGraphQueryInDrawer(
1679 {"--query=lookup",
1680 absl::StrFormat("--address=%06X", jump.address),
1681 "--format=json"},
1682 absl::StrFormat("Project Graph Lookup $%06X",
1683 jump.address));
1684 if (!status.ok()) {
1685 z3disasm_status_ = std::string(status.message());
1686 }
1687 }
1688 ImGui::SameLine();
1689 if (ImGui::SmallButton(copy_label.c_str())) {
1690 const std::string query =
1691 BuildProjectGraphLookupQuery(jump.address);
1692 ImGui::SetClipboardText(query.c_str());
1693 }
1694 ImGui::SameLine();
1695 if (ImGui::SmallButton(addr_label.c_str())) {
1696 disasm_query_ = absl::StrFormat("0x%06X", jump.address);
1697 NavigateDisassemblyQuery().IgnoreError();
1698 }
1699 ImGui::SameLine();
1700 ImGui::TextWrapped("$%06X %s:%d", jump.address,
1701 jump.file.c_str(), jump.line);
1702 ImGui::PopID();
1703 }
1704 }
1705 if (!z3disasm_hook_jumps_.empty() &&
1706 ImGui::CollapsingHeader(tr("Hook Jumps"),
1707 ImGuiTreeNodeFlags_DefaultOpen)) {
1708 for (const auto& hook : z3disasm_hook_jumps_) {
1709 ImGui::PushID(static_cast<int>(hook.address) ^ hook.size ^
1710 0x4000);
1711 const std::string source_label =
1712 absl::StrFormat("Source##hook_jump_%06X", hook.address);
1713 const std::string graph_label =
1714 absl::StrFormat("Graph##hook_graph_%06X", hook.address);
1715 const std::string copy_label =
1716 absl::StrFormat("Copy##hook_copy_%06X", hook.address);
1717 const std::string addr_label =
1718 absl::StrFormat("Addr##hook_addr_%06X", hook.address);
1719 if (!hook.source.empty() &&
1720 ImGui::SmallButton(source_label.c_str())) {
1721 JumpToReference(hook.source).IgnoreError();
1722 }
1723 if (!hook.source.empty()) {
1724 ImGui::SameLine();
1725 }
1726 if (ImGui::SmallButton(graph_label.c_str())) {
1727 auto status = RunProjectGraphQueryInDrawer(
1728 {"--query=lookup",
1729 absl::StrFormat("--address=%06X", hook.address),
1730 "--format=json"},
1731 absl::StrFormat("Project Graph Lookup $%06X",
1732 hook.address));
1733 if (!status.ok()) {
1734 z3disasm_status_ = std::string(status.message());
1735 }
1736 }
1737 ImGui::SameLine();
1738 if (ImGui::SmallButton(copy_label.c_str())) {
1739 const std::string query =
1740 BuildProjectGraphLookupQuery(hook.address);
1741 ImGui::SetClipboardText(query.c_str());
1742 }
1743 ImGui::SameLine();
1744 if (ImGui::SmallButton(addr_label.c_str())) {
1745 disasm_query_ = absl::StrFormat("0x%06X", hook.address);
1746 NavigateDisassemblyQuery().IgnoreError();
1747 }
1748 ImGui::SameLine();
1749 ImGui::TextWrapped("$%06X %s %s (+%d) %s", hook.address,
1750 hook.kind.c_str(), hook.name.c_str(),
1751 hook.size, hook.source.c_str());
1752 ImGui::PopID();
1753 }
1754 }
1755 ImGui::Separator();
1756 ImGui::InputTextMultiline(
1757 "##z3disasm_text", &z3disasm_selected_contents_,
1758 ImVec2(-1.0f, -1.0f), ImGuiInputTextFlags_ReadOnly);
1759 } else {
1760 ImGui::TextDisabled(
1761 tr("Select a generated bank file to preview it."));
1762 }
1763 }
1764 ImGui::EndChild();
1765 ImGui::EndTable();
1766 }
1767 }
1768
1769 ImGui::SeparatorText(tr("Quick ROM Slice"));
1770 ImGui::TextDisabled(tr("Inline viewer for the currently loaded ROM buffer."));
1771 ImGui::SetNextItemWidth(220.0f);
1772 ImGui::InputText(tr("Address or Symbol"), &disasm_query_);
1773 ImGui::SameLine();
1774 if (ImGui::Button(tr("Go"))) {
1775 auto status = NavigateDisassemblyQuery();
1776 if (!status.ok()) {
1777 disasm_status_ = std::string(status.message());
1778 }
1779 }
1780 ImGui::SameLine();
1781 ImGui::SetNextItemWidth(80.0f);
1782 ImGui::InputInt(tr("Count"), &disasm_instruction_count_);
1783 if (disasm_instruction_count_ < 1) {
1784 disasm_instruction_count_ = 1;
1785 }
1786 if (disasm_instruction_count_ > 128) {
1787 disasm_instruction_count_ = 128;
1788 }
1789
1790 if (!disasm_status_.empty()) {
1791 ImGui::TextDisabled("%s", disasm_status_.c_str());
1792 }
1793 ImGui::Separator();
1794
1795 if (!rom_ || !rom_->is_loaded()) {
1796 ImGui::TextDisabled(tr("Load a ROM to browse disassembly."));
1797 return;
1798 }
1799
1800 uint32_t start_address = 0;
1801 if (auto symbol_it = symbols_.find(disasm_query_);
1802 symbol_it != symbols_.end()) {
1803 start_address = symbol_it->second.address;
1804 } else {
1805 auto parsed = ParseHexAddress(disasm_query_);
1806 if (!parsed.has_value()) {
1807 ImGui::TextDisabled(
1808 tr("Enter a SNES address like 0x008000 or a known label."));
1809 return;
1810 }
1811 start_address = *parsed;
1812 }
1813
1814 std::map<uint32_t, std::string> symbols_by_address;
1815 for (const auto& [name, symbol] : symbols_) {
1816 symbols_by_address.emplace(symbol.address, name);
1817 }
1818
1819 emu::debug::Disassembler65816 disassembler;
1820 disassembler.SetSymbolResolver([&symbols_by_address](uint32_t address) {
1821 auto it = symbols_by_address.find(address);
1822 return it == symbols_by_address.end() ? std::string() : it->second;
1823 });
1824
1825 auto read_byte = [this](uint32_t snes_addr) -> uint8_t {
1826 if (!rom_ || !rom_->is_loaded()) {
1827 return 0;
1828 }
1829 const uint32_t pc_addr = SnesToPc(snes_addr);
1830 if (pc_addr >= rom_->size()) {
1831 return 0;
1832 }
1833 return rom_->vector()[pc_addr];
1834 };
1835
1836 const auto instructions = disassembler.DisassembleRange(
1837 start_address, static_cast<size_t>(disasm_instruction_count_), read_byte);
1838 if (instructions.empty()) {
1839 ImGui::TextDisabled(tr("No disassembly available for this address."));
1840 return;
1841 }
1842
1843 if (ImGui::BeginTable("##assembly_disasm", 3,
1844 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
1845 ImGuiTableFlags_Resizable)) {
1846 ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed,
1847 110.0f);
1848 ImGui::TableSetupColumn("Instruction", ImGuiTableColumnFlags_WidthStretch);
1849 ImGui::TableSetupColumn("Context", ImGuiTableColumnFlags_WidthFixed,
1850 140.0f);
1851 ImGui::TableHeadersRow();
1852
1853 for (const auto& instruction : instructions) {
1854 ImGui::PushID(static_cast<int>(instruction.address));
1855 ImGui::TableNextRow();
1856
1857 ImGui::TableSetColumnIndex(0);
1858 const std::string address_label =
1859 absl::StrFormat("$%06X", instruction.address);
1860 ImGui::TextUnformatted(address_label.c_str());
1861
1862 ImGui::TableSetColumnIndex(1);
1863 if (auto label_it = symbols_by_address.find(instruction.address);
1864 label_it != symbols_by_address.end()) {
1865 ImGui::TextColored(ImVec4(0.75f, 0.85f, 1.0f, 1.0f),
1866 "%s:", label_it->second.c_str());
1867 }
1868 ImGui::TextWrapped("%s", instruction.full_text.c_str());
1869
1870 ImGui::TableSetColumnIndex(2);
1871 if (instruction.branch_target != 0) {
1872 auto target_it = symbols_by_address.find(instruction.branch_target);
1873 if (target_it != symbols_by_address.end()) {
1874 if (ImGui::SmallButton(target_it->second.c_str())) {
1875 JumpToSymbolDefinition(target_it->second).IgnoreError();
1876 }
1877 } else {
1878 ImGui::Text("$%06X", instruction.branch_target);
1879 }
1880 } else {
1881 ImGui::TextDisabled("-");
1882 }
1883 ImGui::PopID();
1884 }
1885 ImGui::EndTable();
1886 }
1887}
1888
1889void AssemblyEditor::DrawToolbarContent() {
1890 float button_size = 32.0f;
1891
1892 if (ImGui::Button(ICON_MD_FOLDER_OPEN, ImVec2(button_size, button_size))) {
1893 auto folder = FileDialogWrapper::ShowOpenFolderDialog();
1894 if (!folder.empty()) {
1895 current_folder_ = LoadFolder(folder);
1896 }
1897 }
1898 if (ImGui::IsItemHovered())
1899 ImGui::SetTooltip(tr("Open Folder"));
1900
1901 ImGui::SameLine();
1902 if (ImGui::Button(ICON_MD_FILE_OPEN, ImVec2(button_size, button_size))) {
1903 auto filename = FileDialogWrapper::ShowOpenFileDialog();
1904 if (!filename.empty()) {
1905 ChangeActiveFile(filename);
1906 }
1907 }
1908 if (ImGui::IsItemHovered())
1909 ImGui::SetTooltip(tr("Open File"));
1910
1911 ImGui::SameLine();
1912 bool can_save = HasActiveFile();
1913 ImGui::BeginDisabled(!can_save);
1914 if (ImGui::Button(ICON_MD_SAVE, ImVec2(button_size, button_size))) {
1915 Save();
1916 }
1917 ImGui::EndDisabled();
1918 if (ImGui::IsItemHovered())
1919 ImGui::SetTooltip(tr("Save File"));
1920
1921 ImGui::SameLine();
1922 ImGui::Text("|"); // Visual separator
1923 ImGui::SameLine();
1924
1925 // Build actions
1926 ImGui::BeginDisabled(!can_save);
1927 if (ImGui::Button(ICON_MD_CHECK_CIRCLE, ImVec2(button_size, button_size))) {
1928 ValidateCurrentFile();
1929 }
1930 ImGui::EndDisabled();
1931 if (ImGui::IsItemHovered())
1932 ImGui::SetTooltip(tr("Validate (Ctrl+B)"));
1933
1934 ImGui::SameLine();
1935 bool can_apply = can_save && rom_ && rom_->is_loaded();
1936 ImGui::BeginDisabled(!can_apply);
1937 if (ImGui::Button(ICON_MD_BUILD, ImVec2(button_size, button_size))) {
1938 ApplyPatchToRom();
1939 }
1940 ImGui::EndDisabled();
1941 if (ImGui::IsItemHovered())
1942 ImGui::SetTooltip(tr("Apply to ROM (Ctrl+Shift+B)"));
1943}
1944
1945void AssemblyEditor::DrawFileTabView() {
1946 if (active_files_.empty()) {
1947 return;
1948 }
1949
1950 if (gui::BeginThemedTabBar("##OpenFileTabs",
1951 ImGuiTabBarFlags_Reorderable |
1952 ImGuiTabBarFlags_AutoSelectNewTabs |
1953 ImGuiTabBarFlags_FittingPolicyScroll)) {
1954 for (int i = 0; i < active_files_.Size; i++) {
1955 int file_id = active_files_[i];
1956 if (file_id >= files_.size()) {
1957 continue;
1958 }
1959
1960 // Extract just the filename from the path
1961 std::string filename = files_[file_id];
1962 size_t pos = filename.find_last_of("/\\");
1963 if (pos != std::string::npos) {
1964 filename = filename.substr(pos + 1);
1965 }
1966
1967 bool is_active = (active_file_id_ == file_id);
1968 ImGuiTabItemFlags flags = is_active ? ImGuiTabItemFlags_SetSelected : 0;
1969 bool tab_open = true;
1970
1971 if (ImGui::BeginTabItem(filename.c_str(), &tab_open, flags)) {
1972 // When tab is selected, update active file
1973 if (!is_active) {
1974 active_file_id_ = file_id;
1975 current_file_ = util::GetFileName(files_[file_id]);
1976 }
1977 ImGui::EndTabItem();
1978 }
1979
1980 // Handle tab close
1981 if (!tab_open) {
1982 active_files_.erase(active_files_.Data + i);
1983 if (active_file_id_ == file_id) {
1984 active_file_id_ = active_files_.empty() ? -1 : active_files_[0];
1985 if (active_file_id_ >= 0 &&
1986 active_file_id_ < static_cast<int>(open_files_.size())) {
1987 current_file_ = util::GetFileName(files_[active_file_id_]);
1988 } else {
1989 current_file_.clear();
1990 }
1991 }
1992 i--;
1993 }
1994 }
1996 }
1997}
1998
1999// =============================================================================
2000// Legacy Update Methods (kept for backward compatibility)
2001// =============================================================================
2002
2003absl::Status AssemblyEditor::Update() {
2004 if (!active_) {
2005 return absl::OkStatus();
2006 }
2007
2008 if (dependencies_.window_manager != nullptr) {
2009 return absl::OkStatus();
2010 }
2011
2012 // Legacy window-based update - kept for backward compatibility
2013 // New code should use the panel system via DrawCodeEditor()
2014 ImGui::Begin("Assembly Editor", &active_, ImGuiWindowFlags_MenuBar);
2015 DrawCodeEditor();
2016 ImGui::End();
2017
2018 // Draw symbol panel as separate window if visible (legacy)
2019 DrawSymbolPanel();
2020
2021 return absl::OkStatus();
2022}
2023
2024void AssemblyEditor::InlineUpdate() {
2025 TextEditor* editor = GetActiveEditor();
2026 auto cpos = editor->GetCursorPosition();
2027 const char* file_label =
2028 current_file_.empty() ? "No file" : current_file_.c_str();
2029 ImGui::Text(tr("%6d/%-6d %6d lines | %s | %s | %s | %s"), cpos.mLine + 1,
2030 cpos.mColumn + 1, editor->GetTotalLines(),
2031 editor->IsOverwrite() ? "Ovr" : "Ins",
2032 editor->CanUndo() ? "*" : " ",
2033 editor->GetLanguageDefinition().mName.c_str(), file_label);
2034
2035 editor->Render("##asm_editor", ImVec2(0, 0));
2036}
2037
2038void AssemblyEditor::UpdateCodeView() {
2039 // Deprecated: Use the WindowContent system instead
2040 // This method is kept for backward compatibility during transition
2041 DrawToolbarContent();
2042 ImGui::Separator();
2043 DrawFileBrowser();
2044}
2045
2046absl::Status AssemblyEditor::Save() {
2047 if (!HasActiveFile()) {
2048 return absl::FailedPreconditionError("No active file to save.");
2049 }
2050
2051 const std::string& path = files_[active_file_id_];
2052 std::ofstream file(path);
2053 if (!file.is_open()) {
2054 return absl::InvalidArgumentError(
2055 absl::StrCat("Cannot write file: ", path));
2056 }
2057
2058 file << GetActiveEditor()->GetText();
2059 file.close();
2060 return absl::OkStatus();
2061}
2062
2063void AssemblyEditor::DrawToolset() {
2064 static gui::Toolset toolbar;
2065 toolbar.Begin();
2066
2067 if (toolbar.AddAction(ICON_MD_FOLDER_OPEN, "Open Folder")) {
2068 current_folder_ = LoadFolder(FileDialogWrapper::ShowOpenFolderDialog());
2069 }
2070 if (toolbar.AddAction(ICON_MD_SAVE, "Save File")) {
2071 Save();
2072 }
2073
2074 toolbar.End();
2075}
2076
2077void AssemblyEditor::DrawCurrentFolder() {
2078 // Lazy load project folder if not already loaded
2079 if (current_folder_.name.empty() && dependencies_.project &&
2080 !dependencies_.project->code_folder.empty()) {
2081 OpenFolder(dependencies_.project->GetAbsolutePath(
2082 dependencies_.project->code_folder));
2083 }
2084
2085 if (ImGui::BeginChild("##current_folder", ImVec2(0, 0), true,
2086 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
2087 if (ImGui::BeginTable("##file_table", 2,
2088 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
2089 ImGuiTableFlags_Resizable |
2090 ImGuiTableFlags_Sortable)) {
2091 ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 256.0f);
2092 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch);
2093
2094 ImGui::TableHeadersRow();
2095
2096 for (const auto& file : current_folder_.files) {
2097 ImGui::TableNextRow();
2098 ImGui::TableNextColumn();
2099 if (ImGui::Selectable(file.c_str())) {
2100 ChangeActiveFile(absl::StrCat(current_folder_.name, "/", file));
2101 }
2102 ImGui::TableNextColumn();
2103 ImGui::Text(tr("File"));
2104 }
2105
2106 for (const auto& subfolder : current_folder_.subfolders) {
2107 ImGui::TableNextRow();
2108 ImGui::TableNextColumn();
2109 if (ImGui::TreeNode(subfolder.name.c_str())) {
2110 for (const auto& file : subfolder.files) {
2111 ImGui::TableNextRow();
2112 ImGui::TableNextColumn();
2113 if (ImGui::Selectable(file.c_str())) {
2114 ChangeActiveFile(absl::StrCat(current_folder_.name, "/",
2115 subfolder.name, "/", file));
2116 }
2117 ImGui::TableNextColumn();
2118 ImGui::Text(tr("File"));
2119 }
2120 ImGui::TreePop();
2121 } else {
2122 ImGui::TableNextColumn();
2123 ImGui::Text(tr("Folder"));
2124 }
2125 }
2126
2127 ImGui::EndTable();
2128 }
2129 }
2130 ImGui::EndChild();
2131}
2132
2133void AssemblyEditor::DrawFileMenu() {
2134 if (ImGui::BeginMenu(tr("File"))) {
2135 if (ImGui::MenuItem(ICON_MD_FILE_OPEN " Open", "Ctrl+O")) {
2137 if (!filename.empty()) {
2138 ChangeActiveFile(filename);
2139 }
2140 }
2141 if (ImGui::MenuItem(ICON_MD_SAVE " Save", "Ctrl+S")) {
2142 Save();
2143 }
2144 ImGui::EndMenu();
2145 }
2146}
2147
2148void AssemblyEditor::DrawEditMenu() {
2149 if (ImGui::BeginMenu(tr("Edit"))) {
2150 if (ImGui::MenuItem(ICON_MD_UNDO " Undo", "Ctrl+Z")) {
2151 GetActiveEditor()->Undo();
2152 }
2153 if (ImGui::MenuItem(ICON_MD_REDO " Redo", "Ctrl+Y")) {
2154 GetActiveEditor()->Redo();
2155 }
2156 ImGui::Separator();
2157 if (ImGui::MenuItem(ICON_MD_CONTENT_CUT " Cut", "Ctrl+X")) {
2158 GetActiveEditor()->Cut();
2159 }
2160 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy", "Ctrl+C")) {
2161 GetActiveEditor()->Copy();
2162 }
2163 if (ImGui::MenuItem(ICON_MD_CONTENT_PASTE " Paste", "Ctrl+V")) {
2164 GetActiveEditor()->Paste();
2165 }
2166 ImGui::Separator();
2167 if (ImGui::MenuItem(ICON_MD_SEARCH " Find", "Ctrl+F")) {
2168 // TODO: Implement this.
2169 }
2170 ImGui::EndMenu();
2171 }
2172}
2173
2174void AssemblyEditor::ChangeActiveFile(const std::string_view& filename) {
2175 if (filename.empty()) {
2176 return;
2177 }
2178
2179 // Check if file is already open
2180 for (int i = 0; i < active_files_.Size; ++i) {
2181 int file_id = active_files_[i];
2182 if (files_[file_id] == filename) {
2183 // Optional: Focus window
2184 active_file_id_ = file_id;
2185 current_file_ = util::GetFileName(files_[file_id]);
2186 return;
2187 }
2188 }
2189
2190 // Load file content using utility
2191 try {
2192 std::string content = util::LoadFile(std::string(filename));
2193 int new_file_id = files_.size();
2194 files_.push_back(std::string(filename));
2195 active_files_.push_back(new_file_id);
2196
2197 // Resize open_files_ if needed
2198 if (new_file_id >= open_files_.size()) {
2199 open_files_.resize(new_file_id + 1);
2200 }
2201
2202 open_files_[new_file_id].SetText(content);
2203 open_files_[new_file_id].SetLanguageDefinition(GetAssemblyLanguageDef());
2204 open_files_[new_file_id].SetPalette(TextEditor::GetDarkPalette());
2205 open_files_[new_file_id].SetShowWhitespaces(false);
2206 active_file_id_ = new_file_id;
2207 current_file_ = util::GetFileName(std::string(filename));
2208 } catch (const std::exception& ex) {
2209 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error opening file: %s\n",
2210 ex.what());
2211 }
2212}
2213
2214absl::Status AssemblyEditor::Cut() {
2215 GetActiveEditor()->Cut();
2216 return absl::OkStatus();
2217}
2218
2219absl::Status AssemblyEditor::Copy() {
2220 GetActiveEditor()->Copy();
2221 return absl::OkStatus();
2222}
2223
2224absl::Status AssemblyEditor::Paste() {
2225 GetActiveEditor()->Paste();
2226 return absl::OkStatus();
2227}
2228
2229absl::Status AssemblyEditor::Undo() {
2230 GetActiveEditor()->Undo();
2231 return absl::OkStatus();
2232}
2233
2234absl::Status AssemblyEditor::Redo() {
2235 GetActiveEditor()->Redo();
2236 return absl::OkStatus();
2237}
2238
2239#ifdef YAZE_WITH_Z3DK
2240core::Z3dkAssembleOptions AssemblyEditor::BuildZ3dkAssembleOptions() const {
2242 if (!dependencies_.project) {
2243 return options;
2244 }
2245
2246 const auto& z3dk = dependencies_.project->z3dk_settings;
2247 options.include_paths = z3dk.include_paths;
2248 options.defines = z3dk.defines;
2249 options.std_includes_path = z3dk.std_includes_path;
2250 options.std_defines_path = z3dk.std_defines_path;
2251 options.mapper = z3dk.mapper;
2252 options.rom_size = z3dk.rom_size;
2253 options.capture_nocash_symbols = (z3dk.symbols_format == "nocash");
2254 options.warn_unused_symbols = z3dk.warn_unused_symbols;
2255 options.warn_branch_outside_bank = z3dk.warn_branch_outside_bank;
2256 options.warn_unknown_width = z3dk.warn_unknown_width;
2257 options.warn_org_collision = z3dk.warn_org_collision;
2258 options.warn_unauthorized_hook = z3dk.warn_unauthorized_hook;
2259 options.warn_stack_balance = z3dk.warn_stack_balance;
2260 options.warn_hook_return = z3dk.warn_hook_return;
2261 for (const auto& range : z3dk.prohibited_memory_ranges) {
2262 options.prohibited_memory_ranges.push_back(
2263 {.start = range.start, .end = range.end, .reason = range.reason});
2264 }
2265
2266 auto append_unique = [&options](const std::string& path) {
2267 if (path.empty()) {
2268 return;
2269 }
2270 if (std::find(options.include_paths.begin(), options.include_paths.end(),
2271 path) == options.include_paths.end()) {
2272 options.include_paths.push_back(path);
2273 }
2274 };
2275
2276 append_unique(dependencies_.project->code_folder);
2277 if (HasActiveFile()) {
2278 append_unique(
2279 std::filesystem::path(files_[active_file_id_]).parent_path().string());
2280 }
2281
2282 if (rom_ && rom_->is_loaded() && !rom_->filename().empty()) {
2283 options.hooks_rom_path = rom_->filename();
2284 } else if (!z3dk.rom_path.empty()) {
2285 options.hooks_rom_path = z3dk.rom_path;
2286 }
2287
2288 return options;
2289}
2290
2291void AssemblyEditor::ExportZ3dkArtifacts(const core::AsarPatchResult& result,
2292 bool sync_mesen_symbols) {
2293 if (!dependencies_.project) {
2294 return;
2295 }
2296
2297 const auto& project = *dependencies_.project;
2298 const auto& configured = project.z3dk_settings.artifact_paths;
2299 const std::string symbols_path =
2300 configured.symbols_mlb.empty()
2301 ? project.GetZ3dkArtifactPath("symbols.mlb")
2302 : configured.symbols_mlb;
2303 const std::string sourcemap_path =
2304 configured.sourcemap_json.empty()
2305 ? project.GetZ3dkArtifactPath("sourcemap.json")
2306 : configured.sourcemap_json;
2307 const std::string annotations_path =
2308 configured.annotations_json.empty()
2309 ? project.GetZ3dkArtifactPath("annotations.json")
2310 : configured.annotations_json;
2311 const std::string hooks_path = configured.hooks_json.empty()
2312 ? project.GetZ3dkArtifactPath("hooks.json")
2313 : configured.hooks_json;
2314 const std::string lint_path = configured.lint_json.empty()
2315 ? project.GetZ3dkArtifactPath("lint.json")
2316 : configured.lint_json;
2317 auto write_artifact = [](const std::string& path,
2318 const std::string& content) {
2319 if (path.empty() || content.empty()) {
2320 return;
2321 }
2322 std::error_code ec;
2323 std::filesystem::create_directories(
2324 std::filesystem::path(path).parent_path(), ec);
2325 std::ofstream file(path, std::ios::binary | std::ios::trunc);
2326 if (!file.is_open()) {
2327 return;
2328 }
2329 file << content;
2330 };
2331
2332 write_artifact(symbols_path, result.symbols_mlb);
2333 write_artifact(sourcemap_path, result.sourcemap_json);
2334 write_artifact(annotations_path, result.annotations_json);
2335 write_artifact(hooks_path, result.hooks_json);
2336 write_artifact(lint_path, result.lint_json);
2337
2338 if (!sync_mesen_symbols || symbols_path.empty() ||
2339 result.symbols_mlb.empty()) {
2340 return;
2341 }
2342
2344 if (!client->IsConnected()) {
2345 client->Connect().IgnoreError();
2346 }
2347 if (!client->IsConnected()) {
2348 return;
2349 }
2350
2351 auto status = client->LoadSymbolsFile(symbols_path);
2352 if (!status.ok()) {
2353 if (dependencies_.toast_manager) {
2354 dependencies_.toast_manager->Show(
2355 "Failed to sync Mesen2 symbols: " + std::string(status.message()),
2356 ToastType::kWarning);
2357 }
2358 } else if (dependencies_.toast_manager) {
2359 dependencies_.toast_manager->Show(
2360 "Synced symbols to Mesen2 from " + symbols_path, ToastType::kSuccess);
2361 }
2362}
2363#endif
2364
2365// ============================================================================
2366// Asar Integration Implementation
2367// ============================================================================
2368
2369absl::Status AssemblyEditor::ValidateCurrentFile() {
2370 if (!HasActiveFile()) {
2371 return absl::FailedPreconditionError("No file is currently active");
2372 }
2373
2374 const std::string& file_path = files_[active_file_id_];
2375
2376#ifdef YAZE_WITH_Z3DK
2377 const auto options = BuildZ3dkAssembleOptions();
2378 // z3dk path: validate via scratch assemble; structured diagnostics are
2379 // populated natively. We reuse ApplyPatch against a throwaway buffer so
2380 // we get the full AsarPatchResult (including symbols) back.
2381 std::vector<uint8_t> scratch;
2382 auto result_or = z3dk_.ApplyPatch(file_path, scratch, options);
2383 if (!result_or.ok()) {
2384 last_errors_.clear();
2385 last_errors_.push_back(std::string(result_or.status().message()));
2386 last_warnings_.clear();
2387 last_diagnostics_.clear();
2388 TextEditor::ErrorMarkers empty_markers;
2389 GetActiveEditor()->SetErrorMarkers(empty_markers);
2390 return result_or.status();
2391 }
2392 UpdateErrorMarkers(*result_or);
2393 if (!result_or->success) {
2394 return absl::InternalError("Assembly validation failed");
2395 }
2396 ExportZ3dkArtifacts(*result_or, false);
2397 ClearErrorMarkers();
2398 return absl::OkStatus();
2399#else
2400 // Initialize Asar if not already done
2401 if (!asar_initialized_) {
2402 auto status = asar_.Initialize();
2403 if (!status.ok()) {
2404 return status;
2405 }
2406 asar_initialized_ = true;
2407 }
2408
2409 // Validate the assembly
2410 auto status = asar_.ValidateAssembly(file_path);
2411
2412 // Update error markers based on result
2413 if (!status.ok()) {
2414 // Get the error messages and show them
2415 last_errors_.clear();
2416 last_errors_.push_back(std::string(status.message()));
2417 // Parse and update error markers
2419 // Asar errors typically contain line numbers we can parse
2420 for (const auto& error : last_errors_) {
2421 // Simple heuristic: look for "line X" or ":X:" pattern
2422 size_t line_pos = error.find(':');
2423 if (line_pos != std::string::npos) {
2424 size_t num_start = line_pos + 1;
2425 size_t num_end = error.find(':', num_start);
2426 if (num_end != std::string::npos) {
2427 std::string line_str = error.substr(num_start, num_end - num_start);
2428 try {
2429 int line = std::stoi(line_str);
2430 markers[line] = error;
2431 } catch (...) {
2432 // Not a line number, skip
2433 }
2434 }
2435 }
2436 }
2437 GetActiveEditor()->SetErrorMarkers(markers);
2438 return status;
2439 }
2440
2441 // Clear any previous error markers
2442 ClearErrorMarkers();
2443 return absl::OkStatus();
2444#endif
2445}
2446
2447absl::Status AssemblyEditor::ApplyPatchToRom() {
2448 if (!rom_ || !rom_->is_loaded()) {
2449 return absl::FailedPreconditionError("No ROM is loaded");
2450 }
2451
2452 if (!HasActiveFile()) {
2453 return absl::FailedPreconditionError("No file is currently active");
2454 }
2455
2456 const std::string& file_path = files_[active_file_id_];
2457 std::vector<uint8_t> rom_data = rom_->vector();
2458
2459#ifdef YAZE_WITH_Z3DK
2460 const auto options = BuildZ3dkAssembleOptions();
2461 auto result = z3dk_.ApplyPatch(file_path, rom_data, options);
2462 if (!result.ok()) {
2463 last_errors_.clear();
2464 last_errors_.push_back(std::string(result.status().message()));
2465 last_warnings_.clear();
2466 last_diagnostics_.clear();
2467 TextEditor::ErrorMarkers empty_markers;
2468 GetActiveEditor()->SetErrorMarkers(empty_markers);
2469 return result.status();
2470 }
2471 UpdateErrorMarkers(*result);
2472 if (!result->success) {
2473 return absl::InternalError("Patch application failed");
2474 }
2475 rom_->LoadFromData(rom_data);
2476 symbols_ = z3dk_.GetSymbolTable();
2477 ExportZ3dkArtifacts(*result, true);
2478 ClearErrorMarkers();
2479 return absl::OkStatus();
2480#else
2481 // Initialize Asar if not already done
2482 if (!asar_initialized_) {
2483 auto status = asar_.Initialize();
2484 if (!status.ok()) {
2485 return status;
2486 }
2487 asar_initialized_ = true;
2488 }
2489
2490 // Apply the patch
2491 auto result = asar_.ApplyPatch(file_path, rom_data);
2492
2493 if (!result.ok()) {
2494 UpdateErrorMarkers(*result);
2495 return result.status();
2496 }
2497
2498 if (result->success) {
2499 // Update the ROM with the patched data
2500 rom_->LoadFromData(rom_data);
2501
2502 // Store symbols for lookup
2503 symbols_ = asar_.GetSymbolTable();
2504 last_errors_.clear();
2505 last_warnings_ = result->warnings;
2506
2507 // Clear error markers
2508 ClearErrorMarkers();
2509
2510 return absl::OkStatus();
2511 } else {
2512 UpdateErrorMarkers(*result);
2513 return absl::InternalError("Patch application failed");
2514 }
2515#endif
2516}
2517
2518void AssemblyEditor::UpdateErrorMarkers(const core::AsarPatchResult& result) {
2519 last_errors_ = result.errors;
2520 last_warnings_ = result.warnings;
2521 last_diagnostics_ = result.structured_diagnostics;
2522
2523 if (!HasActiveFile()) {
2524 return;
2525 }
2526
2528
2529 // Prefer the native structured diagnostics when available — they carry
2530 // line numbers directly, no string parsing required. Fall back to the
2531 // flat error strings only when the backend did not populate structured
2532 // diagnostics (e.g., pre-M3 Asar path with a malformed error line).
2533 if (!result.structured_diagnostics.empty()) {
2534 for (const auto& d : result.structured_diagnostics) {
2535 if (d.line > 0 &&
2537 markers[d.line] = d.message;
2538 }
2539 }
2540 } else {
2541 // Legacy fallback: parse "file:line:..." out of the flat strings.
2542 for (const auto& error : result.errors) {
2543 try {
2544 size_t first_colon = error.find(':');
2545 if (first_colon != std::string::npos) {
2546 size_t second_colon = error.find(':', first_colon + 1);
2547 if (second_colon != std::string::npos) {
2548 std::string line_str =
2549 error.substr(first_colon + 1, second_colon - (first_colon + 1));
2550 int line = std::stoi(line_str);
2551 markers[line] = error;
2552 }
2553 }
2554 } catch (...) {
2555 // Ignore parsing errors
2556 }
2557 }
2558 }
2559
2560 GetActiveEditor()->SetErrorMarkers(markers);
2561}
2562
2563void AssemblyEditor::ClearErrorMarkers() {
2564 last_errors_.clear();
2565 last_diagnostics_.clear();
2566
2567 if (!HasActiveFile()) {
2568 return;
2569 }
2570
2571 TextEditor::ErrorMarkers empty_markers;
2572 GetActiveEditor()->SetErrorMarkers(empty_markers);
2573}
2574
2575void AssemblyEditor::DrawAssembleMenu() {
2576 if (ImGui::BeginMenu(tr("Assemble"))) {
2577 bool has_active_file = HasActiveFile();
2578 bool has_rom = (rom_ && rom_->is_loaded());
2579
2580 if (ImGui::MenuItem(ICON_MD_CHECK_CIRCLE " Validate", "Ctrl+B", false,
2581 has_active_file)) {
2582 auto status = ValidateCurrentFile();
2583 if (status.ok()) {
2584 // Show success notification (could add toast notification here)
2585 }
2586 }
2587
2588 if (ImGui::MenuItem(ICON_MD_BUILD " Apply to ROM", "Ctrl+Shift+B", false,
2589 has_active_file && has_rom)) {
2590 auto status = ApplyPatchToRom();
2591 if (status.ok()) {
2592 // Show success notification
2593 }
2594 }
2595
2596 if (ImGui::MenuItem(ICON_MD_FILE_UPLOAD " Load External Symbols", nullptr,
2597 false)) {
2598 if (dependencies_.project) {
2599 std::string sym_file = dependencies_.project->symbols_filename;
2600 if (dependencies_.project->HasZ3dkConfig() &&
2601 !dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb
2602 .empty()) {
2603 sym_file =
2604 dependencies_.project->z3dk_settings.artifact_paths.symbols_mlb;
2605 } else if (sym_file.empty() || !std::filesystem::exists(sym_file)) {
2606 sym_file = dependencies_.project->GetZ3dkArtifactPath("symbols.mlb");
2607 }
2608 if (!sym_file.empty()) {
2609 std::string abs_path = sym_file;
2610 if (!std::filesystem::path(abs_path).is_absolute()) {
2611 abs_path = dependencies_.project->GetAbsolutePath(sym_file);
2612 }
2613 auto status = asar_.LoadSymbolsFromFile(abs_path);
2614 if (status.ok()) {
2615 // Copy symbols to local map for display
2616 symbols_ = asar_.GetSymbolTable();
2617 if (dependencies_.toast_manager) {
2618 dependencies_.toast_manager->Show(
2619 "Successfully loaded external symbols from " + sym_file,
2620 ToastType::kSuccess);
2621 }
2622 } else {
2623 if (dependencies_.toast_manager) {
2624 dependencies_.toast_manager->Show(
2625 "Failed to load symbols: " + std::string(status.message()),
2626 ToastType::kError);
2627 }
2628 }
2629 } else {
2630 if (dependencies_.toast_manager) {
2631 dependencies_.toast_manager->Show(
2632 "Project does not specify a symbols file.",
2633 ToastType::kWarning);
2634 }
2635 }
2636 }
2637 }
2638
2639 ImGui::Separator();
2640
2641 if (ImGui::MenuItem(ICON_MD_LIST " Show Symbols", nullptr,
2642 show_symbol_panel_)) {
2643 show_symbol_panel_ = !show_symbol_panel_;
2644 }
2645
2646 ImGui::Separator();
2647
2648 // Show last error/warning count
2649 ImGui::TextDisabled(tr("Errors: %zu, Warnings: %zu"), last_errors_.size(),
2650 last_warnings_.size());
2651
2652 ImGui::EndMenu();
2653 }
2654
2655 if (ImGui::BeginMenu(tr("Version"))) {
2656 bool has_version_manager = (dependencies_.version_manager != nullptr);
2657 if (ImGui::MenuItem(ICON_MD_CAMERA_ALT " Create Snapshot", nullptr, false,
2658 has_version_manager)) {
2659 if (has_version_manager) {
2660 ImGui::OpenPopup("Create Snapshot");
2661 }
2662 }
2663
2664 // Snapshot Dialog
2665 if (ImGui::BeginPopupModal("Create Snapshot", nullptr,
2666 ImGuiWindowFlags_AlwaysAutoResize)) {
2667 static char message[256] = "";
2668 ImGui::InputText(tr("Message"), message, sizeof(message));
2669
2670 if (ImGui::Button(tr("Create"), ImVec2(120, 0))) {
2671 auto result = dependencies_.version_manager->CreateSnapshot(message);
2672 if (result.ok() && result->success) {
2673 if (dependencies_.toast_manager) {
2674 dependencies_.toast_manager->Show(
2675 "Snapshot Created: " + result->commit_hash,
2676 ToastType::kSuccess);
2677 }
2678 } else {
2679 if (dependencies_.toast_manager) {
2680 std::string err = result.ok()
2681 ? result->message
2682 : std::string(result.status().message());
2683 dependencies_.toast_manager->Show("Snapshot Failed: " + err,
2684 ToastType::kError);
2685 }
2686 }
2687 ImGui::CloseCurrentPopup();
2688 message[0] = '\0'; // Reset
2689 }
2690 ImGui::SameLine();
2691 if (ImGui::Button(tr("Cancel"), ImVec2(120, 0))) {
2692 ImGui::CloseCurrentPopup();
2693 }
2694 ImGui::EndPopup();
2695 }
2696
2697 ImGui::EndMenu();
2698 }
2699}
2700
2701void AssemblyEditor::DrawSymbolPanel() {
2702 if (!show_symbol_panel_) {
2703 return;
2704 }
2705
2706 ImGui::SetNextWindowSize(ImVec2(350, 400), ImGuiCond_FirstUseEver);
2707 if (ImGui::Begin("Symbols", &show_symbol_panel_)) {
2708 if (symbols_.empty()) {
2709 ImGui::TextDisabled(tr("No symbols loaded."));
2710 ImGui::TextDisabled(tr("Apply a patch to load symbols."));
2711 } else {
2712 // Search filter
2713 static char filter[256] = "";
2714 ImGui::InputTextWithHint("##symbol_filter", "Filter symbols...", filter,
2715 sizeof(filter));
2716
2717 ImGui::Separator();
2718
2719 if (ImGui::BeginChild("##symbol_list", ImVec2(0, 0), true)) {
2720 for (const auto& [name, symbol] : symbols_) {
2721 // Apply filter
2722 if (filter[0] != '\0' && name.find(filter) == std::string::npos) {
2723 continue;
2724 }
2725
2726 ImGui::PushID(name.c_str());
2727 if (ImGui::Selectable(name.c_str())) {
2728 // Could jump to symbol definition if line info is available
2729 // For now, just select it
2730 }
2731 ImGui::SameLine(200);
2732 ImGui::TextDisabled("$%06X", symbol.address);
2733 ImGui::PopID();
2734 }
2735 }
2736 ImGui::EndChild();
2737 }
2738 }
2739 ImGui::End();
2740}
2741
2742} // namespace yaze::editor
static const Palette & GetDarkPalette()
Coordinates GetCursorPosition() const
int GetTotalLines() const
bool IsOverwrite() const
void Render(const char *aTitle, const ImVec2 &aSize=ImVec2(), bool aBorder=false)
std::map< int, std::string > ErrorMarkers
const LanguageDefinition & GetLanguageDefinition() const
bool CanUndo() const
void SetLanguageDefinition(const LanguageDefinition &aLanguageDef)
bool is_array() const
Definition json.h:58
bool empty() const
Definition json.h:62
bool contains(const std::string &) const
Definition json.h:53
auto filename() const
Definition rom.h:157
bool is_loaded() const
Definition rom.h:144
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:321
EditorDependencies dependencies_
Definition editor.h:333
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
65816 CPU disassembler for debugging and ROM hacking
std::vector< DisassembledInstruction > DisassembleRange(uint32_t start_address, size_t count, MemoryReader read_byte, bool m_flag=true, bool x_flag=true) const
Disassemble multiple instructions.
void SetSymbolResolver(SymbolResolver resolver)
Set optional symbol resolver for address lookups.
static std::shared_ptr< MesenSocketClient > GetOrCreate()
RAII guard for ImGui style colors.
Definition style_guard.h:27
Ultra-compact toolbar that merges mode buttons with settings.
bool AddAction(const char *icon, const char *tooltip)
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_CONTENT_CUT
Definition icons.h:466
#define ICON_MD_FILE_OPEN
Definition icons.h:747
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_CAMERA_ALT
Definition icons.h:355
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_UNDO
Definition icons.h:2039
std::optional< int > ParsePositiveInt(const std::string &s)
bool LooksLikeAssemblyPathRef(const std::string &file_ref)
bool LoadJsonFile(const std::string &path, Json *out)
std::optional< std::filesystem::path > FindUpwardPath(const std::filesystem::path &start, const std::filesystem::path &relative)
std::optional< AsmFileSymbolRef > ParseAsmFileSymbolRef(const std::string &reference)
std::optional< AsmSymbolLocation > FindLabelInFolder(const std::filesystem::path &root, const std::string &label)
std::optional< uint32_t > ParseHexAddress(const std::string &text)
std::optional< AsmSymbolLocation > FindLabelInFile(const std::filesystem::path &path, const std::string &label)
std::optional< AsmFileLineRef > ParseAsmFileLineRef(const std::string &reference)
bool IsIgnoredFile(const std::string &name, const std::vector< std::string > &ignored_files)
FolderItem LoadFolder(const std::string &folder)
std::optional< int > ParseGeneratedBankIndex(const std::string &path)
std::optional< std::filesystem::path > FindAsmFileInFolder(const std::filesystem::path &root, const std::string &file_ref)
bool IsAssemblyLikeFile(const std::filesystem::path &path)
bool IsGeneratedBankFile(const std::filesystem::path &path)
std::string ShellQuote(const std::string &value)
Editors are the view controllers for the application.
void DrawDiagnosticsPanel(std::span< const core::AssemblyDiagnostic > diagnostics, const DiagnosticsPanelCallbacks &callbacks)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
TextEditor::LanguageDefinition GetAssemblyLanguageDef()
Definition style.cc:195
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:173
WorkspaceWindowManager * window_manager
Definition editor.h:181
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:1565
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1486
Z3dkSettings z3dk_settings
Definition project.h:215
Z3dkArtifactPaths artifact_paths
Definition project.h:152