ultimatepp/uppsrc/TextDiffCtrl/TextCtrl.cpp
2026-01-15 10:22:22 +01:00

727 lines
17 KiB
C++

#include "TextDiffCtrl.h"
namespace Upp {
inline Color HistoryBg() { return Color(255, 255, 0); }
TextCompareCtrl::TextCompareCtrl()
: sbi(*this) {
letter = Size(1, 1);
blame_width = number_width = 0;
number_yshift = 0;
number_bg = WhiteGray();
SetFrame(FieldFrame());
AddFrame(scroll);
SetFont(CourierZ(12), CourierZ(12));
scroll.NoAutoHide();
scroll.WhenScroll = THISBACK(SelfScroll);
maxwidth = 0;
tabsize = 4;
cursor = anchor = Null;
show_line_number = true;
show_white_space = false;
show_diff_highlight = true;
change_paper_color = true;
}
int TextCompareCtrl::GetLineNo(int y, int& ii)
{
ii = scroll.Get().y + y / letter.cy;
return ii >= 0 && ii < lines.GetCount() ? lines[ii].number : Null;
}
bool TextCompareCtrl::SetCursor(int c)
{
int ii = GetLine(c);
if(ii >= 0) {
cursor = anchor = c;
Refresh();
scroll.SetY(max(ii - scroll.GetReducedViewSize().cy / letter.cy / 2, 0));
scroll.ScrollIntoY(ii);
WhenSel();
WhenCursor();
return true;
}
cursor = anchor = Null;
return false;
}
void TextCompareCtrl::DoSelection(int y, bool shift)
{
int ii;
int i = GetLineNo(y, ii);
if(!IsNull(i)) {
if(shift)
cursor = i;
else
cursor = anchor = i;
Refresh();
scroll.ScrollIntoY(ii);
WhenSel();
if(!shift)
WhenCursor();
}
}
int TextCompareCtrl::GetLine(int cursor) const
{
if(cursor >= 0)
for(int i = 0; i < lines.GetCount(); i++)
if(lines[i].number == cursor)
return i;
return -1;
}
int TextCompareCtrl::GetLine() const
{
return GetLine(cursor);
}
void TextCompareCtrl::SetLine(int ii)
{
if(ii >= 0)
DoSelection((ii - scroll.Get().y) * letter.cy, false);
}
const TextCompareCtrl::Blame *TextCompareCtrl::GetBlame(Point p)
{
if(blame.GetCount() && p.x > number_width && p.x < number_width + blame_width) {
int ii;
GetLineNo(p.y, ii);
if(ii >= 0 && ii < lines.GetCount()) {
const Line& l = lines[ii];
if(!IsNull(l.number) && l.number >= 0 && l.number <= blame.GetCount())
return &blame[l.number - 1];
}
}
return nullptr;
}
Image TextCompareCtrl::CursorImage(Point p, dword keyflags)
{
if(GetBlame(p))
return Image::Hand();
return Image::Arrow();
}
void TextCompareCtrl::LeftDown(Point pt, dword keyflags)
{
const Blame *b = GetBlame(pt);
if(b) {
WhenBlame(b->hash);
return;
}
DoSelection(pt.y, keyflags & K_SHIFT);
SetCapture();
SetWantFocus();
}
void TextCompareCtrl::LeftDouble(Point pt, dword keyflags)
{
int ii;
int i = GetLineNo(pt.y, ii);
if(!IsNull(i) && !GetBlame(pt))
WhenLeftDouble(i - 1, ii);
}
void TextCompareCtrl::MouseMove(Point pt, dword flags)
{
int bl = -1;
if(HasCapture()) {
DoSelection(pt.y, true);
}
else
if(GetBlame(pt))
bl = scroll.Get().y + pt.y / letter.cy;
if(bl != blame_line) {
blame_line = bl;
Refresh();
}
}
void Upp::TextCompareCtrl::MouseLeave()
{
if(blame_line >= 0) {
blame_line = -1;
Refresh();
}
}
void TextCompareCtrl::LeftRepeat(Point pt, dword keyflags)
{
if(HasCapture())
DoSelection(pt.y, true);
}
void TextCompareCtrl::LeftUp(Point pt, dword keyflags)
{
ReleaseCapture();
}
void Upp::TextCompareCtrl::LostFocus()
{
Refresh();
}
void TextCompareCtrl::Copy()
{
int sell, selh;
if(GetSelection(sell, selh)) {
String clip;
for(int i = 0; i < lines.GetCount(); i++) {
const Line& l = lines[i];
if(l.number >= sell && l.number <= selh) {
clip << l.text;
#ifdef PLATFORM_WIN32
clip << "\r\n";
#else
clip << "\n";
#endif
}
}
ClearClipboard();
AppendClipboardText(clip);
}
}
void TextCompareCtrl::RightDown(Point p, dword keyflags)
{
MenuBar b;
b.Add(!IsNull(cursor), t_("Copy"), CtrlImg::copy(), THISBACK(Copy)).Key(K_CTRL_C);
b.Execute();
}
bool TextCompareCtrl::Key(dword key, int repcnt)
{
Point pos = scroll, newpos = pos, page = scroll.GetPage();
switch(key) {
case K_CTRL_A:
if(lines.GetCount() > 1) {
cursor = lines[0].number;
anchor = lines.Top().number;
Refresh();
}
break;
case K_LEFT: newpos.x--; break;
case K_RIGHT: newpos.x++; break;
case K_CTRL_LEFT: newpos.x -= page.x >> 1; break;
case K_CTRL_RIGHT: newpos.x += page.x >> 1; break;
case K_UP: newpos.y--; break;
case K_DOWN: newpos.y++; break;
case K_PAGEUP: newpos.y -= page.y; break;
case K_PAGEDOWN: newpos.y += page.y; break;
case K_HOME: newpos.x = 0; break;
case K_END: newpos.x = maxwidth - page.x; break;
case K_CTRL_HOME: newpos.y = 0; break;
case K_CTRL_END: newpos.y = lines.GetCount() - page.y; break;
case K_CTRL_C:
Copy();
break;
case K_F3: {
bool found = false;
int i = max(pos.y + 2, 0);
while(i < lines.GetCount() && lines[i].level)
i++;
while(i < lines.GetCount())
if(lines[i++].level) {
newpos.y = i - 2;
found = true;
break;
}
if(!found) {
BeepExclamation();
return true;
}
break;
}
case K_SHIFT_F3: {
bool found = false;
int i = min(pos.y, lines.GetCount() - 1);
while(i > 0 && lines[i].level)
i--;
while(i >= 0)
if(lines[i--].level) {
newpos.y = i;
found = true;
break;
}
if(!found) {
BeepExclamation();
return true;
}
break;
}
default: return false;
}
if(newpos != pos)
scroll.Set(newpos);
return true;
}
int TextCompareCtrl::GetMatchLen(const wchar *s1, const wchar *s2, int len)
{
for(int q = 0; q < len; q++)
if(s1[q] != s2[q])
return q;
return len;
}
bool TextCompareCtrl::LineDiff(bool left, Vector<LineEdit::Highlight>& hln, Color eq_color,
const wchar *s1, int l1, int h1,
const wchar *s2, int l2, int h2, int depth)
{
int p1 = 0;
int p2 = 0;
int matchlen = 0;
int subscore = 0; // prefer matches that are not in words
for(int pos1 = l1; pos1 < h1; pos1++)
for(int pos2 = l2; pos2 < h2; pos2++) {
int ml = GetMatchLen(s1 + pos1, s2 + pos2, min(h1 - pos1, h2 - pos2));
if(ml && ml >= matchlen) {
int l = pos1;
int h = pos1 + matchlen;
int sc = (l == 0 || IsAlNum(s1[l - 1]) != IsAlNum(s1[l])) +
(h == 0 || h >= l1 || IsAlNum(s1[h - 1]) != IsAlNum(s1[h]));
if(ml > matchlen || sc > subscore) {
p1 = pos1;
p2 = pos2;
matchlen = ml;
subscore = sc;
}
}
}
if(matchlen > 1 || matchlen && !IsAlNum(s1[p1])) {
for(int i = 0; i < matchlen; i++)
hln[(left ? p1 : p2) + i].paper = eq_color;
if(++depth < 20) {
LineDiff(left, hln, eq_color, s1, l1, p1, s2, l2, p2, depth);
LineDiff(left, hln, eq_color, s1, p1 + matchlen, h1, s2, p2 + matchlen, h2, depth);
}
return true;
}
return false;
}
TextCompareCtrl::ScrollBarItems::ScrollBarItems(TextCompareCtrl& e)
: diff(e) {
e.scroll.y.Add(SizePos());
Transparent();
IgnoreMouse();
}
void TextCompareCtrl::ScrollBarItems::Paint(Draw& w)
{
diff.PaintScrollBarItems(w);
}
void TextCompareCtrl::PaintScrollBarItems(Draw& w)
{
Rect sr = scroll.y.GetSliderRect();
for(int pass = 0; pass < 2; pass++) {
Size isz = pass ? DiffImg::dot1().GetSize() : DiffImg::dot().GetSize();
for(int i = 0; i < lines.GetCount(); i++)
if(lines[i].level > 1)
w.DrawImage(sr.CenterPoint().x - isz.cx / 2,
sr.top + scroll.y.GetSliderPos(i) - isz.cy / 2,
pass ? DiffImg::dot1() : DiffImg::dot());
}
}
void TextCompareCtrl::Paint(Draw& draw)
{
Point sc = scroll.Get();
Size offset = (Size)sc * letter;
Size sz = GetSize();
int lcnt = lines.GetCount();
int first_line = offset.cy / letter.cy;
int last_line = min(idivceil(sz.cy + offset.cy, letter.cy), lines.GetCount() - 1);
WString test = "č"; // read text/paper colors from highlighting scheme using likely non-highlighted text
Vector<LineEdit::Highlight> th;
th.SetCount(2);
th[0].ink = SColorText();
th[0].paper = SColorPaper();
WhenHighlight(th, test);
Color text_color = th[0].ink;
Color paper_color = th[0].paper;
Color diffpaper = IsDark(paper_color) ? Magenta() : Yellow();
Color missingpaper = IsDark(paper_color) ? Gray() : LtGray();
int n_width = 0;
if(show_line_number) {
n_width = number_width;
for(int i = first_line; i <= last_line; i++) {
const Line& l = lines[i];
int y = i * letter.cy - offset.cy;
Color paper = IsNull(l.number) ? missingpaper : l.diff ? diffpaper : SColorPaper();
Color ink = l.diff ? SRed(): SGray();
draw.DrawRect(0, y, n_width, letter.cy, paper);
if(!IsNull(l.number))
draw.DrawText(0, y + number_yshift, FormatInt(l.number_diff), number_font, ink);
draw.DrawRect(n_width - 1, y, 1, letter.cy, Gray());
}
}
int blame_y = -1;
Color blame_paper;
String blame_qtf;
int blame_y0 = -1;
if(blame.GetCount()) {
int ty = (letter.cy - blame_font.GetCy()) / 2;
Date now = GetSysDate();
String last_hash;
int bli = 0;
for(int i = first_line; i <= last_line; i++) {
const Line& l = lines[i];
int y = i * letter.cy - offset.cy;
Color paper = missingpaper;
Time tm;
String date, author, hash, summary;
if(!IsNull(l.number) && l.number >= 0 && l.number <= blame.GetCount()) {
Blame& m = blame[l.number - 1];
tm = m.time;
date = Format("%04d-%02d-%02d ", m.time.year, m.time.month, m.time.day);
author = m.author;
hash = m.hash;
summary = m.summary;
auto h = GetHashValue(hash);
int r = h & 15; h >>= 8;
int g = h & 7; h >>= 8;
int b = h & 31;
paper = AColor(255 - r, 255 - g, 255 - b);
}
draw.DrawRect(n_width, y, blame_width, letter.cy, paper);
draw.Clip(n_width, y, blame_width - 2, letter.cy);
if(hash != last_hash)
bli = 0;
if(bli == 0) {
if(i != first_line)
draw.DrawRect(n_width, y, blame_width, 1, SLtGray());
int age = now - (Date)tm;
draw.DrawText(n_width, y + ty, date, blame_font, age < 14 ? SLtBlue() : age < 365 ? SBlue() : SGray());
draw.DrawText(n_width + GetTextSize(date, blame_font).cx, y + ty, author, blame_font, SRed());
last_hash = hash;
blame_y0 = y;
}
if(bli == 1)
draw.DrawText(n_width, y + ty, summary, blame_font);
if(bli == 2 && hash.GetCount() > 6)
draw.DrawText(n_width, y + ty, hash.Mid(0, 6), blame_font, SGreen());
if(i == blame_line) {
blame_qtf = "[g@b " + Format("%04d-%02d-%02d %02d:%02d:%02d ",
tm.year, tm.month, tm.day,
tm.hour, tm.minute, tm.second) +
"[@r \1" + author + "\1&" +
"[@k* \1" + summary + "\1]&"
"[@g " + hash;
blame_y = blame_y0;
blame_paper = paper;
}
draw.End();
draw.DrawRect(n_width + blame_width - 1, y, 1, letter.cy, Gray());
bli++;
}
n_width += blame_width;
}
draw.Clip(n_width, 0, sz.cx - n_width, sz.cy);
for(int i = first_line; i <= last_line; i++) {
const Line& l = lines[i];
int y = i * letter.cy - offset.cy;
Color ink = text_color;
Color paper = IsNull(l.number) ? missingpaper : l.diff ? diffpaper : paper_color;
bool sel = IsSelected(i);
if(sel) {
ink = SColorHighlightText();
paper = HasFocus() ? SColorHighlight() : Blend(SColorHighlight(), SColorDisabled());
}
WString ln = l.text.ToWString();
if(ln.GetCount() > 20000)
ln.Trim(20000);
ln = ExpandTabs(ln);
Vector<LineEdit::Highlight> hln;
hln.SetCount(ln.GetCount() + 1);
for(int i = 0; i < hln.GetCount(); i++) {
LineEdit::Highlight& h = hln[i];
h.paper = sel ? paper : diffpaper;
h.ink = ink;
h.chr = ln[i];
h.font = StdFont();
}
bool ldiff = false;
if(!sel) {
WhenHighlight(hln, ln);
for(int i = 0; i < hln.GetCount(); ++i) {
LineEdit::Highlight& h = hln[i];
if (change_paper_color)
h.paper = paper;
}
if(show_diff_highlight) {
WString ln_diff = l.text_diff.ToWString();
ln_diff = ExpandTabs(ln_diff);
if((int64)ln_diff.GetCount() * ln.GetCount() < 50000) {
if(left)
ldiff = LineDiff(true, hln, paper_color,
~ln, 0, ln.GetCount(), ~ln_diff, 0, ln_diff.GetCount(), 0);
else
ldiff = LineDiff(false, hln, paper_color,
~ln_diff, 0, ln_diff.GetCount(), ~ln, 0, ln.GetCount(), 0);
}
}
if(show_white_space) {
for(int i = ln.GetCount(); i >= 0; --i) {
LineEdit::Highlight& h = hln[i];
if (ln[i] == 0)
continue;
if(ln[i] == 32)
h.paper = IsDark(paper_color) ? LtRed() : Red();
else
break;
}
}
if(ldiff)
paper = paper_color;
}
draw.DrawRect(0, y, sz.cx, letter.cy, paper); // paint the end of line
int x = 0;
int ii = 0;
while(ii < hln.GetCount() - 1) {
LineEdit::Highlight& h = hln[ii];
int l = 1;
while(ii + l < hln.GetCount() - 1 && hln[ii + l].paper == h.paper)
l++;
draw.DrawRect(n_width - offset.cx + x, y, l * letter.cx, letter.cy, h.paper);
x += l * letter.cx;
ii += l;
}
x = 0;
ii = 0;
Vector<int> dx;
while(ii < hln.GetCount() - 1) {
Font fnt = font;
LineEdit::Highlight& h = hln[ii];
WString text;
text.Cat(h.chr);
for(;;) {
int i = ii + text.GetCount();
if(i < hln.GetCount() - 1 && hln[i].ink == h.ink && hln[i].font == h.font)
text.Cat(hln[i].chr);
else
break;
}
fnt.Bold(h.font.IsBold());
fnt.Italic(h.font.IsItalic());
fnt.Underline(h.font.IsUnderline());
dx.SetCount(text.GetCount());
dx.Set(0, letter.cx, text.GetCount());
draw.DrawText(n_width - offset.cx + x, y, text, fnt, h.ink, dx);
x += text.GetCount() * letter.cx;
ii += text.GetCount();
}
}
int lcy = lcnt * letter.cy - offset.cy;
draw.DrawRect(0, lcy, sz.cx, sz.cy - lcy, SGray());
draw.End();
draw.DrawRect(0, lcy, n_width, sz.cy - lcy, number_bg);
if(blame_y >= 0 && HasMouse()) {
RichText txt = ParseQTF(blame_qtf);
txt.ApplyZoom(GetRichTextStdScreenZoom());
int cx = clamp(txt.GetWidth(), 10, sz.cx - n_width);
int cy = txt.GetHeight(cx);
if(blame_y + cy > sz.cy)
blame_y = max(sz.cy - cy, 0);
draw.DrawRect(n_width, blame_y, cx + 2 + DPI(8), cy + 2, blame_paper);
DrawFrame(draw, n_width - 1, blame_y, cx + 2 + DPI(8), cy + 2, SLtGray());
txt.Paint(draw, n_width + DPI(4), blame_y + 1, cx);
}
}
void TextCompareCtrl::TabSize(int t)
{
tabsize = t;
UpdateWidth();
Layout();
}
void TextCompareCtrl::SetFont(Font f, Font nf)
{
font = f;
number_font = nf;
letter.cy = f.GetCy();
letter.cx = f.GetMonoWidth();
number_width = 5 * nf.GetMonoWidth();
for(int i = 0; i < font.GetHeight(); i++) {
blame_font = StdFont(font.GetHeight() - i);
if(blame_font.GetCy() <= font.GetCy())
break;
}
blame_width = GetTextSize("2025-10-10 Dear User Name", blame_font).cx;
number_yshift = (f.GetCy() - nf.GetCy() + 2) >> 1;
Layout();
}
void TextCompareCtrl::SetFont(Font f)
{
SetFont(f, GetNumberFont());
}
void TextCompareCtrl::Layout()
{
int n_width = show_line_number ? number_width : 0;
if(blame.GetCount())
n_width += Zx(8 * 40);
scroll.Set(scroll, (scroll.GetReducedViewSize() - Size(n_width, 0)) / letter, Size(maxwidth, lines.GetCount()));
Refresh();
}
void TextCompareCtrl::MouseWheel(Point pt, int zdelta, dword keyflags)
{
if(keyflags & K_SHIFT)
scroll.WheelX(zdelta);
else
scroll.WheelY(zdelta);
}
void TextCompareCtrl::HorzMouseWheel(Point pt, int zdelta, dword keyflags)
{
scroll.WheelX(zdelta);
}
void TextCompareCtrl::SetCount(int c)
{
bool rl = (c < lines.GetCount());
lines.SetCount(c);
if(rl)
UpdateWidth();
Layout();
}
void TextCompareCtrl::AddCount(int c)
{
lines.InsertN(lines.GetCount(), c);
Layout();
}
void TextCompareCtrl::Set(int line, String text, bool diff, int number, int level, String text_diff, int number_diff)
{
Line& l = lines.At(line);
int tl = MeasureLength(text.ToWString());
int lt = MeasureLength(l.text.ToWString());
bool rl = (tl < lt && lt == maxwidth);
l.number = number;
l.text = text;
l.diff = diff;
l.level = level;
l.text_diff = text_diff;
l.number_diff = number_diff;
if(rl)
UpdateWidth();
else if(tl > maxwidth) {
maxwidth = tl;
Layout();
}
}
void TextCompareCtrl::SelfScroll()
{
Refresh();
WhenScroll();
}
void TextCompareCtrl::PairScroll(TextCompareCtrl *ctrl)
{
scroll.Set(ctrl->scroll.Get());
}
void TextCompareCtrl::UpdateWidth()
{
maxwidth = 0;
for(int i = 0; i < lines.GetCount(); i++)
maxwidth = max(maxwidth, MeasureLength(lines[i].text.ToWString()));
}
int TextCompareCtrl::MeasureLength(const wchar *text) const
{
int pos = 0;
while(*text)
if(*text++ == '\t')
pos += tabsize - pos % tabsize;
else
pos++;
return pos;
}
bool TextCompareCtrl::IsSelected(int i) const
{
int sell, selh;
if(HasLine(i) && GetSelection(sell, selh)) {
i = GetNumber(i);
return i >= sell && i <= selh;
}
return false;
}
bool TextCompareCtrl::GetSelection(int& l, int& h) const
{
if(IsNull(cursor)) {
l = h = -1;
return false;
}
l = min(cursor, anchor);
h = max(cursor, anchor);
return true;
}
WString TextCompareCtrl::ExpandTabs(const wchar *text) const
{
WString out;
for(wchar c; (c = *text++);)
if(c == '\t')
out.Cat(' ', tabsize - out.GetLength() % tabsize);
else
out.Cat(c);
return out;
}
Point TextCompareCtrl::GetPos() const
{
Point pos(0, 0);
int ltop = minmax(scroll.Get().y, 0, lines.GetCount() - 1);
while(ltop >= 0 && IsNull(lines[ltop].number)) {
ltop--;
pos.y++;
}
if(ltop >= 0)
pos.x = lines[ltop].number;
return pos;
}
void TextCompareCtrl::SetPos(Point pos)
{
int l = FindFieldIndex(lines, &Line::number, pos.x);
if(l < 0)
l = 0;
SetSb(l + pos.y);
}
};