1213 lines
60 KiB
C++
1213 lines
60 KiB
C++
#include "ui/diff_viewer.h"
|
|
|
|
#include "managers/avatar_cache.h"
|
|
#include "managers/git_manager.h"
|
|
#include "models/repository.h"
|
|
|
|
#include <IconsTabler.h>
|
|
#include <imgui.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <initializer_list>
|
|
#include <numeric>
|
|
#include <sstream>
|
|
#include <string_view>
|
|
|
|
namespace {
|
|
float scaled(float value, float scale) { return value * scale; }
|
|
|
|
std::vector<std::string> splitLines(const std::string& text) {
|
|
std::vector<std::string> 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<unsigned char>(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<ImU32>(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<char>(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<std::string_view> 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<unsigned char>(text[cursor]))) {
|
|
size_t end = cursor + 1;
|
|
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(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<unsigned char>(text[cursor + 1])) || text[cursor + 1] == '_')) {
|
|
size_t end = cursor + 2;
|
|
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
|
|
text[end] == '_' || text[end] == '.')) ++end;
|
|
drawSpan(cursor, end, syntax_decorator);
|
|
cursor = end;
|
|
continue;
|
|
}
|
|
if (std::isalpha(static_cast<unsigned char>(text[cursor])) || text[cursor] == '_') {
|
|
size_t end = cursor + 1;
|
|
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(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<unsigned char>(text[next]))) ++next;
|
|
size_t previous = cursor;
|
|
while (previous > 0 && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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<int>(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<size_t>(wrap_columns)) {
|
|
++lines;
|
|
break;
|
|
}
|
|
size_t split = cursor + static_cast<size_t>(wrap_columns);
|
|
size_t break_at = text.rfind(' ', split);
|
|
if (break_at == std::string_view::npos || break_at < cursor + static_cast<size_t>(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<MinimapSegment> segments;
|
|
float units = 1.0f;
|
|
};
|
|
|
|
void addMinimapSegment(std::vector<MinimapSegment>& 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<MinimapSegment> buildMinimapSegments(const std::string& text, SyntaxLanguage language) {
|
|
std::vector<MinimapSegment> 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<unsigned char>(text[cursor]);
|
|
if (std::isspace(value)) {
|
|
size_t end = cursor + 1;
|
|
while (end < text.size() && std::isspace(static_cast<unsigned char>(text[end]))) ++end;
|
|
addMinimapSegment(segments, std::max(0.04f, static_cast<float>(end - cursor) * 0.18f), 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<float>(text.size() - cursor) * 0.42f), syntax_comment);
|
|
break;
|
|
}
|
|
if (language == SyntaxLanguage::python && text[cursor] == '#') {
|
|
addMinimapSegment(segments, std::max(0.18f, static_cast<float>(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<float>(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<float>(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<unsigned char>(text[end])) || text[end] == '.' || text[end] == '_')) ++end;
|
|
addMinimapSegment(segments, std::max(0.08f, static_cast<float>(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<unsigned char>(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<float>(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<float>(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<MinimapEntry>& 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);
|
|
}
|
|
|
|
void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewport_size, float scale,
|
|
float rendered_content_height, float visible_start, float visible_height, float max_scroll_y,
|
|
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;
|
|
|
|
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);
|
|
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);
|
|
const float target = std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
|
ImGui::SetScrollY(target);
|
|
}
|
|
draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
|
|
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
|
IM_COL32(109, 129, 170, hovered || active ? 44 : 28), scaled(2.0f, scale));
|
|
draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
|
|
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
|
IM_COL32(120, 138, 170, 120), scaled(2.0f, scale));
|
|
}
|
|
|
|
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<float>(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<int>(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<size_t>(wrap_columns)) {
|
|
chunk_end = cursor + static_cast<size_t>(wrap_columns);
|
|
size_t break_at = text.rfind(' ', chunk_end);
|
|
if (break_at == std::string::npos || break_at < cursor + static_cast<size_t>(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<float>(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());
|
|
}
|
|
|
|
std::string ellipsizeText(std::string_view text, float max_width) {
|
|
if (text.empty() || max_width <= 0.0f) return {};
|
|
|
|
std::string display{text};
|
|
if (ImGui::CalcTextSize(display.c_str()).x <= max_width) return display;
|
|
|
|
constexpr const char* overflow = "...";
|
|
const float overflow_width = ImGui::CalcTextSize(overflow).x;
|
|
if (overflow_width >= max_width) return overflow;
|
|
|
|
while (!display.empty() && ImGui::CalcTextSize(display.c_str()).x + overflow_width > max_width) {
|
|
display.pop_back();
|
|
}
|
|
display += overflow;
|
|
return display;
|
|
}
|
|
|
|
void drawEllipsizedText(ImDrawList* draw, ImVec2 position, ImU32 color, std::string_view text,
|
|
float max_width, const ImVec2& clip_min, const ImVec2& clip_max) {
|
|
const std::string display = ellipsizeText(text, max_width);
|
|
if (display.empty()) return;
|
|
draw->PushClipRect(clip_min, clip_max, true);
|
|
draw->AddText(position, color, display.c_str());
|
|
draw->PopClipRect();
|
|
}
|
|
}
|
|
|
|
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<std::string> 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::time_t>(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<std::string> 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<std::string> 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<std::string> 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<MinimapEntry> 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<int>(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<int>(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<int>(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<int>(hunk_index);
|
|
pending_action = HunkAction::stage;
|
|
}
|
|
ImGui::PopStyleColor();
|
|
} else if (!historical && ImGui::SmallButton("Unstage Hunk")) {
|
|
pending_hunk = static_cast<int>(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<int>(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(270.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<int>(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 = line.show_attribution ? scaled(28.0f, scale) : 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(16.0f, scale);
|
|
const ImVec2 avatar_min{line_minimum.x + scaled(8.0f, scale), line_minimum.y + scaled(2.0f, scale)};
|
|
const unsigned int avatar_texture = avatars ? avatars->textureFor(line.email) : 0;
|
|
if (avatar_texture) {
|
|
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(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));
|
|
}
|
|
const float info_x = avatar_min.x + avatar_size + scaled(8.0f, scale);
|
|
const float info_right = line_minimum.x + info_width - scaled(10.0f, scale);
|
|
const float info_text_width = std::max(0.0f, info_right - info_x);
|
|
const ImVec2 info_clip_min{info_x, line_minimum.y};
|
|
const ImVec2 info_clip_max{info_right, line_maximum.y};
|
|
const std::string author = line.author.empty() ? "Unknown author" : line.author;
|
|
std::string summary = line.summary.empty() ? "(no summary)" : line.summary;
|
|
if (!line.date.empty()) summary += " " + line.date;
|
|
drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(1.0f, scale)},
|
|
IM_COL32(216, 220, 226, 255), author, info_text_width, info_clip_min, info_clip_max);
|
|
drawEllipsizedText(draw, {info_x, line_minimum.y + scaled(10.0f, scale)},
|
|
IM_COL32(134, 140, 151, 255), summary, info_text_width, info_clip_min, info_clip_max);
|
|
} 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<std::string>* 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<int>(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<int>(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<int>(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);
|
|
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_content_height);
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor(5);
|
|
}
|
|
if (use_code_font) ImGui::PopFont();
|
|
ImGui::EndChild();
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar();
|
|
}
|