16 KB、554行 生 Blame 履歴
1// TextDiff - A syntax highlighting text diff widget for Dear ImGui.
2// Copyright (c) 2024-2026 Johan A. Goossens. All rights reserved.
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
7
8//
9// Include files
10//
11
12#include <cmath>
13
14#include "imgui_internal.h"
15
16#include "dtl.h"
17#include "TextDiff.h"
18
19
20//
21// TextDiff::TextDiff
22//
23
24TextDiff::TextDiff() {
25 readOnly = true;
26 showLineNumbers = false;
27 showMatchingBrackets = false;
28}
29
30
31//
32// TextDiff::SetText
33//
34
35void TextDiff::SetText(const std::string_view& left, const std::string_view& right) {
36 // split text into lines
37 std::vector<std::string_view> leftLines;
38 std::vector<std::string_view> rightLines;
39 splitLines(leftLines, left);
40 splitLines(rightLines, right);
41
42 // create two documents and colorize them
43 leftDocument.setText(leftLines);
44 rightDocument.setText(rightLines);
45
46 colorizer.updateEntireDocument(leftDocument, language);
47 colorizer.updateEntireDocument(rightDocument, language);
48
49 // setup line number decoration
50 leftLineNumberDigits = static_cast<int>(std::log10(leftDocument.lineCount() + 1) + 1.0f);
51 rightLineNumberDigits = static_cast<int>(std::log10(leftDocument.lineCount() + 1) + 1.0f);
52 decoratorWidth = -(leftLineNumberDigits + rightLineNumberDigits + 4.0f);
53
54 decoratorCallback = [this](TextEditor::Decorator& decorator) {
55 this->decorateLine(decorator);
56 };
57
58 // calculate the difference
59 dtl::Diff<std::string_view> diff(leftLines, rightLines);
60 diff.compose();
61 auto ranges = diff.getSes().getSequence();
62
63 // determine status of each line in diff view
64 lineInfo.clear();
65 size_t leftIndex = 0;
66 size_t rightIndex = 0;
67
68 for (auto& [text, info] : ranges) {
69 switch (info.type) {
70 case dtl::SES_COMMON:
71 lineInfo.emplace_back(leftIndex++, rightIndex++, LineStatus::common);
72 break;
73
74 case dtl::SES_ADD:
75 lineInfo.emplace_back(leftIndex, rightIndex++, LineStatus::added);
76 break;
77
78 case dtl::SES_DELETE:
79 lineInfo.emplace_back(leftIndex++, rightIndex, LineStatus::deleted);
80 break;
81 }
82 }
83
84 // set flag
85 updated = true;
86}
87
88
89//
90// TextDiff::SetLanguage
91//
92
93void TextDiff::SetLanguage(const Language* l) {
94 language = l;
95 colorizer.updateEntireDocument(leftDocument, language);
96 colorizer.updateEntireDocument(rightDocument, language);
97 updated = true;
98}
99
100
101//
102// TextDiff::Render
103//
104
105void TextDiff::Render(const char* title, const ImVec2& size, bool border) {
106 if (sideBySideMode) {
107 // render a custom side-by-side view
108 renderSideBySide(title, size, border);
109
110 } else {
111 // create a combined view (if required)
112 if (updated) {
113 createCombinedView();
114 updated = false;
115 }
116
117 // render combined view as a normal read-only text editor
118 TextEditor::render(title, size, border);
119 }
120}
121
122
123//
124// TextDiff::splitLines
125//
126
127void TextDiff::splitLines(std::vector<std::string_view>& result, const std::string_view& text) {
128 size_t prev = CodePoint::skipBOM(text.begin(), text.end()) - text.begin();
129 size_t pos;
130
131 while ((pos = text.find('\n', prev)) != std::string_view::npos) {
132 result.push_back(text.substr(prev, pos - prev));
133 prev = pos + 1;
134 }
135
136 result.push_back(text.substr(prev));
137}
138
139
140//
141// TextDiff::createCombinedView
142//
143
144void TextDiff::createCombinedView() {
145 document.clear();
146 cursors.clearAll();
147 clearMarkers();
148
149 for (size_t i = 0; i < lineInfo.size(); i++) {
150 auto& line = lineInfo[i];
151
152 switch(line.status) {
153 case LineStatus::common:
154 document.emplace_back(leftDocument[line.leftLine]);
155 break;
156
157 case LineStatus::added:
158 document.emplace_back(rightDocument[line.rightLine]);
159 addMarker(static_cast<int>(i), 0, addedColor, "", "");
160 break;
161
162 case LineStatus::deleted:
163 document.emplace_back(leftDocument[line.leftLine]);
164 addMarker(static_cast<int>(i), 0, deletedColor, "", "");
165 break;
166 }
167 }
168
169 document.updateMaximumColumn(0, document.lineCount() - 1);
170}
171
172
173//
174// TextDiff::decorateLine
175//
176
177void TextDiff::decorateLine(TextEditor::Decorator& decorator) {
178 auto& line = lineInfo[decorator.line];
179 auto lineLeft = static_cast<int>(line.leftLine + 1);
180 auto lineRight = static_cast<int>(line.rightLine + 1);
181 auto color = ImGui::ColorConvertU32ToFloat4(palette.get(Color::lineNumber));
182
183 switch(line.status) {
184 case LineStatus::common:
185 ImGui::TextColored(color, " %*d %*d ", leftLineNumberDigits, lineLeft, rightLineNumberDigits, lineRight);
186 break;
187
188 case LineStatus::added:
189 ImGui::TextColored(color, " %*s %*d +", leftLineNumberDigits, "", rightLineNumberDigits, lineRight);
190 break;
191
192 case LineStatus::deleted:
193 ImGui::TextColored(color, " %*d %*s -", leftLineNumberDigits, lineLeft, rightLineNumberDigits, "");
194 break;
195 }
196}
197
198
199//
200// TextDiff::renderSideBySide
201//
202
203void TextDiff::renderSideBySide(const char* title, const ImVec2& size, bool border) {
204 // update color palette (if required)
205 auto& style = ImGui::GetStyle();
206
207 if (paletteAlpha != style.Alpha) {
208 updatePalette();
209 }
210
211 // get font information
212 font = ImGui::GetFont();
213 fontSize = ImGui::GetFontSize();
214 glyphSize = ImVec2(ImGui::CalcTextSize("#").x, ImGui::GetTextLineHeightWithSpacing() * lineSpacing);
215
216 // scroll to specified line (if required)
217 if (scrollToLineNumber >= 0) {
218 scrollToLineNumber = std::min(scrollToLineNumber, static_cast<int>(lineInfo.size()));
219
220 switch (scrollToAlignment) {
221 case Scroll::alignTop:
222 ImGui::SetNextWindowScroll(ImVec2(0.0f, std::max(0.0f, static_cast<float>(scrollToLineNumber) * glyphSize.y)));
223 break;
224
225 case Scroll::alignMiddle:
226 ImGui::SetNextWindowScroll(ImVec2(0.0f, std::max(0.0f, static_cast<float>(scrollToLineNumber - visibleLines / 2) * glyphSize.y)));
227 break;
228
229 case Scroll::alignBottom:
230 ImGui::SetNextWindowScroll(ImVec2(0.0f, std::max(0.0f, static_cast<float>(scrollToLineNumber - (visibleLines - 1)) * glyphSize.y)));
231 break;
232 }
233
234 scrollToLineNumber = -1;
235 }
236
237 // ensure diff has focus (if required)
238 if (focusOnEditor) {
239 ImGui::SetNextWindowFocus();
240 focusOnEditor = false;
241 }
242
243 // start rendering the widget
244 ImGui::SetNextWindowContentSize(ImVec2(0.0f, glyphSize.y * lineInfo.size()));
245 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
246 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(palette.get(Color::background)));
247 ImGui::BeginChild(title, size, border, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoNavInputs);
248
249 auto cursorScreenPos = ImGui::GetCursorScreenPos();
250 auto visibleSize = ImGui::GetCurrentWindow()->Rect().GetSize();
251
252 // determine view parameters
253 leftLineNumberWidth = glyphSize.x * (leftLineNumberDigits + 4);
254 rightLineNumberWidth = glyphSize.x * (rightLineNumberDigits + 4);
255 textColumnWidth = static_cast<float>(visibleSize.x - leftLineNumberWidth - rightLineNumberWidth) / 2.0f;
256
257 leftLineNumberPos = cursorScreenPos.x;
258 leftTextPos = leftLineNumberPos + leftLineNumberWidth;
259 rightLineNumberPos = leftTextPos + textColumnWidth;
260 rightTextPos = rightLineNumberPos + rightLineNumberWidth;
261 rightTextEnd = rightTextPos + textColumnWidth;
262
263 visibleLines = std::max(static_cast<int>(std::ceil(visibleSize.y / glyphSize.y)), 0);
264 visibleColumns = std::max(static_cast<int>(std::ceil(textColumnWidth / glyphSize.x)), 0);
265
266 firstVisibleColumn = std::max(static_cast<int>(std::floor(textScroll / glyphSize.x)), 0);
267 lastVisibleColumn = static_cast<int>(std::floor((textScroll + textColumnWidth) / glyphSize.x));
268 firstVisibleLine = std::max(static_cast<int>(std::floor(ImGui::GetScrollY() / glyphSize.y)), 0);
269 lastVisibleLine = std::min(static_cast<int>(std::floor((ImGui::GetScrollY() + visibleSize.y) / glyphSize.y)), static_cast<int>(lineInfo.size() - 1));
270
271 renderSideBySideBackground();
272 renderSideBySideText();
273 renderSideBySideTextScrollbars();
274 renderSideBySideMiniMap();
275
276 ImGui::EndChild();
277 ImGui::PopStyleColor();
278 ImGui::PopStyleVar();
279}
280
281
282//
283// TextDiff::renderSideBySideBackground
284//
285
286void TextDiff::renderSideBySideBackground() {
287 // render line numbers and text backgrounds
288 auto drawList = ImGui::GetWindowDrawList();
289 auto y = ImGui::GetCursorScreenPos().y + firstVisibleLine * glyphSize.y;
290 char buffer[32];
291
292 for (auto i = firstVisibleLine; i <= lastVisibleLine; i++) {
293 auto& line = lineInfo[i];
294 auto lineLeft = static_cast<int>(line.leftLine + 1);
295 auto lineRight = static_cast<int>(line.rightLine + 1);
296
297 switch(line.status) {
298 case LineStatus::common:
299 snprintf(buffer, sizeof(buffer), " %*d", leftLineNumberDigits, lineLeft);
300 drawList->AddText(ImVec2(leftLineNumberPos, y), palette.get(Color::lineNumber), buffer);
301 snprintf(buffer, sizeof(buffer), " %*d", rightLineNumberDigits, lineRight);
302 drawList->AddText(ImVec2(rightLineNumberPos, y), palette.get(Color::lineNumber), buffer);
303 break;
304
305 case LineStatus::added:
306 snprintf(buffer, sizeof(buffer), " %*d +", rightLineNumberDigits, lineRight);
307 drawList->AddText(ImVec2(rightLineNumberPos, y), palette.get(Color::lineNumber), buffer);
308
309 drawList->AddRectFilled(
310 ImVec2(rightTextPos, y),
311 ImVec2(rightTextEnd, y + glyphSize.y),
312 addedColor);
313
314 break;
315
316 case LineStatus::deleted:
317 snprintf(buffer, sizeof(buffer), " %*d -", leftLineNumberDigits, lineLeft);
318 drawList->AddText(ImVec2(leftLineNumberPos, y), palette.get(Color::lineNumber), buffer);
319
320 drawList->AddRectFilled(
321 ImVec2(leftTextPos, y),
322 ImVec2(rightLineNumberPos, y + glyphSize.y),
323 deletedColor);
324
325 break;
326 }
327
328 y += glyphSize.y;
329 }
330}
331
332
333//
334// TextDiff::renderSideBySideText
335//
336
337void TextDiff::renderSideBySideText() {
338 // setup rendering
339 auto drawList = ImGui::GetWindowDrawList();
340 auto cursorScreenPos = ImGui::GetCursorScreenPos();
341 auto yTop = drawList->GetClipRectMin().y;
342 auto yBottom = drawList->GetClipRectMax().y;
343
344 // render left text
345 drawList->PushClipRect(ImVec2(leftTextPos, yTop), ImVec2(rightLineNumberPos, yBottom), false);
346
347 for (auto i = firstVisibleLine; i <= lastVisibleLine; i++) {
348 auto& line = lineInfo[i];
349 auto y = cursorScreenPos.y + i * glyphSize.y;
350
351 switch(line.status) {
352 case LineStatus::common:
353 case LineStatus::deleted:
354 renderSideBySideLine(leftTextPos, y, leftDocument[lineInfo[i].leftLine]);
355 break;
356
357 case LineStatus::added:
358 break;
359 }
360 }
361
362 drawList->PopClipRect();
363
364 // render right text
365 drawList->PushClipRect(ImVec2(rightTextPos, yTop), ImVec2(rightTextEnd, yBottom), false);
366
367 for (auto i = firstVisibleLine; i <= lastVisibleLine; i++) {
368 auto& line = lineInfo[i];
369 auto y = cursorScreenPos.y + i * glyphSize.y;
370
371 switch(line.status) {
372 case LineStatus::common:
373 case LineStatus::added:
374 renderSideBySideLine(rightTextPos, y, rightDocument[lineInfo[i].rightLine]);
375 break;
376
377 case LineStatus::deleted:
378 break;
379 }
380 }
381
382 drawList->PopClipRect();
383}
384
385
386//
387// TextDiff::renderSideBySideLine
388//
389
390void TextDiff::renderSideBySideLine(float x, float y, TextEditor::Line& line) {
391 // draw colored glyphs for specified line
392 auto drawList = ImGui::GetWindowDrawList();
393 auto tabSize = document.getTabSize();
394 auto firstRenderableColumn = (firstVisibleColumn / tabSize) * tabSize;
395
396 auto column = firstRenderableColumn;
397 auto index = document.getIndex(line, column);
398 auto lineSize = line.size();
399
400 while (index < lineSize && column <= lastVisibleColumn) {
401 auto& glyph = line[index];
402 auto codepoint = glyph.codepoint;
403 ImVec2 glyphPos(x + column * glyphSize.x - textScroll, y);
404
405 if (codepoint == '\t') {
406 if (showTabs) {
407 const auto x1 = glyphPos.x + glyphSize.x * 0.3f;
408 const auto y1 = glyphPos.y + fontSize * 0.5f;
409 const auto x2 = glyphPos.x + glyphSize.x;
410
411 ImVec2 p1, p2, p3, p4;
412 p1 = ImVec2(x1, y1);
413 p2 = ImVec2(x2, y1);
414 p3 = ImVec2(x2 - fontSize * 0.16f, y1 - fontSize * 0.16f);
415 p4 = ImVec2(x2 - fontSize * 0.16f, y1 + fontSize * 0.16f);
416
417 drawList->AddLine(p1, p2, palette.get(Color::whitespace));
418 drawList->AddLine(p2, p3, palette.get(Color::whitespace));
419 drawList->AddLine(p2, p4, palette.get(Color::whitespace));
420 }
421
422 } else if (codepoint == ' ') {
423 if (showSpaces) {
424 const auto x1 = glyphPos.x + glyphSize.x * 0.5f;
425 const auto y1 = glyphPos.y + fontSize * 0.5f;
426 drawList->AddCircleFilled(ImVec2(x1, y1), 1.5f, palette.get(Color::whitespace), 4);
427 }
428
429 } else {
430 font->RenderChar(drawList, fontSize, glyphPos, palette.get(glyph.color), codepoint);
431 }
432
433 index++;
434 column += (codepoint == '\t') ? tabSize - (column % tabSize) : 1;
435 }
436}
437
438
439//
440// TextDiff::renderSideBySideTextScrollbars
441//
442
443void TextDiff::renderSideBySideTextScrollbars() {
444 auto maxColumnsWidth = std::max(leftDocument.getMaxColumn(), rightDocument.getMaxColumn()) * glyphSize.x;
445 auto visibleColumnsWidth = rightLineNumberPos - leftTextPos;
446
447 if (maxColumnsWidth > visibleColumnsWidth) {
448 auto window = ImGui::GetCurrentWindow();
449 const ImRect outerRect = window->Rect();
450 auto borderSize = std::round(window->WindowBorderSize * 0.5f);
451 auto scrollbarSize = ImGui::GetStyle().ScrollbarSize;
452
453 auto textScrollbarTop = std::max(outerRect.Min.y + borderSize, outerRect.Max.y - borderSize - scrollbarSize);
454 ImRect leftScrollbarFrame(leftTextPos, textScrollbarTop, rightLineNumberPos, textScrollbarTop + scrollbarSize);
455 ImRect rightScrollbarFrame(rightTextPos, textScrollbarTop, rightTextEnd, textScrollbarTop + scrollbarSize);
456 ImS64 scroll = static_cast<ImS64>(textScroll);
457
458 if (ImGui::ScrollbarEx(
459 leftScrollbarFrame,
460 ImGui::GetID("leftTextScroll"),
461 ImGuiAxis_X,
462 &scroll,
463 static_cast<ImS64>(visibleColumnsWidth),
464 static_cast<ImS64>(maxColumnsWidth),
465 ImDrawFlags_RoundCornersAll)) { textScroll = static_cast<float>(scroll); }
466
467 if (ImGui::ScrollbarEx(
468 rightScrollbarFrame,
469 ImGui::GetID("rightTextScroll"),
470 ImGuiAxis_X,
471 &scroll,
472 static_cast<ImS64>(visibleColumnsWidth),
473 static_cast<ImS64>(maxColumnsWidth),
474 ImDrawFlags_RoundCornersAll)) { textScroll = static_cast<float>(scroll); }
475
476 if (ImGui::IsWindowHovered()) {
477 textScroll = std::clamp(textScroll - ImGui::GetIO().MouseWheelH * ImGui::GetFontSize(), 0.0f, maxColumnsWidth - visibleColumnsWidth);
478
479 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
480 textScroll = std::max(textScroll - glyphSize.x, 0.0f);
481
482 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
483 textScroll = std::min(textScroll + glyphSize.x, maxColumnsWidth - visibleColumnsWidth);
484
485 } else if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
486 textScroll = 0.0f;
487
488 } else if (ImGui::IsKeyPressed(ImGuiKey_End)) {
489 textScroll = maxColumnsWidth - visibleColumnsWidth;
490 }
491 }
492 }
493
494 if (ImGui::IsWindowHovered()) {
495 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
496 if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) {
497 ImGui::SetScrollY(0.0f);
498
499 } else {
500 ImGui::SetScrollY(std::max(ImGui::GetScrollY() - glyphSize.y, 0.0f));
501 }
502
503 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
504 if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) {
505 ImGui::SetScrollY(ImGui::GetScrollMaxY());
506
507 } else {
508 ImGui::SetScrollY(std::min(ImGui::GetScrollY() + glyphSize.y, ImGui::GetScrollMaxY()));
509 }
510
511 } else if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
512 ImGui::SetScrollY(std::max(ImGui::GetScrollY() - (visibleLines - 2) * glyphSize.y, 0.0f));
513
514 } else if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
515 ImGui::SetScrollY(std::min(ImGui::GetScrollY() + (visibleLines - 2) * glyphSize.y, ImGui::GetScrollMaxY()));
516 }
517 }
518}
519
520
521//
522// TextDiff::renderSideBySideMiniMap
523//
524
525void TextDiff::renderSideBySideMiniMap() {
526 // based on https://github.com/ocornut/imgui/issues/3114
527 if (showScrollbarMiniMap) {
528 auto window = ImGui::GetCurrentWindow();
529
530 if (window->ScrollbarY) {
531 auto drawList = ImGui::GetWindowDrawList();
532 auto rect = ImGui::GetWindowScrollbarRect(window, ImGuiAxis_Y);
533 auto lineHeight = rect.GetHeight() / static_cast<float>(document.size());
534 auto offset = (rect.Max.x - rect.Min.x) * 0.3f;
535 auto left = rect.Min.x + offset;
536 auto right = rect.Max.x - offset;
537
538 drawList->PushClipRect(rect.Min, rect.Max, false);
539
540 // render diff locations
541 for (size_t i = 0; i < lineInfo.size(); i++) {
542 auto& line = lineInfo[i];
543
544 if (line.status != LineStatus::common) {
545 auto color = (line.status == LineStatus::added) ? addedColor : deletedColor;
546 auto ly = std::round(rect.Min.y + i * lineHeight);
547 drawList->AddRectFilled(ImVec2(left, ly), ImVec2(right, ly + lineHeight), color);
548 }
549 }
550
551 drawList->PopClipRect();
552 }
553 }
554}