diff options
| author | hachem <im@hachem.wtf> | 2025-12-14 16:44:22 +0100 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2025-12-14 16:44:22 +0100 |
| commit | 0e8a995bc2cb5416228c12add93d10a631b7f952 (patch) | |
| tree | 0c440a2a7c537df5f6bad73d901638d59fdd0470 | |
| parent | 643287696ea9baccc90a26643b6adb9fabcdf433 (diff) | |
feat: macro expansion
| -rw-r--r-- | src/ui/macro_expander.cpp | 802 | ||||
| -rw-r--r-- | src/ui/macro_expander.h | 76 | ||||
| -rw-r--r-- | src/ui/source_view.cpp | 132 | ||||
| -rw-r--r-- | src/ui/source_view.h | 17 |
4 files changed, 1021 insertions, 6 deletions
diff --git a/src/ui/macro_expander.cpp b/src/ui/macro_expander.cpp new file mode 100644 index 0000000..5c53482 --- /dev/null +++ b/src/ui/macro_expander.cpp @@ -0,0 +1,802 @@ +#include "ui/macro_expander.h" + +#include <algorithm> +#include <cctype> +#include <fstream> + +namespace Hsdbg +{ + namespace + { + constexpr size_t MAX_TOKENS = 200000; + constexpr int MAX_INCLUDE_DEPTH = 8; + + auto is_ident_start(char c) -> bool + { + return std::isalpha(static_cast<unsigned char>(c)) != 0 || c == '_'; + } + + auto is_ident(char c) -> bool + { + return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_'; + } + + auto lex_one(std::string_view text, size_t& at) -> PpToken + { + while (at < text.size() && std::isspace(static_cast<unsigned char>(text[at])) != 0) + ++at; + + PpToken token; + + if (at >= text.size()) + return token; + + const char c = text[at]; + + if (is_ident_start(c)) + { + const size_t start = at; + while (at < text.size() && is_ident(text[at])) + ++at; + + token.kind = PpKind::Identifier; + token.text = std::string(text.substr(start, at - start)); + return token; + } + + if (std::isdigit(static_cast<unsigned char>(c)) != 0 || + (c == '.' && at + 1 < text.size() && + std::isdigit(static_cast<unsigned char>(text[at + 1])) != 0)) + { + const size_t start = at++; + + while (at < text.size()) + { + const char d = text[at]; + + if ((d == 'e' || d == 'E' || d == 'p' || d == 'P') && at + 1 < text.size() && + (text[at + 1] == '+' || text[at + 1] == '-')) + { + at += 2; + continue; + } + + if (is_ident(d) || d == '.') + ++at; + else + break; + } + + token.kind = PpKind::Number; + token.text = std::string(text.substr(start, at - start)); + return token; + } + + if (c == '"' || c == '\'') + { + const size_t start = at++; + + while (at < text.size()) + { + if (text[at] == '\\' && at + 1 < text.size()) + { + at += 2; + continue; + } + + if (text[at] == c) + { + ++at; + break; + } + + ++at; + } + + token.kind = c == '"' ? PpKind::String : PpKind::Char; + token.text = std::string(text.substr(start, at - start)); + return token; + } + + static constexpr std::string_view multis[] = { + "...", "<<=", ">>=", "->*", "::", "->", "##", "<<", ">>", "<=", ">=", + "==", "!=", "&&", "||", "+=", "-=", "*=", "/=", "%=", "&=", "|=", + "^=", "++", "--", ".*", + }; + + for (const std::string_view op : multis) + { + if (text.compare(at, op.size(), op) == 0) + { + at += op.size(); + token.kind = PpKind::Punct; + token.text = std::string(op); + return token; + } + } + + token.kind = PpKind::Punct; + token.text = std::string(1, c); + ++at; + return token; + } + + auto tokenize(std::string_view text) -> std::vector<PpToken> + { + std::vector<PpToken> tokens; + size_t at = 0; + + while (at < text.size() && tokens.size() < MAX_TOKENS) + { + PpToken token = lex_one(text, at); + + if (token.text.empty()) + break; + + tokens.push_back(std::move(token)); + } + + return tokens; + } + + auto hidden_by(const PpToken& token, const std::string& name) -> bool + { + return std::ranges::find(token.hide, name) != token.hide.end(); + } + + auto needs_space(const PpToken& left, const PpToken& right) -> bool + { + const std::string& l = left.text; + const std::string& r = right.text; + + if (r == ")" || r == "]" || r == "," || r == ";" || r == "::" || + r == "." || r == "->") + return false; + + if (l == "(" || l == "[" || l == "::" || l == "." || l == "->") + return false; + + if (r == "(" && (left.kind == PpKind::Identifier || l == ")" || l == "]")) + return false; + + return true; + } + + auto render(const std::vector<PpToken>& tokens) -> std::string + { + std::string out; + + for (size_t i = 0; i < tokens.size(); ++i) + { + if (i != 0 && needs_space(tokens[i - 1], tokens[i])) + out += ' '; + + out += tokens[i].text; + } + + return out; + } + + auto param_index(const MacroDef& def, const std::string& name) -> int + { + for (size_t i = 0; i < def.params.size(); ++i) + { + if (def.params[i] == name) + return static_cast<int>(i); + } + + return -1; + } + + auto stringize(const std::vector<PpToken>& arg) -> PpToken + { + std::string inner = render(arg); + std::string escaped; + + for (const char c : inner) + { + if (c == '\\' || c == '"') + escaped += '\\'; + + escaped += c; + } + + return PpToken{ PpKind::String, "\"" + escaped + "\"", {} }; + } + + auto paste_token(const std::string& text) -> PpToken + { + std::vector<PpToken> lexed = tokenize(text); + + if (lexed.size() == 1) + return lexed.front(); + + return PpToken{ PpKind::Identifier, text, {} }; + } + + using ArgList = std::vector<std::vector<PpToken>>; + + auto expand_full(const MacroTable& table, std::vector<PpToken> tokens) + -> std::vector<PpToken>; + + auto substitute(const MacroDef& def, const ArgList& raw, const ArgList& expanded) + -> std::vector<PpToken> + { + std::vector<PpToken> out; + + const auto raw_tokens = [&](int index) -> std::vector<PpToken> + { + if (index >= 0 && index < static_cast<int>(raw.size())) + return raw[static_cast<size_t>(index)]; + + return {}; + }; + + const auto expanded_tokens = [&](int index) -> std::vector<PpToken> + { + if (index >= 0 && index < static_cast<int>(expanded.size())) + return expanded[static_cast<size_t>(index)]; + + return {}; + }; + + const auto raw_varargs = [&]() -> std::vector<PpToken> + { + std::vector<PpToken> joined; + + for (size_t i = def.params.size(); i < raw.size(); ++i) + { + if (i != def.params.size()) + joined.push_back(PpToken{ PpKind::Punct, ",", {} }); + + joined.insert(joined.end(), raw[i].begin(), raw[i].end()); + } + + return joined; + }; + + const auto expanded_varargs = [&]() -> std::vector<PpToken> + { + std::vector<PpToken> joined; + + for (size_t i = def.params.size(); i < expanded.size(); ++i) + { + if (i != def.params.size()) + joined.push_back(PpToken{ PpKind::Punct, ",", {} }); + + joined.insert(joined.end(), expanded[i].begin(), expanded[i].end()); + } + + return joined; + }; + + const auto& body = def.body; + + for (size_t i = 0; i < body.size(); ++i) + { + const PpToken& tok = body[i]; + + if (tok.text == "#" && def.function_like && i + 1 < body.size() && + body[i + 1].kind == PpKind::Identifier) + { + if (const int index = param_index(def, body[i + 1].text); index >= 0) + { + out.push_back(stringize(raw_tokens(index))); + ++i; + continue; + } + + if (def.variadic && body[i + 1].text == "__VA_ARGS__") + { + out.push_back(stringize(raw_varargs())); + ++i; + continue; + } + } + + if (tok.text == "##" && !out.empty() && i + 1 < body.size()) + { + const PpToken& rhs = body[i + 1]; + std::vector<PpToken> pieces; + + if (const int index = param_index(def, rhs.text); index >= 0) + pieces = raw_tokens(index); + else if (def.variadic && rhs.text == "__VA_ARGS__") + pieces = raw_varargs(); + else + pieces = { rhs }; + + if (!pieces.empty()) + { + out.back() = paste_token(out.back().text + pieces.front().text); + out.insert(out.end(), pieces.begin() + 1, pieces.end()); + } + + ++i; + continue; + } + + const bool paste_operand = + (i + 1 < body.size() && body[i + 1].text == "##") || + (i > 0 && body[i - 1].text == "##"); + + if (const int index = param_index(def, tok.text); index >= 0) + { + const std::vector<PpToken> value = + paste_operand ? raw_tokens(index) : expanded_tokens(index); + out.insert(out.end(), value.begin(), value.end()); + continue; + } + + if (def.variadic && tok.text == "__VA_ARGS__") + { + const std::vector<PpToken> value = + paste_operand ? raw_varargs() : expanded_varargs(); + out.insert(out.end(), value.begin(), value.end()); + continue; + } + + out.push_back(tok); + } + + return out; + } + + auto paint(std::vector<PpToken>& tokens, const std::vector<std::string>& carried, + const std::string& name) -> void + { + for (PpToken& token : tokens) + { + for (const std::string& hidden : carried) + { + if (!hidden_by(token, hidden)) + token.hide.push_back(hidden); + } + + if (!hidden_by(token, name)) + token.hide.push_back(name); + } + } + + auto collect_args(const std::vector<PpToken>& tokens, size_t open, ArgList& args, + size_t& end) -> bool + { + int depth = 0; + std::vector<PpToken> current; + bool any = false; + + for (size_t i = open; i < tokens.size(); ++i) + { + const PpToken& tok = tokens[i]; + + if (tok.text == "(") + { + ++depth; + + if (depth == 1) + continue; + } + else if (tok.text == ")") + { + --depth; + + if (depth == 0) + { + if (any || !current.empty()) + args.push_back(std::move(current)); + + end = i + 1; + return true; + } + } + else if (tok.text == "," && depth == 1) + { + args.push_back(std::move(current)); + current.clear(); + any = true; + continue; + } + + current.push_back(tok); + any = true; + } + + return false; + } + + auto arity_ok(const MacroDef& def, const ArgList& args) -> bool + { + const size_t provided = + args.size() == 1 && args.front().empty() ? 0 : args.size(); + + if (def.variadic) + return provided >= def.params.size(); + + return provided == def.params.size(); + } + + auto expand_pass(const MacroTable& table, const std::vector<PpToken>& in, + std::vector<PpToken>& out, std::vector<std::string>& expanded) -> bool + { + bool changed = false; + size_t i = 0; + + while (i < in.size()) + { + const PpToken& tok = in[i]; + + if (tok.kind != PpKind::Identifier || hidden_by(tok, tok.text)) + { + out.push_back(tok); + ++i; + continue; + } + + const MacroDef* def = table.find(tok.text); + + if (def == nullptr) + { + out.push_back(tok); + ++i; + continue; + } + + if (!def->function_like) + { + std::vector<PpToken> repl = substitute(*def, {}, {}); + paint(repl, tok.hide, def->name); + out.insert(out.end(), repl.begin(), repl.end()); + expanded.push_back(def->name); + changed = true; + ++i; + continue; + } + + const size_t paren = i + 1; + + if (paren >= in.size() || in[paren].text != "(") + { + out.push_back(tok); + ++i; + continue; + } + + ArgList args; + size_t end = 0; + + if (!collect_args(in, paren, args, end) || !arity_ok(*def, args)) + { + out.push_back(tok); + ++i; + continue; + } + + ArgList expanded_args; + expanded_args.reserve(args.size()); + + for (const std::vector<PpToken>& arg : args) + expanded_args.push_back(expand_full(table, arg)); + + std::vector<PpToken> repl = substitute(*def, args, expanded_args); + + std::vector<std::string> carried = tok.hide; + const std::vector<std::string>& close_hide = in[end - 1].hide; + + std::erase_if(carried, [&](const std::string& name) + { + return std::ranges::find(close_hide, name) == close_hide.end(); + }); + + paint(repl, carried, def->name); + out.insert(out.end(), repl.begin(), repl.end()); + expanded.push_back(def->name); + changed = true; + i = end; + } + + return changed && out.size() < MAX_TOKENS; + } + + auto expand_full(const MacroTable& table, std::vector<PpToken> tokens) + -> std::vector<PpToken> + { + for (int level = 0; level < 64; ++level) + { + std::vector<PpToken> next; + std::vector<std::string> expanded; + + if (!expand_pass(table, tokens, next, expanded) || expanded.empty()) + break; + + tokens = std::move(next); + + if (tokens.size() >= MAX_TOKENS) + break; + } + + return tokens; + } + } + + auto MacroTable::clear() -> void + { + m_macros.clear(); + ++m_revision; + } + + auto MacroTable::find(std::string_view name) const -> const MacroDef* + { + const auto it = m_macros.find(std::string(name)); + return it == m_macros.end() ? nullptr : &it->second; + } + + auto MacroTable::build(const std::vector<std::string>& lines, + const std::filesystem::path& base_dir) -> void + { + m_macros.clear(); + + std::vector<std::filesystem::path> seen; + scan_lines(lines, base_dir, 0, seen); + + ++m_revision; + } + + namespace + { + auto splice_lines(const std::vector<std::string>& lines) -> std::vector<std::string> + { + std::vector<std::string> out; + std::string pending; + bool continuing = false; + + for (const std::string& line : lines) + { + std::string text = line; + + const bool cont = !text.empty() && text.back() == '\\'; + + if (cont) + text.pop_back(); + + if (continuing) + pending += text; + else + pending = text; + + if (cont) + { + continuing = true; + continue; + } + + out.push_back(std::move(pending)); + pending.clear(); + continuing = false; + } + + if (continuing) + out.push_back(std::move(pending)); + + return out; + } + + auto directive_of(const std::string& line, std::string& rest) -> std::string + { + size_t at = 0; + + while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0) + ++at; + + if (at >= line.size() || line[at] != '#') + return {}; + + ++at; + + while (at < line.size() && std::isspace(static_cast<unsigned char>(line[at])) != 0) + ++at; + + const size_t start = at; + + while (at < line.size() && is_ident(line[at])) + ++at; + + std::string word = line.substr(start, at - start); + rest = line.substr(at); + return word; + } + + auto quote_include(const std::string& rest) -> std::string + { + const size_t open = rest.find('"'); + + if (open == std::string::npos) + return {}; + + const size_t close = rest.find('"', open + 1); + + if (close == std::string::npos) + return {}; + + return rest.substr(open + 1, close - open - 1); + } + + auto parse_define(const std::string& rest, MacroDef& def) -> bool + { + size_t at = 0; + + while (at < rest.size() && std::isspace(static_cast<unsigned char>(rest[at])) != 0) + ++at; + + if (at >= rest.size() || !is_ident_start(rest[at])) + return false; + + const size_t name_start = at; + + while (at < rest.size() && is_ident(rest[at])) + ++at; + + def.name = rest.substr(name_start, at - name_start); + + if (at < rest.size() && rest[at] == '(') + { + def.function_like = true; + ++at; + + while (true) + { + while (at < rest.size() && + std::isspace(static_cast<unsigned char>(rest[at])) != 0) + ++at; + + if (at < rest.size() && rest[at] == ')') + { + ++at; + break; + } + + if (rest.compare(at, 3, "...") == 0) + { + def.variadic = true; + at += 3; + continue; + } + + if (at < rest.size() && is_ident_start(rest[at])) + { + const size_t p = at; + + while (at < rest.size() && is_ident(rest[at])) + ++at; + + std::string param = rest.substr(p, at - p); + + if (rest.compare(at, 3, "...") == 0) + { + def.variadic = true; + at += 3; + } + else + { + def.params.push_back(std::move(param)); + } + + continue; + } + + if (at < rest.size() && rest[at] == ',') + { + ++at; + continue; + } + + return false; + } + } + + def.body = tokenize(std::string_view(rest).substr(at)); + return true; + } + } + + auto MacroTable::scan_lines(const std::vector<std::string>& lines, + const std::filesystem::path& base_dir, int depth, + std::vector<std::filesystem::path>& seen) -> void + { + const std::vector<std::string> logical = splice_lines(lines); + + for (const std::string& line : logical) + { + std::string rest; + const std::string directive = directive_of(line, rest); + + if (directive == "define") + { + MacroDef def; + + if (parse_define(rest, def)) + m_macros[def.name] = std::move(def); + } + else if (directive == "undef") + { + MacroDef tmp; + + if (parse_define(rest, tmp)) + m_macros.erase(tmp.name); + } + else if (directive == "include" && depth < MAX_INCLUDE_DEPTH && !base_dir.empty()) + { + const std::string name = quote_include(rest); + + if (!name.empty()) + scan_path(base_dir / name, depth + 1, seen); + } + } + } + + auto MacroTable::scan_path(const std::filesystem::path& path, int depth, + std::vector<std::filesystem::path>& seen) -> void + { + std::error_code error; + const std::filesystem::path canonical = std::filesystem::weakly_canonical(path, error); + const std::filesystem::path key = error ? path : canonical; + + if (std::ranges::find(seen, key) != seen.end()) + return; + + seen.push_back(key); + + std::ifstream file(path); + + if (!file.is_open()) + return; + + std::vector<std::string> lines; + std::string line; + + while (std::getline(file, line)) + { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + lines.push_back(std::move(line)); + } + + scan_lines(lines, path.parent_path(), depth, seen); + } + + auto expand_stages(const MacroTable& table, std::string_view input, int max_levels) + -> MacroExpansion + { + MacroExpansion result; + + std::vector<PpToken> tokens = tokenize(input); + + result.levels.push_back(render(tokens)); + result.expanded.emplace_back(); + + for (int level = 0; level < max_levels; ++level) + { + std::vector<PpToken> next; + std::vector<std::string> expanded; + + if (!expand_pass(table, tokens, next, expanded) || expanded.empty()) + { + result.m_reached_fixpoint = true; + return result; + } + + tokens = std::move(next); + result.levels.push_back(render(tokens)); + result.expanded.push_back(std::move(expanded)); + + if (tokens.size() >= MAX_TOKENS) + { + result.m_reached_fixpoint = false; + return result; + } + } + + result.m_reached_fixpoint = false; + return result; + } +} diff --git a/src/ui/macro_expander.h b/src/ui/macro_expander.h new file mode 100644 index 0000000..3352a53 --- /dev/null +++ b/src/ui/macro_expander.h @@ -0,0 +1,76 @@ +#pragma once + +#include <cstdint> +#include <filesystem> +#include <string> +#include <string_view> +#include <unordered_map> +#include <vector> + +namespace Hsdbg +{ + enum class PpKind : uint8_t + { + Identifier, + Number, + String, + Char, + Punct, + }; + + struct PpToken + { + PpKind kind = PpKind::Identifier; + std::string text; + std::vector<std::string> hide; + }; + + struct MacroDef + { + std::string name; + bool function_like = false; + bool variadic = false; + std::vector<std::string> params; + std::vector<PpToken> body; + }; + + class MacroTable + { + public: + auto clear() -> void; + + auto build(const std::vector<std::string>& lines, + const std::filesystem::path& base_dir) -> void; + + auto find(std::string_view name) const -> const MacroDef*; + + auto empty() const -> bool { return m_macros.empty(); } + auto size() const -> size_t { return m_macros.size(); } + + auto revision() const -> uint64_t { return m_revision; } + + private: + auto scan_lines(const std::vector<std::string>& lines, + const std::filesystem::path& base_dir, int depth, + std::vector<std::filesystem::path>& seen) -> void; + auto scan_path(const std::filesystem::path& path, int depth, + std::vector<std::filesystem::path>& seen) -> void; + + std::unordered_map<std::string, MacroDef> m_macros; + uint64_t m_revision = 0; + }; + + struct MacroExpansion + { + std::vector<std::string> levels; + + std::vector<std::vector<std::string>> expanded; + + auto fully_expanded() const -> bool { return m_reached_fixpoint; } + + bool m_reached_fixpoint = true; + }; + + auto expand_stages(const MacroTable& table, std::string_view input, + int max_levels = 64) -> MacroExpansion; +} diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp index e0c26b4..5ced658 100644 --- a/src/ui/source_view.cpp +++ b/src/ui/source_view.cpp @@ -100,7 +100,8 @@ namespace Hsdbg return true; } - auto highlight_lines(const std::vector<std::string>& lines, Language language) + auto highlight_lines(const std::vector<std::string>& lines, Language language, + const MacroTable& macros) -> std::vector<std::vector<SourceSpan>> { std::vector<std::vector<SourceSpan>> out(lines.size()); @@ -260,9 +261,15 @@ namespace Hsdbg while (at < size && is_word(line[at])) ++at; - if (const SyntaxKind kind = classify_word( - std::string_view(line).substr(start, at - start), language); - kind != SyntaxKind::Default) + const std::string_view word = + std::string_view(line).substr(start, at - start); + SyntaxKind kind = classify_word(word, language); + + if (kind == SyntaxKind::Default && language == Language::Cpp && + macros.find(word) != nullptr) + kind = SyntaxKind::Macro; + + if (kind != SyntaxKind::Default) { flush_default(start); emit(start, at - start, kind); @@ -314,6 +321,42 @@ namespace Hsdbg { return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x; } + + auto capture_invocation(const std::string& line, uint32_t start) -> std::string + { + const size_t size = line.size(); + size_t at = start; + + while (at < size && is_word(line[at])) + ++at; + + size_t paren = at; + + while (paren < size && std::isspace(static_cast<unsigned char>(line[paren])) != 0) + ++paren; + + if (paren < size && line[paren] == '(') + { + int depth = 0; + + for (size_t i = paren; i < size; ++i) + { + if (line[i] == '(') + { + ++depth; + } + else if (line[i] == ')') + { + --depth; + + if (depth == 0) + return line.substr(start, i + 1 - start); + } + } + } + + return line.substr(start, at - start); + } } auto SourceView::open(const std::filesystem::path& path) -> Result<void> @@ -338,7 +381,13 @@ namespace Hsdbg m_path = path; m_lines = std::move(lines); m_highlight = language.has_value(); - m_spans = language ? highlight_lines(m_lines, *language) + + m_macros.clear(); + + if (language == Language::Cpp) + m_macros.build(m_lines, path.parent_path()); + + m_spans = language ? highlight_lines(m_lines, *language, m_macros) : std::vector<std::vector<SourceSpan>>{}; m_path_input = path.string(); m_error.clear(); @@ -354,11 +403,20 @@ namespace Hsdbg m_path.clear(); m_lines.clear(); m_spans.clear(); + m_macros.clear(); + m_macro_request.reset(); m_highlight = false; m_error.clear(); m_highlighted_line = 0; } + auto SourceView::take_macro_request() -> std::optional<std::string> + { + std::optional<std::string> request = std::move(m_macro_request); + m_macro_request.reset(); + return request; + } + auto SourceView::set_highlighted_line(uint32_t line) -> void { m_highlighted_line = line; @@ -533,6 +591,10 @@ namespace Hsdbg { ImGui::TextUnformatted(begin, end); } + else if (span.kind == SyntaxKind::Macro) + { + draw_macro_span(draw_list, text, span); + } else { ImGui::PushStyleColor(ImGuiCol_Text, @@ -549,4 +611,64 @@ namespace Hsdbg ImGui::EndChild(); } + + auto SourceView::draw_macro_span(ImDrawList* draw_list, const std::string& line, + const SourceSpan& span) -> void + { + const char* const begin = line.c_str() + span.start; + const char* const end = begin + span.length; + const ImU32 color = m_syntax_colors[static_cast<size_t>(SyntaxKind::Macro)]; + + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(begin, end); + ImGui::PopStyleColor(); + + if (!ImGui::IsItemHovered()) + return; + + const ImVec2 rect_min = ImGui::GetItemRectMin(); + const ImVec2 rect_max = ImGui::GetItemRectMax(); + draw_list->AddLine(ImVec2(rect_min.x, rect_max.y - 1.0f), + ImVec2(rect_max.x, rect_max.y - 1.0f), color); + + const std::string invocation = capture_invocation(line, span.start); + const MacroExpansion expansion = expand_stages(m_macros, invocation); + + ImGui::BeginTooltip(); + + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(invocation.c_str()); + ImGui::PopStyleColor(); + + ImGui::Separator(); + + std::string preview = expansion.levels.back(); + + if (constexpr size_t limit = 240; preview.size() > limit) + { + preview.resize(limit); + preview += " ..."; + } + + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 30.0f); + ImGui::TextUnformatted(preview.c_str()); + ImGui::PopTextWrapPos(); + + ImGui::Spacing(); + + const size_t steps = expansion.levels.size() - 1; + + if (steps == 0) + ImGui::TextDisabled("no expansion"); + else if (!expansion.fully_expanded()) + ImGui::TextDisabled("%zu+ levels — click to step through", steps); + else + ImGui::TextDisabled("%zu level%s — click to step through", steps, + steps == 1 ? "" : "s"); + + ImGui::EndTooltip(); + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + m_macro_request = invocation; + } } diff --git a/src/ui/source_view.h b/src/ui/source_view.h index dd3ed27..6750d17 100644 --- a/src/ui/source_view.h +++ b/src/ui/source_view.h @@ -1,12 +1,16 @@ #pragma once #include "core/result.h" +#include "ui/macro_expander.h" #include <cstdint> #include <filesystem> +#include <optional> #include <string> #include <vector> +struct ImDrawList; + namespace Hsdbg { class Debugger; @@ -20,6 +24,7 @@ namespace Hsdbg Number, Comment, Preprocessor, + Macro, }; struct SourceSpan @@ -49,20 +54,30 @@ namespace Hsdbg auto line_count() const -> size_t { return m_lines.size(); } auto is_open() const -> bool { return !m_lines.empty(); } + auto macros() const -> const MacroTable& { return m_macros; } + + auto take_macro_request() -> std::optional<std::string>; + private: auto draw_open_bar() -> void; auto draw_lines(Debugger& debugger) -> void; + auto draw_macro_span(ImDrawList* draw_list, const std::string& line, + const SourceSpan& span) -> void; + std::filesystem::path m_path; std::vector<std::string> m_lines; std::vector<std::vector<SourceSpan>> m_spans; + MacroTable m_macros; + std::optional<std::string> m_macro_request; bool m_highlight = false; bool m_highlighting_enabled = true; bool m_line_numbers = true; bool m_highlight_current_line = true; - unsigned int m_syntax_colors[7] = { + unsigned int m_syntax_colors[8] = { 0, 0xFFD69C56, 0xFFB0C94E, 0xFF7891CE, 0xFFA8CEB5, 0xFF55996A, 0xFFC086C5, + 0xFF4FA3E0, }; unsigned int m_current_line_color = 0xFF2E483A; std::string m_path_input; |
