#include "ui/diff_viewer.h" #include "managers/avatar_cache.h" #include "managers/git_manager.h" #include "models/repository.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { float scaled(float value, float scale) { return value * scale; } std::vector splitLines(const std::string& text) { std::vector lines; std::istringstream stream(text); std::string line; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); lines.push_back(std::move(line)); } return lines; } void parseRange(const std::string& header, char marker, int& line) { const size_t marker_position = header.find(marker); if (marker_position == std::string::npos) return; size_t cursor = marker_position + 1; line = 0; while (cursor < header.size() && std::isdigit(static_cast(header[cursor]))) { line = line * 10 + (header[cursor] - '0'); ++cursor; } } enum class SyntaxLanguage { plain, c, cpp, csharp, lua, python, javascript }; enum class SyntaxContinuation { none, block_comment, lua_comment, python_single, python_double, template_string }; struct SyntaxState { SyntaxContinuation continuation = SyntaxContinuation::none; }; [[maybe_unused]] ImU32 blameColor(std::string_view hash, int alpha = 255) { static constexpr ImU32 colors[] = { IM_COL32(24, 181, 204, 255), IM_COL32(73, 123, 235, 255), IM_COL32(200, 64, 200, 255), IM_COL32(239, 79, 89, 255), IM_COL32(255, 122, 41, 255), IM_COL32(240, 186, 46, 255), IM_COL32(64, 186, 128, 255), }; uint32_t value = 2166136261u; for (const unsigned char character : hash) value = (value ^ character) * 16777619u; return (colors[value % std::size(colors)] & ~IM_COL32_A_MASK) | (static_cast(alpha) << IM_COL32_A_SHIFT); } [[maybe_unused]] SyntaxLanguage languageForPath(const std::string& path) { std::string extension = std::filesystem::path(path).extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { return static_cast(std::tolower(value)); }); if (extension == ".c") return SyntaxLanguage::c; if (extension == ".h" || extension == ".hh" || extension == ".hpp" || extension == ".hxx" || extension == ".cc" || extension == ".cpp" || extension == ".cxx") return SyntaxLanguage::cpp; if (extension == ".cs") return SyntaxLanguage::csharp; if (extension == ".lua") return SyntaxLanguage::lua; if (extension == ".py" || extension == ".pyw") return SyntaxLanguage::python; if (extension == ".js" || extension == ".jsx" || extension == ".mjs" || extension == ".cjs") return SyntaxLanguage::javascript; return SyntaxLanguage::plain; } bool wordIs(std::string_view word, std::initializer_list values) { return std::find(values.begin(), values.end(), word) != values.end(); } bool isKeyword(SyntaxLanguage language, std::string_view word) { static constexpr std::string_view common[] = { "break", "case", "continue", "default", "do", "else", "for", "if", "return", "switch", "while" }; if (std::find(std::begin(common), std::end(common), word) != std::end(common)) return true; switch (language) { case SyntaxLanguage::c: return wordIs(word, {"const", "enum", "extern", "goto", "register", "sizeof", "static", "struct", "typedef", "union", "volatile"}); case SyntaxLanguage::cpp: return wordIs(word, {"alignas", "auto", "class", "concept", "const", "constexpr", "consteval", "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", "enum", "explicit", "export", "extern", "friend", "inline", "mutable", "namespace", "new", "noexcept", "operator", "override", "private", "protected", "public", "requires", "sizeof", "static", "struct", "template", "this", "thread_local", "throw", "try", "typedef", "typename", "union", "using", "virtual", "volatile"}); case SyntaxLanguage::csharp: return wordIs(word, {"abstract", "as", "async", "await", "base", "class", "const", "delegate", "enum", "event", "explicit", "extern", "finally", "fixed", "foreach", "implicit", "in", "interface", "internal", "is", "lock", "namespace", "new", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "record", "ref", "sealed", "sizeof", "stackalloc", "static", "struct", "this", "throw", "try", "typeof", "unchecked", "unsafe", "using", "virtual", "volatile", "where", "yield"}); case SyntaxLanguage::lua: return wordIs(word, {"and", "elseif", "end", "false", "function", "goto", "in", "local", "nil", "not", "or", "repeat", "then", "true", "until"}); case SyntaxLanguage::python: return wordIs(word, {"and", "as", "assert", "async", "await", "class", "def", "del", "elif", "except", "False", "finally", "from", "global", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "True", "try", "with", "yield"}); case SyntaxLanguage::javascript: return wordIs(word, {"async", "await", "catch", "class", "const", "debugger", "delete", "export", "extends", "false", "finally", "from", "function", "get", "import", "in", "instanceof", "let", "new", "null", "of", "set", "static", "super", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "with", "yield"}); default: return false; } } bool isTypeWord(SyntaxLanguage language, std::string_view word) { if (language == SyntaxLanguage::python || language == SyntaxLanguage::lua || language == SyntaxLanguage::plain) return false; return wordIs(word, {"bool", "byte", "char", "decimal", "double", "dynamic", "float", "int", "int8_t", "int16_t", "int32_t", "int64_t", "long", "nint", "nuint", "object", "sbyte", "short", "signed", "size_t", "string", "uint", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "ulong", "unsigned", "ushort", "var", "void", "wchar_t"}); } bool isBuiltin(SyntaxLanguage language, std::string_view word) { switch (language) { case SyntaxLanguage::cpp: return wordIs(word, {"std", "move", "forward", "make_shared", "make_unique", "optional", "string_view", "vector"}); case SyntaxLanguage::csharp: return wordIs(word, {"Console", "DateTime", "Dictionary", "Enumerable", "List", "Task", "ValueTask"}); case SyntaxLanguage::lua: return wordIs(word, {"assert", "error", "ipairs", "io", "math", "os", "pairs", "pcall", "print", "require", "string", "table", "tonumber", "tostring", "type"}); case SyntaxLanguage::python: return wordIs(word, {"abs", "all", "any", "dict", "enumerate", "filter", "float", "int", "len", "list", "map", "max", "min", "object", "open", "print", "range", "reversed", "set", "str", "sum", "super", "tuple", "zip"}); case SyntaxLanguage::javascript: return wordIs(word, {"Array", "Boolean", "console", "Date", "Error", "JSON", "Map", "Math", "Number", "Object", "process", "Promise", "RegExp", "require", "Set", "String", "Symbol"}); default: return false; } } bool isLiteral(std::string_view word) { return wordIs(word, {"false", "False", "nil", "null", "nullptr", "None", "true", "True", "undefined"}); } bool isDeclarationKeyword(std::string_view word) { return wordIs(word, {"class", "concept", "delegate", "enum", "interface", "namespace", "record", "struct", "type"}); } bool isFunctionDeclarationKeyword(std::string_view word) { return word == "def" || word == "function"; } bool isMacroName(std::string_view word) { bool has_letter = false; for (const unsigned char character : word) { if (std::isalpha(character)) { has_letter = true; if (std::islower(character)) return false; } else if (!std::isdigit(character) && character != '_') { return false; } } return has_letter && word.size() > 1; } constexpr ImU32 syntax_normal = IM_COL32(218, 221, 226, 255); constexpr ImU32 syntax_keyword = IM_COL32(198, 139, 230, 255); constexpr ImU32 syntax_type = IM_COL32(91, 198, 190, 255); constexpr ImU32 syntax_string = IM_COL32(226, 166, 140, 255); constexpr ImU32 syntax_number = IM_COL32(181, 206, 126, 255); constexpr ImU32 syntax_comment = IM_COL32(112, 153, 105, 255); constexpr ImU32 syntax_function = IM_COL32(220, 199, 128, 255); constexpr ImU32 syntax_preprocessor = IM_COL32(205, 157, 222, 255); constexpr ImU32 syntax_member = IM_COL32(122, 184, 225, 255); constexpr ImU32 syntax_builtin = IM_COL32(103, 172, 232, 255); constexpr ImU32 syntax_constant = IM_COL32(214, 139, 102, 255); constexpr ImU32 syntax_operator = IM_COL32(105, 180, 210, 255); constexpr ImU32 syntax_decorator = IM_COL32(226, 190, 105, 255); constexpr ImU32 syntax_macro = IM_COL32(215, 128, 180, 255); [[maybe_unused]] void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text, SyntaxLanguage language, SyntaxState& state) { const auto drawSpan = [&](size_t begin, size_t end, ImU32 color) { if (end <= begin) return; const char* first = text.data() + begin; const char* last = text.data() + end; draw->AddText(position, color, first, last); position.x += ImGui::CalcTextSize(first, last, false).x; }; if (language == SyntaxLanguage::plain) { drawSpan(0, text.size(), syntax_normal); return; } const size_t first_non_space = text.find_first_not_of(" \t"); if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) && first_non_space != std::string::npos && text[first_non_space] == '#') { drawSpan(0, first_non_space, syntax_normal); drawSpan(first_non_space, text.size(), syntax_preprocessor); return; } size_t cursor = 0; std::string_view previous_word; while (cursor < text.size()) { if (state.continuation == SyntaxContinuation::block_comment) { const size_t end = text.find("*/", cursor); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; } drawSpan(cursor, end + 2, syntax_comment); cursor = end + 2; state.continuation = SyntaxContinuation::none; continue; } if (state.continuation == SyntaxContinuation::lua_comment) { const size_t end = text.find("]]", cursor); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); return; } drawSpan(cursor, end + 2, syntax_comment); cursor = end + 2; state.continuation = SyntaxContinuation::none; continue; } if (state.continuation == SyntaxContinuation::python_single || state.continuation == SyntaxContinuation::python_double) { const std::string_view delimiter = state.continuation == SyntaxContinuation::python_single ? "'''" : "\"\"\""; const size_t end = text.find(delimiter, cursor); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; } drawSpan(cursor, end + delimiter.size(), syntax_string); cursor = end + delimiter.size(); state.continuation = SyntaxContinuation::none; continue; } if (state.continuation == SyntaxContinuation::template_string) { const size_t end = text.find('`', cursor); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); return; } drawSpan(cursor, end + 1, syntax_string); cursor = end + 1; state.continuation = SyntaxContinuation::none; continue; } const bool slash_comments = language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp || language == SyntaxLanguage::javascript; if (slash_comments && text.compare(cursor, 2, "//") == 0) { drawSpan(cursor, text.size(), syntax_comment); return; } if (slash_comments && text.compare(cursor, 2, "/*") == 0) { const size_t end = text.find("*/", cursor + 2); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); state.continuation = SyntaxContinuation::block_comment; return; } drawSpan(cursor, end + 2, syntax_comment); cursor = end + 2; continue; } if (language == SyntaxLanguage::python && text[cursor] == '#') { drawSpan(cursor, text.size(), syntax_comment); return; } if (language == SyntaxLanguage::lua && text.compare(cursor, 4, "--[[") == 0) { const size_t end = text.find("]]", cursor + 4); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_comment); state.continuation = SyntaxContinuation::lua_comment; return; } drawSpan(cursor, end + 2, syntax_comment); cursor = end + 2; continue; } if (language == SyntaxLanguage::lua && text.compare(cursor, 2, "--") == 0) { drawSpan(cursor, text.size(), syntax_comment); return; } if (language == SyntaxLanguage::python && (text.compare(cursor, 3, "'''") == 0 || text.compare(cursor, 3, "\"\"\"") == 0)) { const std::string_view delimiter(text.data() + cursor, 3); const size_t end = text.find(delimiter, cursor + 3); if (end == std::string::npos) { drawSpan(cursor, text.size(), syntax_string); state.continuation = delimiter[0] == '\'' ? SyntaxContinuation::python_single : SyntaxContinuation::python_double; return; } drawSpan(cursor, end + 3, syntax_string); cursor = end + 3; continue; } if (text[cursor] == '\'' || text[cursor] == '"' || (language == SyntaxLanguage::javascript && text[cursor] == '`')) { const char quote = text[cursor]; size_t end = cursor + 1; bool escaped = false; for (; end < text.size(); ++end) { if (!escaped && text[end] == quote) { ++end; break; } escaped = !escaped && text[end] == '\\'; if (text[end] != '\\') escaped = false; } const bool closed = end <= text.size() && end > cursor + 1 && text[end - 1] == quote; drawSpan(cursor, end, syntax_string); if (quote == '`' && !closed) state.continuation = SyntaxContinuation::template_string; cursor = end; continue; } if (std::isdigit(static_cast(text[cursor]))) { size_t end = cursor + 1; while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '.' || text[end] == '_')) ++end; drawSpan(cursor, end, syntax_number); cursor = end; continue; } if (text[cursor] == '@' && cursor + 1 < text.size() && (std::isalpha(static_cast(text[cursor + 1])) || text[cursor + 1] == '_')) { size_t end = cursor + 2; while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '_' || text[end] == '.')) ++end; drawSpan(cursor, end, syntax_decorator); cursor = end; continue; } if (std::isalpha(static_cast(text[cursor])) || text[cursor] == '_') { size_t end = cursor + 1; while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '_')) ++end; const std::string_view word(text.data() + cursor, end - cursor); size_t next = end; while (next < text.size() && std::isspace(static_cast(text[next]))) ++next; size_t previous = cursor; while (previous > 0 && std::isspace(static_cast(text[previous - 1]))) --previous; const bool member_access = previous > 0 && (text[previous - 1] == '.' || (text[previous - 1] == '>' && previous > 1 && text[previous - 2] == '-') || (text[previous - 1] == ':' && previous > 1 && text[previous - 2] == ':')); const bool declared_name = isDeclarationKeyword(previous_word); const bool declared_function = isFunctionDeclarationKeyword(previous_word); const bool capitalized_type = (language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) && std::isupper(static_cast(word.front())) && !member_access && (next >= text.size() || text[next] != '('); const bool likely_type = declared_name || isTypeWord(language, word) || capitalized_type; const ImU32 color = isLiteral(word) ? syntax_constant : isKeyword(language, word) ? syntax_keyword : declared_function ? syntax_function : declared_name || likely_type ? syntax_type : isMacroName(word) ? syntax_macro : isBuiltin(language, word) ? syntax_builtin : member_access ? (next < text.size() && text[next] == '(' ? syntax_function : syntax_member) : next < text.size() && text[next] == '(' ? syntax_function : syntax_normal; drawSpan(cursor, end, color); previous_word = word; cursor = end; continue; } if (std::string_view("+-*/%=!<>&|^~?:").find(text[cursor]) != std::string_view::npos) { size_t end = cursor + 1; while (end < text.size() && std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos) ++end; drawSpan(cursor, end, syntax_operator); cursor = end; continue; } size_t end = cursor + 1; while (end < text.size()) { const unsigned char value = static_cast(text[end]); const bool token_start = std::isalnum(value) || text[end] == '_' || text[end] == '\'' || text[end] == '"' || (language == SyntaxLanguage::javascript && text[end] == '`') || (slash_comments && text[end] == '/') || (language == SyntaxLanguage::python && text[end] == '#') || (language == SyntaxLanguage::lua && text[end] == '-') || text[end] == '@' || std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos; if (token_start) break; ++end; } drawSpan(cursor, end, syntax_normal); cursor = end; } } bool compactButton(const char* label, bool active = false) { if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f)); const bool icon_only = label && std::strlen(label) <= 4; const bool clicked = icon_only ? ImGui::Button(label, {ImGui::GetFrameHeight(), ImGui::GetFrameHeight()}) : ImGui::SmallButton(label); if (active) ImGui::PopStyleColor(); return clicked; } bool toolbarSegmentButton(const char* label, bool active, float width) { ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); if (active) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.29f, 0.49f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.12f, 0.22f, 0.39f, 1.0f)); } const bool clicked = ImGui::Button(label, {width, 0.0f}); if (active) ImGui::PopStyleColor(3); ImGui::PopStyleVar(); return clicked; } int wrappedLineCount(std::string_view text, int wrap_columns) { if (wrap_columns <= 0 || static_cast(text.size()) <= wrap_columns) return 1; int lines = 0; size_t cursor = 0; while (cursor < text.size()) { size_t remaining = text.size() - cursor; if (remaining <= static_cast(wrap_columns)) { ++lines; break; } size_t split = cursor + static_cast(wrap_columns); size_t break_at = text.rfind(' ', split); if (break_at == std::string_view::npos || break_at < cursor + static_cast(wrap_columns / 3)) break_at = split; cursor = break_at; while (cursor < text.size() && text[cursor] == ' ') ++cursor; ++lines; } return std::max(lines, 1); } struct MinimapSegment { float weight = 0.0f; ImU32 color = syntax_normal; }; struct MinimapEntry { std::vector segments; float units = 1.0f; }; void addMinimapSegment(std::vector& segments, float weight, ImU32 color) { if (weight <= 0.0f) return; if (!segments.empty() && segments.back().color == color) { segments.back().weight += weight; return; } segments.push_back({weight, color}); } std::vector buildMinimapSegments(const std::string& text, SyntaxLanguage language) { std::vector segments; if (text.empty()) { segments.push_back({0.10f, IM_COL32(0, 0, 0, 0)}); return segments; } size_t cursor = 0; bool seen_identifier = false; while (cursor < text.size()) { const unsigned char value = static_cast(text[cursor]); if (std::isspace(value)) { size_t end = cursor + 1; while (end < text.size() && std::isspace(static_cast(text[end]))) ++end; if (segments.empty()) { cursor = end; continue; } addMinimapSegment(segments, static_cast(end - cursor) * 0.22f, IM_COL32(0, 0, 0, 0)); cursor = end; continue; } if ((language == SyntaxLanguage::c || language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) && cursor + 1 < text.size() && text[cursor] == '/' && text[cursor + 1] == '/') { addMinimapSegment(segments, std::max(0.18f, static_cast(text.size() - cursor) * 0.42f), syntax_comment); break; } if (language == SyntaxLanguage::python && text[cursor] == '#') { addMinimapSegment(segments, std::max(0.18f, static_cast(text.size() - cursor) * 0.42f), syntax_comment); break; } if (language == SyntaxLanguage::lua && cursor + 1 < text.size() && text[cursor] == '-' && text[cursor + 1] == '-') { addMinimapSegment(segments, std::max(0.18f, static_cast(text.size() - cursor) * 0.42f), syntax_comment); break; } if (text[cursor] == '"' || text[cursor] == '\'' || (language == SyntaxLanguage::javascript && text[cursor] == '`')) { const char delimiter = text[cursor]; size_t end = cursor + 1; while (end < text.size()) { if (text[end] == delimiter && text[end - 1] != '\\') { ++end; break; } ++end; } addMinimapSegment(segments, std::max(0.10f, static_cast(end - cursor) * 0.34f), syntax_string); cursor = end; continue; } if (std::isdigit(value)) { size_t end = cursor + 1; while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '.' || text[end] == '_')) ++end; addMinimapSegment(segments, std::max(0.08f, static_cast(end - cursor) * 0.28f), syntax_number); cursor = end; continue; } if (std::isalpha(value) || text[cursor] == '_') { size_t end = cursor + 1; while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '_')) ++end; const std::string_view word(text.data() + cursor, end - cursor); ImU32 color = syntax_normal; if (isKeyword(language, word)) color = syntax_keyword; else if (isTypeWord(language, word)) color = syntax_type; else if (isBuiltin(language, word)) color = syntax_builtin; else if (isLiteral(word)) color = syntax_constant; else if (word.front() == '@') color = syntax_decorator; else if (isMacroName(word)) color = syntax_macro; else if (!seen_identifier && (isDeclarationKeyword(word) || isFunctionDeclarationKeyword(word))) color = syntax_keyword; else if (!seen_identifier && cursor > 0 && text[cursor - 1] == '.') color = syntax_member; else if (end < text.size() && text[end] == '(') color = syntax_function; addMinimapSegment(segments, std::max(0.08f, static_cast(end - cursor) * 0.26f), color); seen_identifier = true; cursor = end; continue; } if (std::string_view("+-*/%=!<>&|^~?:#").find(text[cursor]) != std::string_view::npos) { size_t end = cursor + 1; while (end < text.size() && std::string_view("+-*/%=!<>&|^~?:#").find(text[end]) != std::string_view::npos) ++end; const ImU32 color = text[cursor] == '#' ? syntax_preprocessor : syntax_operator; addMinimapSegment(segments, std::max(0.06f, static_cast(end - cursor) * 0.22f), color); cursor = end; continue; } addMinimapSegment(segments, 0.05f, syntax_normal); ++cursor; } if (segments.empty()) segments.push_back({0.10f, syntax_normal}); return segments; } ImU32 dimMinimapColor(ImU32 color) { ImVec4 rgba = ImGui::ColorConvertU32ToFloat4(color); rgba.x *= 0.58f; rgba.y *= 0.58f; rgba.z *= 0.58f; rgba.w *= 0.82f; return ImGui::ColorConvertFloat4ToU32(rgba); } float minimapTotalUnits(const std::vector& entries) { float total_units = 0.0f; for (const MinimapEntry& entry : entries) total_units += std::max(0.10f, entry.units); return std::max(1.0f, total_units); } std::optional drawMinimap(const std::vector& entries, const ImVec2& viewport_size, float scale, float rendered_content_height, float visible_start, float visible_height, float max_scroll_y, bool& drag_active, float& drag_offset, float content_height_override = 0.0f) { const ImVec2 minimum = ImGui::GetCursorScreenPos(); const float total_units = minimapTotalUnits(entries); const float min_unit_height = scaled(0.72f, scale); const float ideal_unit_height = scaled(2.05f, scale); const float unit_height = content_height_override > 0.0f ? std::max(min_unit_height, content_height_override / total_units) : ideal_unit_height; const float content_height = std::max(content_height_override, total_units * unit_height); const ImVec2 content_size{viewport_size.x, content_height}; ImGui::InvisibleButton("##code_minimap", content_size); const bool hovered = ImGui::IsItemHovered(); const bool active = ImGui::IsItemActive(); ImDrawList* draw = ImGui::GetWindowDrawList(); draw->AddRectFilled(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y}, IM_COL32(26, 28, 33, 188), scaled(3.0f, scale)); draw->AddRect(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y}, hovered || active ? IM_COL32(70, 76, 88, 180) : IM_COL32(46, 50, 58, 140), scaled(3.0f, scale)); if (entries.empty() || content_size.y <= 1.0f) return std::nullopt; const float content_left = minimum.x + scaled(5.0f, scale); const float content_right = minimum.x + viewport_size.x - scaled(5.0f, scale); const float content_width = std::max(1.0f, content_right - content_left); const float line_height = std::max(scaled(0.65f, scale), std::min(scaled(2.1f, scale), unit_height)); float y = minimum.y; for (const MinimapEntry& entry : entries) { const float entry_units = std::max(0.10f, entry.units); const float total_weight = std::max(0.10f, std::accumulate(entry.segments.begin(), entry.segments.end(), 0.0f, [](float sum, const MinimapSegment& segment) { return sum + std::max(0.0f, segment.weight); })); float x = content_left; for (const MinimapSegment& segment : entry.segments) { if (segment.color == IM_COL32(0, 0, 0, 0)) { x += content_width * (segment.weight / total_weight); continue; } const float width = std::max(scaled(1.0f, scale), content_width * (segment.weight / total_weight)); draw->AddRectFilled({x, y}, {std::min(content_right, x + width), y + line_height}, dimMinimapColor(segment.color), scaled(1.0f, scale)); x += width; } y += entry_units * unit_height; } const float rendered_height = std::max(rendered_content_height, 1.0f); const float clamped_visible_height = std::clamp(visible_height, 0.0f, rendered_height); const float clamped_visible_start = std::clamp(visible_start, 0.0f, std::max(0.0f, rendered_height - clamped_visible_height)); const float viewport_height = std::clamp(content_size.y * (clamped_visible_height / rendered_height), scaled(14.0f, scale), content_size.y); const float viewport_y = minimum.y + content_size.y * (clamped_visible_start / rendered_height); const ImVec2 viewport_min{minimum.x + scaled(1.0f, scale), viewport_y}; const ImVec2 viewport_max{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height}; const bool viewport_hovered = ImGui::IsMouseHoveringRect(viewport_min, viewport_max); if (ImGui::IsItemActivated() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (viewport_hovered) { drag_active = true; drag_offset = ImGui::GetIO().MousePos.y - viewport_y; } else { drag_active = false; const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f); return std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y); } } if (drag_active) { if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { drag_active = false; } else { const float desired_viewport_y = std::clamp(ImGui::GetIO().MousePos.y - drag_offset, minimum.y, minimum.y + content_size.y - viewport_height); const float travel = std::max(1.0f, content_size.y - viewport_height); const float scroll_ratio = (desired_viewport_y - minimum.y) / travel; return std::clamp(scroll_ratio * max_scroll_y, 0.0f, max_scroll_y); } } else if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f); return std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y); } draw->AddRectFilled(viewport_min, viewport_max, IM_COL32(109, 129, 170, hovered || active || drag_active ? 52 : 28), scaled(2.0f, scale)); draw->AddRect(viewport_min, viewport_max, IM_COL32(120, 138, 170, drag_active ? 180 : 120), scaled(2.0f, scale)); return std::nullopt; } void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax, float scale, ImU32 background = IM_COL32(0, 0, 0, 0), float left_gutter = 0.0f, float minimum_width = 0.0f, float custom_row_height = 0.0f, int wrap_columns = 0) { const float base_row_height = scaled(21.0f, scale); const int visual_lines = wrappedLineCount(text, wrap_columns); const float row_height = custom_row_height > 0.0f ? custom_row_height : base_row_height * static_cast(visual_lines); const ImVec2 start = ImGui::GetCursorScreenPos(); const float width = minimum_width > 0.0f ? minimum_width : std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale)); ImGui::InvisibleButton("##code_line", {width, row_height}); const ImVec2 minimum = ImGui::GetItemRectMin(); const ImVec2 maximum = ImGui::GetItemRectMax(); ImDrawList* draw = ImGui::GetWindowDrawList(); if ((background & IM_COL32_A_MASK) != 0) draw->AddRectFilled(minimum, maximum, background); if (wrap_columns <= 0 || static_cast(text.size()) <= wrap_columns) { drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax); return; } size_t cursor = 0; int line_index = 0; while (cursor < text.size()) { size_t remaining = text.size() - cursor; size_t chunk_end = text.size(); if (remaining > static_cast(wrap_columns)) { chunk_end = cursor + static_cast(wrap_columns); size_t break_at = text.rfind(' ', chunk_end); if (break_at == std::string::npos || break_at < cursor + static_cast(wrap_columns / 3)) break_at = chunk_end; chunk_end = break_at; } const std::string chunk = text.substr(cursor, chunk_end - cursor); drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale) + base_row_height * static_cast(line_index)}, chunk, language, syntax); cursor = chunk_end; while (cursor < text.size() && text[cursor] == ' ') ++cursor; ++line_index; } } void drawCodeLineNumber(int value, float x, float y, ImU32 color) { if (value <= 0) return; const std::string text = std::to_string(value); ImGui::GetWindowDrawList()->AddText({x, y}, color, text.c_str()); } } void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path, bool staged, std::string& notice) { path_ = path; commit_id_.clear(); staged_ = staged; mode_ = Mode::diff; scroll_to_top_ = true; reload(repository, manager, notice); } void DiffViewer::openCommit(RepositoryView& repository, GitManager& manager, const std::string& path, const std::string& commit_id, std::string& notice) { path_ = path; commit_id_ = commit_id; staged_ = false; mode_ = Mode::diff; scroll_to_top_ = true; reload(repository, manager, notice); if (hunks_.empty()) { mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice); scroll_to_top_ = true; } } void DiffViewer::close() { path_.clear(); commit_id_.clear(); file_header_.clear(); hunks_.clear(); file_lines_.clear(); blame_lines_.clear(); history_entries_.clear(); } void DiffViewer::parseDiff(const std::string& text) { file_header_.clear(); hunks_.clear(); Hunk* current = nullptr; int old_line = 0; int new_line = 0; for (const std::string& raw : splitLines(text)) { if (raw.starts_with("@@")) { hunks_.push_back({}); current = &hunks_.back(); current->header = raw; parseRange(raw, '-', old_line); parseRange(raw, '+', new_line); current->patch = file_header_ + raw + "\n"; continue; } if (!current) { file_header_ += raw + "\n"; continue; } Line line; line.raw = raw; line.text = raw.empty() ? std::string{} : raw.substr(1); if (!raw.empty() && raw[0] == '+') { line.kind = LineKind::added; line.new_number = new_line++; } else if (!raw.empty() && raw[0] == '-') { line.kind = LineKind::removed; line.old_number = old_line++; } else if (!raw.starts_with("\\ No newline")) { line.old_number = old_line++; line.new_number = new_line++; } current->lines.push_back(std::move(line)); current->patch += raw + "\n"; } } void DiffViewer::parseBlame(const std::string& text) { blame_lines_.clear(); const std::vector lines = splitLines(text); std::string previous_hash; for (size_t index = 0; index < lines.size();) { std::istringstream header(lines[index++]); BlameLine line; int original_line = 0; if (!(header >> line.hash >> original_line >> line.line_number)) continue; if (line.hash.size() < 8 || !std::all_of(line.hash.begin(), line.hash.end(), [](unsigned char value) { return std::isxdigit(value) != 0; })) continue; std::time_t author_time = 0; while (index < lines.size()) { const std::string& field = lines[index++]; if (!field.empty() && field.front() == '\t') { line.text = field.substr(1); break; } const size_t separator = field.find(' '); const std::string_view key(field.data(), separator == std::string::npos ? field.size() : separator); const std::string value = separator == std::string::npos ? std::string{} : field.substr(separator + 1); if (key == "author") line.author = value; else if (key == "author-mail") { line.email = value; if (line.email.size() >= 2 && line.email.front() == '<' && line.email.back() == '>') line.email = line.email.substr(1, line.email.size() - 2); } else if (key == "author-time") { try { author_time = static_cast(std::stoll(value)); } catch (...) { author_time = 0; } } else if (key == "summary") line.summary = value; } if (author_time != 0) { std::tm date{}; #ifdef _WIN32 localtime_s(&date, &author_time); #else localtime_r(&author_time, &date); #endif std::ostringstream formatted; formatted << std::put_time(&date, "%Y-%m-%d"); line.date = formatted.str(); } line.show_attribution = line.hash != previous_hash; previous_hash = line.hash; blame_lines_.push_back(std::move(line)); } } void DiffViewer::parseHistory(const std::string& text) { history_entries_.clear(); for (const std::string& line : splitLines(text)) { if (line.empty()) continue; HistoryEntry entry; size_t cursor = 0; auto consume_field = [&](std::string& out) { const size_t separator = line.find('\x1f', cursor); if (separator == std::string::npos) { out = line.substr(cursor); cursor = line.size(); return; } out = line.substr(cursor, separator - cursor); cursor = separator + 1; }; consume_field(entry.hash); consume_field(entry.date); consume_field(entry.author); consume_field(entry.summary); if (!entry.hash.empty() || !entry.summary.empty()) history_entries_.push_back(std::move(entry)); } } void DiffViewer::reload(RepositoryView& repository, GitManager& manager, std::string& notice) { std::vector arguments; if (commit_id_.empty()) { arguments = {"diff", "--no-ext-diff", "--no-color", "--unified=3"}; if (staged_) arguments.push_back("--cached"); } else { arguments = {"show", "--first-parent", "--format=", "--no-ext-diff", "--no-color", "--unified=3", commit_id_}; } arguments.insert(arguments.end(), {"--", path_}); std::string output; std::string error; if (!manager.captureGit(repository, arguments, output, error)) notice = error; parseDiff(output); if (hunks_.empty() && commit_id_.empty()) { const auto file = std::find_if(repository.working_files.begin(), repository.working_files.end(), [this](const WorkingFile& item) { return item.path == path_; }); if (file != repository.working_files.end() && file->kind == FileChangeKind::added && !staged_) { std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary); std::ostringstream contents; contents << stream.rdbuf(); const auto lines = splitLines(contents.str()); file_header_ = "diff --git a/" + path_ + " b/" + path_ + "\nnew file mode 100644\n--- /dev/null\n+++ b/" + path_ + "\n"; Hunk hunk; hunk.header = "@@ -0,0 +1," + std::to_string(lines.size()) + " @@"; hunk.patch = file_header_ + hunk.header + "\n"; int number = 1; for (const auto& text : lines) { hunk.lines.push_back({0, number++, LineKind::added, text, "+" + text}); hunk.patch += "+" + text + "\n"; } hunks_.push_back(std::move(hunk)); } } file_lines_.clear(); blame_lines_.clear(); history_entries_.clear(); if (mode_ != Mode::diff) loadSupplement(repository, manager, mode_, notice); } void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice) { std::string output; std::string error; if (mode == Mode::file) { if (!commit_id_.empty()) { if (!manager.captureGit(repository, {"show", commit_id_ + ":" + path_}, output, error)) notice = error; } else if (staged_) { if (!manager.captureGit(repository, {"show", ":" + path_}, output, error)) notice = error; } else { std::ifstream stream(std::filesystem::path(repository.path) / path_, std::ios::binary); std::ostringstream contents; contents << stream.rdbuf(); output = contents.str(); } file_lines_ = splitLines(output); } else if (mode == Mode::blame) { std::vector arguments{"blame", "--line-porcelain"}; if (!commit_id_.empty()) arguments.push_back(commit_id_); arguments.insert(arguments.end(), {"--", path_}); if (!manager.captureGit(repository, arguments, output, error)) notice = error; parseBlame(output); } else if (mode == Mode::history) { std::vector arguments{ "log", "--follow", "--date=short", "--pretty=format:%h%x1f%ad%x1f%an%x1f%s"}; if (!commit_id_.empty()) arguments.push_back(commit_id_); arguments.insert(arguments.end(), {"--", path_}); if (!manager.captureGit(repository, arguments, output, error)) notice = error; parseHistory(output); } } void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars, float scale, ImFont* code_font, std::string& notice) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)}); ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_TB_PEN); ImGui::SameLine(0, scaled(7, scale)); ImGui::TextUnformatted(path_.c_str()); const bool historical = !commit_id_.empty(); const float top_right_width = scaled(72.0f, scale); ImGui::SameLine(std::max(scaled(160.0f, scale), ImGui::GetWindowWidth() - top_right_width)); ImGui::TextDisabled("UTF-8"); ImGui::SameLine(0, scaled(10.0f, scale)); if (compactButton(ICON_TB_XMARK)) close(); ImGui::Separator(); const float segment_width = scaled(86.0f, scale); const float file_group_width = segment_width * 2.0f; const float blame_group_width = segment_width * 2.0f; const float wrap_width = scaled(34.0f, scale); const float between_groups = scaled(22.0f, scale); const float wrap_gap = scaled(12.0f, scale); const float toolbar_width = file_group_width + between_groups + blame_group_width + wrap_gap + wrap_width; ImGui::SetCursorPosX(std::max(0.0f, (ImGui::GetWindowWidth() - toolbar_width) * 0.5f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0.0f, ImGui::GetStyle().ItemSpacing.y}); if (toolbarSegmentButton("File View", mode_ == Mode::file, segment_width)) { mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice); scroll_to_top_ = true; } ImGui::SameLine(); if (toolbarSegmentButton("Diff View", mode_ == Mode::diff, segment_width)) { mode_ = Mode::diff; scroll_to_top_ = true; } ImGui::SameLine(0, between_groups); if (toolbarSegmentButton("Blame", mode_ == Mode::blame, segment_width)) { mode_ = Mode::blame; loadSupplement(repository, manager, mode_, notice); } ImGui::SameLine(); if (toolbarSegmentButton("History", mode_ == Mode::history, segment_width)) { mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice); } ImGui::PopStyleVar(); ImGui::SameLine(0, wrap_gap); toolbarSegmentButton(ICON_TB_BARS, line_wrap_, wrap_width); if (ImGui::IsItemClicked()) line_wrap_ = !line_wrap_; if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", line_wrap_ ? "Disable line wrap" : "Enable line wrap"); ImGui::Separator(); const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file || mode_ == Mode::blame || mode_ == Mode::history; const float minimap_width = scaled(168.0f, scale); const float minimap_gap = scaled(10.0f, scale); const float scrollbar_width = ImGui::GetStyle().ScrollbarSize; const SyntaxLanguage language = languageForPath(path_); float main_scroll_y = 0.0f; float main_window_height = 0.0f; float rendered_content_height = 0.0f; std::vector minimap_entries; if (show_minimap) { minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.65f}); if (mode_ == Mode::diff) { for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) { if (hunk_index > 0) { minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.55f}); minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.45f}); } for (const Line& line : hunks_[hunk_index].lines) { minimap_entries.push_back({buildMinimapSegments(line.text, language), line.kind == LineKind::context ? 1.0f : 1.0f}); } minimap_entries.push_back({{{0.10f, IM_COL32(0, 0, 0, 0)}}, 0.30f}); } } else if (mode_ == Mode::file) { minimap_entries.reserve(file_lines_.size()); for (const std::string& line : file_lines_) minimap_entries.push_back({buildMinimapSegments(line, language), 1.0f}); } else if (mode_ == Mode::blame) { minimap_entries.reserve(blame_lines_.size()); for (const BlameLine& line : blame_lines_) minimap_entries.push_back({buildMinimapSegments(line.text, language), line.show_attribution ? 1.35f : 1.0f}); } else { minimap_entries.reserve(history_entries_.size()); for (const HistoryEntry& entry : history_entries_) minimap_entries.push_back({buildMinimapSegments(entry.summary, SyntaxLanguage::plain), 1.7f}); } } ImGuiWindowFlags content_flags = line_wrap_ ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; const bool request_scroll_to_top = scroll_to_top_; if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f}); ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, content_flags); scroll_to_top_ = false; const bool use_code_font = code_font && mode_ != Mode::history; if (use_code_font) ImGui::PushFont(code_font, 0.0f); const float row_height = scaled(21, scale); const ImVec2 child_minimum = ImGui::GetWindowPos(); const ImVec2 child_size = ImGui::GetWindowSize(); const float content_width = std::max( ImGui::GetContentRegionAvail().x - (show_minimap ? minimap_width + minimap_gap + scrollbar_width + scaled(6.0f, scale) : 0.0f), scaled(200.0f, scale)); const float wrapable_text_width = std::max(scaled(32.0f, scale), content_width - scaled(12.0f, scale)); const float code_character_width = ImGui::CalcTextSize("M").x; const int wrap_columns = line_wrap_ && code_character_width > 0.0f ? std::max(12, static_cast(wrapable_text_width / code_character_width)) : 0; // Keep the first source row clear of the toolbar and aligned with a normal row inset. ImGui::Dummy({0.0f, row_height}); if (mode_ == Mode::diff) { if (hunks_.empty()) ImGui::TextDisabled("No textual diff is available for this file."); int line_id = 0; int pending_hunk = -1; enum class HunkAction { none, stage, discard, unstage }; HunkAction pending_action = HunkAction::none; for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) { SyntaxState old_syntax; SyntaxState new_syntax; ImGui::PushID(static_cast(hunk_index)); if (hunk_index > 0) { ImGui::Dummy({0.0f, scaled(11.0f, scale)}); ImGui::Separator(); ImGui::Dummy({0.0f, scaled(8.0f, scale)}); } ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.62f, 0.64f, 0.68f, 1.0f)); ImGui::TextUnformatted(hunks_[hunk_index].header.c_str()); ImGui::PopStyleColor(); ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale))); if (!historical && !staged_) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1)); if (ImGui::SmallButton("Discard Hunk")) { pending_hunk = static_cast(hunk_index); pending_action = HunkAction::discard; } ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1)); if (ImGui::SmallButton("Stage Hunk")) { pending_hunk = static_cast(hunk_index); pending_action = HunkAction::stage; } ImGui::PopStyleColor(); } else if (!historical && ImGui::SmallButton("Unstage Hunk")) { pending_hunk = static_cast(hunk_index); pending_action = HunkAction::unstage; } ImGui::Separator(); ImGui::Dummy({0.0f, scaled(4.0f, scale)}); const float number_column = scaled(44.0f, scale); const float code_gutter = number_column * 2.0f + scaled(14.0f, scale); for (const Line& line : hunks_[hunk_index].lines) { ImGui::PushID(line_id++); const ImVec2 line_minimum = ImGui::GetCursorScreenPos(); const ImU32 background = line.kind == LineKind::added ? IM_COL32(30, 68, 46, 170) : line.kind == LineKind::removed ? IM_COL32(86, 38, 42, 170) : IM_COL32(0, 0, 0, 0); drawCodeLine(line.text, language, line.kind == LineKind::removed ? old_syntax : new_syntax, scale, background, code_gutter, content_width, 0.0f, wrap_columns); const float text_y = line_minimum.y + scaled(2.0f, scale); drawCodeLineNumber(line.old_number, line_minimum.x + scaled(6.0f, scale), text_y, IM_COL32(126, 132, 142, 255)); drawCodeLineNumber(line.new_number, line_minimum.x + number_column + scaled(6.0f, scale), text_y, IM_COL32(126, 132, 142, 255)); ImGui::PopID(); } ImGui::PopID(); } if (pending_hunk >= 0 && pending_hunk < static_cast(hunks_.size())) { const bool cached = pending_action != HunkAction::discard; const bool reverse = pending_action != HunkAction::stage; if (manager.applyPatch(repository, hunks_[pending_hunk].patch, cached, reverse, notice)) { if (pending_action == HunkAction::stage) close(); reload(repository, manager, notice); } } } else if (mode_ == Mode::blame) { if (blame_lines_.empty()) { ImGui::TextDisabled("No blame data is available for this file."); } else { SyntaxState syntax; const float info_width = scaled(34.0f, scale); const float line_number_width = scaled(48.0f, scale); const float code_gutter = info_width + line_number_width + scaled(14.0f, scale); for (size_t index = 0; index < blame_lines_.size(); ++index) { ImGui::PushID(static_cast(index)); const BlameLine& line = blame_lines_[index]; const ImVec2 line_minimum = ImGui::GetCursorScreenPos(); const ImU32 accent = blameColor(line.hash, line.show_attribution ? 255 : 190); const ImU32 background = line.show_attribution ? IM_COL32(39, 42, 50, 150) : IM_COL32(31, 34, 40, 105); const float blame_row_height = scaled(21.0f, scale); drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width, blame_row_height, wrap_columns); ImDrawList* draw = ImGui::GetWindowDrawList(); const ImVec2 line_maximum = ImGui::GetItemRectMax(); draw->AddRectFilled({line_minimum.x, line_minimum.y}, {line_minimum.x + scaled(3.0f, scale), line_maximum.y}, accent); drawCodeLineNumber(line.line_number, line_minimum.x + info_width + scaled(6.0f, scale), line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255)); if (line.show_attribution) { const float avatar_size = scaled(14.0f, scale); const ImVec2 avatar_min{line_minimum.x + scaled(10.0f, scale), line_minimum.y + scaled(3.0f, scale)}; const unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0; if (avatar_texture) { draw->AddImageRounded(ImTextureRef(static_cast(avatar_texture)), avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size}, {0, 0}, {1, 1}, IM_COL32_WHITE, scaled(3.0f, scale)); } else { draw->AddRectFilled(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size}, accent, scaled(3.0f, scale)); } if (ImGui::IsMouseHoveringRect(avatar_min, {avatar_min.x + avatar_size, avatar_min.y + avatar_size})) { const std::string author = line.author.empty() ? "Unknown author" : line.author; ImGui::BeginTooltip(); ImGui::TextUnformatted(author.c_str()); if (!line.summary.empty()) ImGui::TextDisabled("%s", line.summary.c_str()); if (!line.date.empty()) ImGui::TextDisabled("%s", line.date.c_str()); ImGui::EndTooltip(); } } else { draw->AddText({line_minimum.x + scaled(16.0f, scale), line_minimum.y + scaled(2.0f, scale)}, IM_COL32(92, 99, 110, 255), "..."); } ImGui::PopID(); } } } else if (mode_ == Mode::file) { const std::vector* lines = &file_lines_; if (mode_ == Mode::file) { SyntaxState syntax; const float code_gutter = scaled(52.0f, scale); for (size_t index = 0; index < lines->size(); ++index) { ImGui::PushID(static_cast(index)); const ImVec2 line_minimum = ImGui::GetCursorScreenPos(); drawCodeLine((*lines)[index], language, syntax, scale, IM_COL32(0, 0, 0, 0), code_gutter, content_width, 0.0f, wrap_columns); drawCodeLineNumber(static_cast(index + 1), line_minimum.x + scaled(6.0f, scale), line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255)); ImGui::PopID(); } } if (lines->empty()) ImGui::TextDisabled("No data is available for this view."); } else { if (history_entries_.empty()) { ImGui::TextDisabled("No history data is available for this file."); } else { for (size_t index = 0; index < history_entries_.size(); ++index) { ImGui::PushID(static_cast(index)); const HistoryEntry& entry = history_entries_[index]; const ImVec2 row_min = ImGui::GetCursorScreenPos(); const float history_row_height = scaled(36.0f, scale); ImGui::InvisibleButton("##history_row", {content_width, history_row_height}); const ImVec2 row_max = ImGui::GetItemRectMax(); ImDrawList* draw = ImGui::GetWindowDrawList(); const bool hovered = ImGui::IsItemHovered(); draw->AddRectFilled(row_min, row_max, hovered ? IM_COL32(49, 53, 61, 180) : IM_COL32(37, 40, 47, 130), scaled(4.0f, scale)); draw->AddRectFilled(row_min, {row_min.x + scaled(3.0f, scale), row_max.y}, IM_COL32(23, 181, 204, 220), scaled(3.0f, scale)); const std::string hash_text = entry.hash.empty() ? "-" : entry.hash; draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(4.0f, scale)}, IM_COL32(96, 184, 235, 255), hash_text.c_str()); const std::string author_date = entry.author + (entry.date.empty() ? std::string{} : std::string(" ") + entry.date); draw->AddText({row_min.x + scaled(78.0f, scale), row_min.y + scaled(4.0f, scale)}, IM_COL32(188, 192, 198, 255), author_date.c_str()); const std::string summary = entry.summary.empty() ? "(no summary)" : entry.summary; draw->AddText({row_min.x + scaled(10.0f, scale), row_min.y + scaled(18.0f, scale)}, IM_COL32(219, 223, 229, 255), summary.c_str()); ImGui::PopID(); } } } main_scroll_y = ImGui::GetScrollY(); main_window_height = ImGui::GetWindowHeight(); rendered_content_height = ImGui::GetCursorPosY(); const float max_scroll_y = ImGui::GetScrollMaxY(); if (show_minimap) { const float total_units = minimapTotalUnits(minimap_entries); const float min_unit_height = scaled(0.72f, scale); const float ideal_unit_height = scaled(2.05f, scale); const float max_minimap_height = std::max(scaled(24.0f, scale), child_size.y - scaled(8.0f, scale)); const float fitted_unit_height = std::clamp(max_minimap_height / total_units, min_unit_height, ideal_unit_height); const float minimap_content_height = total_units * fitted_unit_height; const float minimap_height = std::min(max_minimap_height, minimap_content_height); const float minimap_x = child_minimum.x + child_size.x - scrollbar_width - minimap_width - scaled(4.0f, scale); const float minimap_y = child_minimum.y + scaled(4.0f, scale); ImGui::SetCursorScreenPos({minimap_x, minimap_y}); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.28f, 0.31f, 0.37f, 0.55f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.36f, 0.40f, 0.47f, 0.72f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.44f, 0.49f, 0.57f, 0.86f)); if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f}); ImGui::BeginChild("diff_content_minimap", {minimap_width, minimap_height}, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove); const auto requested_scroll = drawMinimap(minimap_entries, {minimap_width - scaled(1.0f, scale), minimap_height}, scale, rendered_content_height, main_scroll_y, main_window_height, max_scroll_y, minimap_drag_active_, minimap_drag_offset_, minimap_content_height); ImGui::EndChild(); ImGui::PopStyleColor(5); if (requested_scroll) ImGui::SetScrollY(*requested_scroll); } if (use_code_font) ImGui::PopFont(); ImGui::EndChild(); ImGui::EndChild(); ImGui::PopStyleVar(); }