From 0e923adea9bc470e958b3a4da5e29a98aeb668c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 23 Mar 2026 18:12:23 -0300 Subject: [PATCH] UI: Add UIDiffView for specialized diff/patch visualization - Integrated dtl into thirdparty for native string diffing - Implemented UIDiffView, a composite wrapper for UICodeEditor with a custom plugin (UIDiffEditorPlugin) - Handles dynamic string diffing and standard .patch file parsing - Automatically extracts target filename from patch headers for proper syntax highlighting - Corrects line numbers in gutter to match the diff origins while hiding patch headers - Wired into App::loadDiffFromPath and GitPlugin - Added exhaustive unit tests in uidiffview_test.cpp --- .agent/SOUL.md | 4 + .agent/plans/diff.md | 84 ++ include/eepp/thirdparty/dtl/dtl.hpp | 1284 +++++++++++++++++ include/eepp/ui/tools/uidiffview.hpp | 49 + include/eepp/ui/uihelper.hpp | 1 + src/eepp/ui/tools/uidiffview.cpp | 277 ++++ src/eepp/ui/uiwidgetcreator.cpp | 2 + src/tests/unit_tests/uidiffview_test.cpp | 101 ++ src/tools/ecode/ecode.cpp | 16 + src/tools/ecode/ecode.hpp | 4 + src/tools/ecode/plugins/git/gitplugin.cpp | 21 +- .../ecode/plugins/plugincontextprovider.hpp | 4 + 12 files changed, 1839 insertions(+), 8 deletions(-) create mode 100644 .agent/plans/diff.md create mode 100644 include/eepp/thirdparty/dtl/dtl.hpp create mode 100644 include/eepp/ui/tools/uidiffview.hpp create mode 100644 src/eepp/ui/tools/uidiffview.cpp create mode 100644 src/tests/unit_tests/uidiffview_test.cpp diff --git a/.agent/SOUL.md b/.agent/SOUL.md index bbaa58d9f..956cf590b 100644 --- a/.agent/SOUL.md +++ b/.agent/SOUL.md @@ -19,3 +19,7 @@ Your name is Negen (from negentropy: the process of creating order out of chaos) - Immediately after implementing a feature, you must perform a strict self-review of your changes. - Actively hunt for mistakes and inefficiencies. - Eradicate code duplication. Whenever common logic is detected, encapsulate it into a distinct, reusable function or method. + +4. **Never `git commit` any change:** + - You're an implementer, you don't manage the project, you can freely use `git` for read-only operations. + - You should **never** do write operations in `git` (no commit, no push), with a single exception: `git stash` is allowed. diff --git a/.agent/plans/diff.md b/.agent/plans/diff.md new file mode 100644 index 000000000..860e47929 --- /dev/null +++ b/.agent/plans/diff.md @@ -0,0 +1,84 @@ +# Plan: UIDiffView Implementation + +## 1. Overview and Objectives +The goal of this project is to implement `UIDiffView`, a new widget within the `eepp` UI framework that provides a rich visual representation of code differences. + +### Key Objectives: +- **Phase 1: Unified View (Single Panel):** Display a unified diff with custom line background colors (red for removed, green for added, specific styling for headers). +- **Phase 2: Proper Syntax Highlighting:** Use language-specific syntax highlighting (e.g., C++, Python) for the text, rather than treating the entire file as a `.diff` text. +- **Phase 3: Diff Generation:** Integrate `dtl.h` (or similar) to generate diff information dynamically when comparing two text documents or strings, in addition to parsing existing `.patch` or `.diff` files. +- **Phase 4: Split View (Two Panel):** Support an optional side-by-side split view. + +## 2. Architecture & Design + +### 2.1 Widget Hierarchy +- **`UIDiffView`** will be a custom composite widget, *not* a direct subclass of `UICodeEditor`. +- By composing `UICodeEditor`(s) internally, `UIDiffView` can seamlessly transition between a "Unified" single-editor mode and a "Side-by-Side" two-editor mode in the future. +- `UIDiffView` will manage an internal instance of `UICodeEditor` (for unified view) and attach a custom `UICodeEditorPlugin` to handle custom background drawing. + +### 2.2 Data Model & Diff Processing +We need a representation of diff data that abstracts away whether the diff was loaded from a file or generated on the fly. +- Create a struct `DiffLine`: + ```cpp + enum class DiffLineType { Added, Removed, Context, Header }; + struct DiffLine { + DiffLineType type; + String text; + // Original line numbers (for the gutter later) + Int64 oldLineNum; + Int64 newLineNum; + }; + ``` +- **Parsing:** If loaded from a `.patch`/`.diff` file, a parser will extract `DiffLine`s and determine the underlying file extension (e.g., from `+++ b/src/main.cpp` -> `.cpp`). +- **Generation (dtl.h):** If provided two strings or `TextDocument`s (Old vs New), we will use `dtl.h` to compute the differences and generate a unified list of `DiffLine`s. + +### 2.3 Syntax Highlighting Challenge +A standard syntax highlighter will fail if lines start with `+` or `-` because it breaks language grammar. +**Solution:** +- The internal `TextDocument` of the `UICodeEditor` will hold the *clean* text (without the leading `+` or `-`). +- The syntax highlighter will run normally, initialized with the detected base language (e.g., C++). +- The `+` and `-` indicators will be drawn visually in the gutter or injected via rendering hooks, rather than being part of the raw `TextDocument` string. Alternatively, if we keep `+` and `-` in the string, we might need a composite `SyntaxHighlighter` that delegates to the underlying language while skipping the first character. *Recommendation: Strip `+`/`-` from the document text, and draw them manually during rendering to preserve perfect syntax highlighting.* + +### 2.4 Custom Rendering (Backgrounds & Indicators) +We will leverage `UICodeEditorPlugin` to draw custom line backgrounds without modifying the core `UICodeEditor` drawing routine. +- Create `UIDiffEditorPlugin : public UICodeEditorPlugin`. +- Override `drawBeforeLineText`: + - Check the line index against the list of `DiffLine`s. + - If `DiffLineType::Added`, draw a greenish `Primitives::drawRectangle` across the editor's width. + - If `DiffLineType::Removed`, draw a reddish rectangle. + - If `DiffLineType::Header`, draw a bluish/gray rectangle. +- Override `drawGutter` (or similar) if we want to display dual line numbers (Old and New) or custom `+`/`-` icons. + +## 3. Step-by-Step Execution Plan + +### Step 1: Core Diff Parsing & Generation +1. Integrate `dtl.h` into `src/eepp/thirdparty/dtl/` (if not already present). +2. Create `DiffDocument` (or `DiffData`) utility class capable of: + - Parsing a unified diff string into a structured format. + - Generating a unified diff structure from two source strings using `dtl.h`. + - Identifying the target language extension from diff headers. + +### Step 2: Custom Rendering Plugin +1. Implement `UIDiffEditorPlugin` inheriting from `UICodeEditorPlugin`. +2. Implement background drawing in `drawBeforeLineText` based on line states provided by `DiffDocument`. +3. Test drawing performance. Ensure no memory/object allocations happen during the render loop (Negen mandate). + +### Step 3: `UIDiffView` Implementation (Unified View) +1. Create `UIDiffView` widget (`include/eepp/ui/tools/uidiffview.hpp` and `src/eepp/ui/tools/uidiffview.cpp`). +2. Instantiate a read-only `UICodeEditor` internally. +3. Apply the `UIDiffEditorPlugin` to the editor. +4. Implement `loadFromPatch(const std::string& patchText)` and `loadFromStrings(const std::string& oldText, const std::string& newText)`. +5. Set the syntax definition of the internal `TextDocument` based on the detected file extension. + +### Step 4: Gutter and Line Numbers (Refinement) +1. Hide the default line number gutter of `UICodeEditor` or override it. +2. Draw custom line numbers representing both Old and New file line numbers. + +### Step 5: Integration into ecode +1. Map the `.diff` and `.patch` extensions to open in `UIDiffView` instead of standard `UICodeEditor` inside `ecode`. +2. Add a command/shortcut to "Compare against saved version" or "Compare against Git HEAD" which generates a diff dynamically using the newly integrated `dtl.h` logic. + +## 4. Performance & Memory Considerations (Negen's Directives) +- The mapping of line index to `DiffLineType` must be fast (e.g., an `std::vector` indexed directly by `lineIndex`). +- Do not allocate strings or complex objects inside the `drawBeforeLineText` or `drawGutter` render loops. +- Avoid modifying the `UICodeEditor` document layout excessively on the fly. Build the unified document cleanly once. \ No newline at end of file diff --git a/include/eepp/thirdparty/dtl/dtl.hpp b/include/eepp/thirdparty/dtl/dtl.hpp new file mode 100644 index 000000000..0be6e7565 --- /dev/null +++ b/include/eepp/thirdparty/dtl/dtl.hpp @@ -0,0 +1,1284 @@ +/** + dtl -- Diff Template Library + + In short, Diff Template Library is distributed under so called "BSD license", + + Copyright (c) 2015 Tatsuhiko Kubo + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the authors nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef DTL_H +#define DTL_H + + +#ifndef DTL_VARIABLES_H +#define DTL_VARIABLES_H + +#include +#include +#include +#include +#include + +namespace dtl { + + using std::vector; + using std::string; + using std::pair; + using std::ostream; + using std::list; + using std::for_each; + using std::distance; + using std::fill; + using std::cout; + using std::endl; + using std::rotate; + using std::swap; + using std::max; + + /** + * version string + */ + const string version = "1.21"; + + /** + * type of edit for SES + */ + typedef int edit_t; + const edit_t SES_DELETE = -1; + const edit_t SES_COMMON = 0; + const edit_t SES_ADD = 1; + + /** + * mark of SES + */ +#define SES_MARK_DELETE "-" +#define SES_MARK_COMMON " " +#define SES_MARK_ADD "+" + + /** + * info for Unified Format + */ + typedef struct eleminfo { + long long beforeIdx; // index of prev sequence + long long afterIdx; // index of after sequence + edit_t type; // type of edit(Add, Delete, Common) + bool operator==(const eleminfo& other) const{ + return (this->beforeIdx == other.beforeIdx && this->afterIdx == other.afterIdx && this->type == other.type); + } + } elemInfo; + + const long long DTL_SEPARATE_SIZE = 3; + const long long DTL_CONTEXT_SIZE = 3; + + /** + * cordinate for registering route + */ + typedef struct Point { + long long x; // x cordinate + long long y; // y cordinate + long long k; // vertex + } P; + + /** + * limit of cordinate size + */ + const unsigned long long MAX_CORDINATES_SIZE = 2000000; + + typedef vector< long long > editPath; + typedef vector< P > editPathCordinates; + + /** + * Structure of Unified Format Hunk + */ + template + struct uniHunk { + long long a, b, c, d; // @@ -a,b +c,d @@ + vector< sesElem > common[2]; // anteroposterior commons on changes + vector< sesElem > change; // changes + long long inc_dec_count; // count of increace and decrease + }; + +#define dtl_typedefs(elem, sequence) \ + typedef pair< elem, elemInfo > sesElem; \ + typedef vector< sesElem > sesElemVec; \ + typedef vector< uniHunk< sesElem > > uniHunkVec; \ + typedef list< elem > elemList; \ + typedef vector< elem > elemVec; \ + typedef typename uniHunkVec::iterator uniHunkVec_iter; \ + typedef typename sesElemVec::iterator sesElemVec_iter; \ + typedef typename elemList::iterator elemList_iter; \ + typedef typename sequence::iterator sequence_iter; \ + typedef typename sequence::const_iterator sequence_const_iter; \ + typedef typename elemVec::iterator elemVec_iter; + + +} + +#endif // DTL_VARIABLES_H + +#ifndef DTL_FUNCTORS_H +#define DTL_FUNCTORS_H + +namespace dtl { + + /** + * printer class template + */ + template + class Printer + { + public : + Printer () : out_(cout) {} + Printer (stream& out) : out_(out) {} + virtual ~Printer () {} + virtual void operator() (const sesElem& se) const = 0; + protected : + stream& out_; + }; + + /** + * common element printer class template + */ + template + class CommonPrinter : public Printer < sesElem, stream > + { + public : + CommonPrinter () : Printer < sesElem, stream > () {} + CommonPrinter (stream& out) : Printer < sesElem, stream > (out) {} + ~CommonPrinter () {} + void operator() (const sesElem& se) const { + this->out_ << SES_MARK_COMMON << se.first << endl; + } + }; + + /** + * ses element printer class template + */ + template + class ChangePrinter : public Printer < sesElem, stream > + { + public : + ChangePrinter () : Printer < sesElem, stream > () {} + ChangePrinter (stream& out) : Printer < sesElem, stream > (out) {} + ~ChangePrinter () {} + void operator() (const sesElem& se) const { + switch (se.second.type) { + case SES_ADD: + this->out_ << SES_MARK_ADD << se.first << endl; + break; + case SES_DELETE: + this->out_ << SES_MARK_DELETE << se.first << endl; + break; + case SES_COMMON: + this->out_ << SES_MARK_COMMON << se.first << endl; + break; + } + } + }; + + /** + * unified format element printer class template + */ + template + class UniHunkPrinter + { + public : + UniHunkPrinter () : out_(cout) {} + UniHunkPrinter (stream& out) : out_(out) {} + ~UniHunkPrinter () {} + void operator() (const uniHunk< sesElem >& hunk) const { + out_ << "@@" + << " -" << hunk.a << "," << hunk.b + << " +" << hunk.c << "," << hunk.d + << " @@" << endl; + + for_each(hunk.common[0].begin(), hunk.common[0].end(), CommonPrinter< sesElem, stream >(out_)); + for_each(hunk.change.begin(), hunk.change.end(), ChangePrinter< sesElem, stream >(out_)); + for_each(hunk.common[1].begin(), hunk.common[1].end(), CommonPrinter< sesElem, stream >(out_)); + } + private : + stream& out_; + }; + + /** + * storage class template + */ + template + class Storage + { + public: + Storage(storedData& sd) : storedData_(sd) {} + virtual ~Storage() {} + virtual void operator() (const sesElem& se) const = 0; + protected: + storedData& storedData_; + }; + + /** + * compare class template + */ + template + class Compare + { + public : + Compare () {} + virtual ~Compare () {} + virtual inline bool impl (const elem& e1, const elem& e2) const { + return e1 == e2; + } + }; +} + +#endif // DTL_FUNCTORS_H + +#ifndef DTL_SEQUENCE_H +#define DTL_SEQUENCE_H + +namespace dtl { + + /** + * sequence class template + */ + template + class Sequence + { + public : + typedef vector< elem > elemVec; + Sequence () {} + virtual ~Sequence () {} + + elemVec getSequence () const { + return sequence; + } + void addSequence (elem e) { + sequence.push_back(e); + } + protected : + elemVec sequence; + }; +} + +#endif // DTL_SEQUENCE_H + +#ifndef DTL_LCS_H +#define DTL_LCS_H + +namespace dtl { + + /** + * Longest Common Subsequence template class + */ + template + class Lcs : public Sequence< elem > + { + public : + Lcs () {} + ~Lcs () {} + }; +} + +#endif // DTL_LCS_H + +#ifndef DTL_SES_H +#define DTL_SES_H + +namespace dtl { + + /** + * Shortest Edit Script template class + */ + template + class Ses : public Sequence< elem > + { + private : + typedef pair< elem, elemInfo > sesElem; + typedef vector< sesElem > sesElemVec; + public : + + Ses () : onlyAdd(true), onlyDelete(true), onlyCopy(true), deletesFirst(false) { + nextDeleteIdx = 0; + } + Ses (bool moveDel) : onlyAdd(true), onlyDelete(true), onlyCopy(true), deletesFirst(moveDel) { + nextDeleteIdx = 0; + } + ~Ses () {} + + bool isOnlyAdd () const { + return onlyAdd; + } + + bool isOnlyDelete () const { + return onlyDelete; + } + + bool isOnlyCopy () const { + return onlyCopy; + } + + bool isOnlyOneOperation () const { + return isOnlyAdd() || isOnlyDelete() || isOnlyCopy(); + } + + bool isChange () const { + return !onlyCopy; + } + + using Sequence< elem >::addSequence; + void addSequence (elem e, long long beforeIdx, long long afterIdx, const edit_t type) { + elemInfo info; + info.beforeIdx = beforeIdx; + info.afterIdx = afterIdx; + info.type = type; + sesElem pe(e, info); + if (!deletesFirst) { + sequence.push_back(pe); + } + switch (type) { + case SES_DELETE: + onlyCopy = false; + onlyAdd = false; + if (deletesFirst) { + sequence.insert(sequence.begin() + nextDeleteIdx, pe); + nextDeleteIdx++; + } + break; + case SES_COMMON: + onlyAdd = false; + onlyDelete = false; + if (deletesFirst) { + sequence.push_back(pe); + nextDeleteIdx = sequence.size(); + } + break; + case SES_ADD: + onlyDelete = false; + onlyCopy = false; + if (deletesFirst) { + sequence.push_back(pe); + } + break; + } + } + + sesElemVec getSequence () const { + return sequence; + } + private : + sesElemVec sequence; + bool onlyAdd; + bool onlyDelete; + bool onlyCopy; + bool deletesFirst; + size_t nextDeleteIdx; + }; +} + +#endif // DTL_SES_H + +#ifndef DTL_DIFF_H +#define DTL_DIFF_H + +namespace dtl { + + /** + * diff class template + * sequence must support random_access_iterator. + */ + template , typename comparator = Compare< elem > > + class Diff + { + private : + dtl_typedefs(elem, sequence) + sequence A; + sequence B; + size_t M; + size_t N; + size_t delta; + size_t offset; + long long *fp; + long long editDistance; + Lcs< elem > lcs; + Ses< elem > ses; + editPath path; + editPathCordinates pathCordinates; + bool swapped; + bool huge; + bool trivial; + bool editDistanceOnly; + uniHunkVec uniHunks; + comparator cmp; + long long ox; + long long oy; + public : + Diff () {} + + Diff (const sequence& a, + const sequence& b) : A(a), B(b), ses(false) { + init(); + } + + Diff (const sequence& a, + const sequence& b, + bool deletesFirst) : A(a), B(b), ses(deletesFirst) { + init(); + } + + Diff (const sequence& a, + const sequence& b, + const comparator& comp) : A(a), B(b), ses(false), cmp(comp) { + init(); + } + + Diff (const sequence& a, + const sequence& b, + bool deleteFirst, + const comparator& comp) : A(a), B(b), ses(deleteFirst), cmp(comp) { + init(); + } + + ~Diff() {} + + long long getEditDistance () const { + return editDistance; + } + + Lcs< elem > getLcs () const { + return lcs; + } + + elemVec getLcsVec () const { + return lcs.getSequence(); + } + + Ses< elem > getSes () const { + return ses; + } + + uniHunkVec getUniHunks () const { + return uniHunks; + } + + /* These should be deprecated */ + bool isHuge () const { + return huge; + } + + void onHuge () { + this->huge = true; + } + + void offHuge () { + this->huge = false; + } + + bool isUnserious () const { + return trivial; + } + + void onUnserious () { + this->trivial = true; + } + + void offUnserious () { + this->trivial = false; + } + + void onOnlyEditDistance () { + this->editDistanceOnly = true; + } + + /* These are the replacements for the above */ + bool hugeEnabled () const { + return huge; + } + + void enableHuge () { + this->huge = true; + } + + void disableHuge () { + this->huge = false; + } + + bool trivialEnabled () const { + return trivial; + } + + void enableTrivial () { + this->trivial = true; + } + + void disableTrivial () { + this->trivial = false; + } + + void editDistanceOnlyEnabled () { + this->editDistanceOnly = true; + } + + /** + * patching with Unified Format Hunks + */ + sequence uniPatch (const sequence& seq) { + elemList seqLst(seq.begin(), seq.end()); + sesElemVec shunk; + sesElemVec_iter vsesIt; + elemList_iter lstIt = seqLst.begin(); + long long inc_dec_total = 0; + long long gap = 1; + for (uniHunkVec_iter it=uniHunks.begin();it!=uniHunks.end();++it) { + joinSesVec(shunk, it->common[0]); + joinSesVec(shunk, it->change); + joinSesVec(shunk, it->common[1]); + it->a += inc_dec_total; + inc_dec_total += it->inc_dec_count; + for (long long i=0;ia - gap;++i) { + ++lstIt; + } + gap = it->a + it->b + it->inc_dec_count; + vsesIt = shunk.begin(); + while (vsesIt!=shunk.end()) { + switch (vsesIt->second.type) { + case SES_ADD : + seqLst.insert(lstIt, vsesIt->first); + break; + case SES_DELETE : + if (lstIt != seqLst.end()) { + lstIt = seqLst.erase(lstIt); + } + break; + case SES_COMMON : + if (lstIt != seqLst.end()) { + ++lstIt; + } + break; + default : + // no fall-through + break; + } + ++vsesIt; + } + shunk.clear(); + } + + sequence patchedSeq(seqLst.begin(), seqLst.end()); + return patchedSeq; + } + + /** + * patching with Shortest Edit Script (SES) + */ + sequence patch (const sequence& seq) const { + sesElemVec sesSeq = ses.getSequence(); + elemList seqLst(seq.begin(), seq.end()); + elemList_iter lstIt = seqLst.begin(); + for (sesElemVec_iter sesIt=sesSeq.begin();sesIt!=sesSeq.end();++sesIt) { + switch (sesIt->second.type) { + case SES_ADD : + seqLst.insert(lstIt, sesIt->first); + break; + case SES_DELETE : + lstIt = seqLst.erase(lstIt); + break; + case SES_COMMON : + ++lstIt; + break; + default : + // no through + break; + } + } + sequence patchedSeq(seqLst.begin(), seqLst.end()); + return patchedSeq; + } + + /** + * compose Longest Common Subsequence and Shortest Edit Script. + * The algorithm implemented here is based on "An O(NP) Sequence Comparison Algorithm" + * described by Sun Wu, Udi Manber and Gene Myers + */ + void compose() { + + if (isHuge()) { + pathCordinates.reserve(MAX_CORDINATES_SIZE); + } + ox = 0; + oy = 0; + long long p = -1; + fp = new long long[M + N + 3]; + fill(&fp[0], &fp[M + N + 3], -1); + path = editPath(M + N + 3); + fill(path.begin(), path.end(), -1); + ONP: + do { + ++p; + for (long long k=-p;k<=static_cast(delta)-1;++k) { + fp[k+offset] = snake(k, fp[k-1+offset]+1, fp[k+1+offset]); + } + for (long long k=static_cast(delta)+p;k>=static_cast(delta)+1;--k) { + fp[k+offset] = snake(k, fp[k-1+offset]+1, fp[k+1+offset]); + } + fp[delta+offset] = snake(static_cast(delta), fp[delta-1+offset]+1, fp[delta+1+offset]); + } while (fp[delta+offset] != static_cast(N) && pathCordinates.size() < MAX_CORDINATES_SIZE); + + editDistance += static_cast(delta) + 2 * p; + long long r = path[delta+offset]; + P cordinate; + editPathCordinates epc(0); + + // recording edit distance only + if (editDistanceOnly) { + delete[] this->fp; + return; + } + + while(r != -1) { + cordinate.x = pathCordinates[(size_t)r].x; + cordinate.y = pathCordinates[(size_t)r].y; + epc.push_back(cordinate); + r = pathCordinates[(size_t)r].k; + } + + // record Longest Common Subsequence & Shortest Edit Script + if (!recordSequence(epc)) { + pathCordinates.resize(0); + epc.resize(0); + p = -1; + goto ONP; + } + delete[] this->fp; + } + + /** + * print difference between A and B as an SES + */ + template < typename stream > + void printSES (stream& out) const { + sesElemVec ses_v = ses.getSequence(); + for_each(ses_v.begin(), ses_v.end(), ChangePrinter< sesElem, stream >(out)); + } + + void printSES (ostream& out = cout) const { + printSES< ostream >(out); + } + + /** + * print differences given an SES + */ + template < typename stream > + static void printSES (const Ses< elem >& s, stream& out) { + sesElemVec ses_v = s.getSequence(); + for_each(ses_v.begin(), ses_v.end(), ChangePrinter< sesElem, stream >(out)); + } + + static void printSES (const Ses< elem >& s, ostream& out = cout) { + printSES< ostream >(s, out); + } + + /** + * print difference between A and B as an SES with custom printer + */ + template < typename stream, template < typename SEET, typename STRT > class PT > + void printSES (stream& out) const { + sesElemVec ses_v = ses.getSequence (); + for_each (ses_v.begin (), ses_v.end(), PT < sesElem, stream > (out)); + } + + /** + * store difference between A and B as an SES with custom storage + */ + template < typename storedData, template < typename SEET, typename STRT > class ST > + void storeSES(storedData& sd) const { + sesElemVec ses_v = ses.getSequence(); + for_each(ses_v.begin(), ses_v.end(), ST < sesElem, storedData >(sd)); + } + + /** + * print difference between A and B in the Unified Format + */ + template < typename stream > + void printUnifiedFormat (stream& out) const { + for_each(uniHunks.begin(), uniHunks.end(), UniHunkPrinter< sesElem, stream >(out)); + } + + void printUnifiedFormat (ostream& out = cout) const { + printUnifiedFormat< ostream >(out); + } + + /** + * print unified format difference with given unified format hunks + */ + template < typename stream > + static void printUnifiedFormat (const uniHunkVec& hunks, stream& out) { + for_each(hunks.begin(), hunks.end(), UniHunkPrinter< sesElem >(out)); + } + + static void printUnifiedFormat (const uniHunkVec& hunks, ostream& out = cout) { + printUnifiedFormat< ostream >(hunks, out); + } + + /** + * compose Unified Format Hunks from Shortest Edit Script + */ + void composeUnifiedHunks () { + sesElemVec common[2]; + sesElemVec change; + sesElemVec ses_v = ses.getSequence(); + long long l_cnt = 1; + long long length = distance(ses_v.begin(), ses_v.end()); + long long middle = 0; + bool isMiddle, isAfter; + elemInfo einfo; + long long a, b, c, d; // @@ -a,b +c,d @@ + long long inc_dec_count = 0; + uniHunk< sesElem > hunk; + sesElemVec adds; + sesElemVec deletes; + + isMiddle = isAfter = false; + a = b = c = d = 0; + + for (sesElemVec_iter it=ses_v.begin();it!=ses_v.end();++it, ++l_cnt) { + einfo = it->second; + switch (einfo.type) { + case SES_ADD : + middle = 0; + ++inc_dec_count; + adds.push_back(*it); + if (!isMiddle) isMiddle = true; + if (isMiddle) ++d; + if (l_cnt >= length) { + joinSesVec(change, deletes); + joinSesVec(change, adds); + isAfter = true; + } + break; + case SES_DELETE : + middle = 0; + --inc_dec_count; + deletes.push_back(*it); + if (!isMiddle) isMiddle = true; + if (isMiddle) ++b; + if (l_cnt >= length) { + joinSesVec(change, deletes); + joinSesVec(change, adds); + isAfter = true; + } + break; + case SES_COMMON : + ++b;++d; + if (common[1].empty() && adds.empty() && deletes.empty() && change.empty()) { + if (static_cast(common[0].size()) < DTL_CONTEXT_SIZE) { + if (a == 0 && c == 0) { + if (!wasSwapped()) { + a = einfo.beforeIdx; + c = einfo.afterIdx; + } else { + a = einfo.afterIdx; + c = einfo.beforeIdx; + } + } + common[0].push_back(*it); + } else { + rotate(common[0].begin(), common[0].begin() + 1, common[0].end()); + common[0].pop_back(); + common[0].push_back(*it); + ++a;++c; + --b;--d; + } + } + if (isMiddle && !isAfter) { + ++middle; + joinSesVec(change, deletes); + joinSesVec(change, adds); + change.push_back(*it); + if (middle >= DTL_SEPARATE_SIZE || l_cnt >= length) { + isAfter = true; + } + adds.clear(); + deletes.clear(); + } + break; + default : + // no through + break; + } + // compose unified format hunk + if (isAfter && !change.empty()) { + sesElemVec_iter cit = it; + long long cnt = 0; + for (long long i=0;isecond.type == SES_COMMON) { + ++cnt; + } + } + if (cnt < DTL_SEPARATE_SIZE && l_cnt < length) { + middle = 0; + isAfter = false; + continue; + } + if (static_cast(common[0].size()) >= DTL_SEPARATE_SIZE) { + long long c0size = static_cast(common[0].size()); + rotate(common[0].begin(), + common[0].begin() + (size_t)c0size - DTL_SEPARATE_SIZE, + common[0].end()); + for (long long i=0;i + static Ses< elem > composeSesFromStream (stream& st) + { + elem line; + Ses< elem > ret; + long long x_idx, y_idx; + x_idx = y_idx = 1; + while (getline(st, line)) { + elem mark(line.begin(), line.begin() + 1); + elem e(line.begin() + 1, line.end()); + if (mark == SES_MARK_DELETE) { + ret.addSequence(e, x_idx, 0, SES_DELETE); + ++x_idx; + } else if (mark == SES_MARK_ADD) { + ret.addSequence(e, y_idx, 0, SES_ADD); + ++y_idx; + } else if (mark == SES_MARK_COMMON) { + ret.addSequence(e, x_idx, y_idx, SES_COMMON); + ++x_idx; + ++y_idx; + } + } + return ret; + } + + private : + /** + * initialize + */ + void init () { + M = distance(A.begin(), A.end()); + N = distance(B.begin(), B.end()); + if (M < N) { + swapped = false; + } else { + swap(A, B); + swap(M, N); + swapped = true; + } + editDistance = 0; + delta = N - M; + offset = M + 1; + huge = false; + trivial = false; + editDistanceOnly = false; + fp = NULL; + } + + /** + * search shortest path and record the path + */ + long long snake(const long long& k, const long long& above, const long long& below) { + long long r = above > below ? path[(size_t)k-1+offset] : path[(size_t)k+1+offset]; + long long y = max(above, below); + long long x = y - k; + while ((size_t)x < M && (size_t)y < N && (swapped ? cmp.impl(B[(size_t)y], A[(size_t)x]) : cmp.impl(A[(size_t)x], B[(size_t)y]))) { + ++x;++y; + } + + path[(size_t)k+offset] = static_cast(pathCordinates.size()); + if (!editDistanceOnly) { + P p; + p.x = x;p.y = y;p.k = r; + pathCordinates.push_back(p); + } + return y; + } + + /** + * record SES and LCS + */ + bool recordSequence (const editPathCordinates& v) { + sequence_const_iter x(A.begin()); + sequence_const_iter y(B.begin()); + long long x_idx, y_idx; // line number for Unified Format + long long px_idx, py_idx; // cordinates + bool complete = false; + x_idx = y_idx = 1; + px_idx = py_idx = 0; + for (size_t i=v.size()-1;!complete;--i) { + while(px_idx < v[i].x || py_idx < v[i].y) { + if (v[i].y - v[i].x > py_idx - px_idx) { + if (!wasSwapped()) { + ses.addSequence(*y, 0, y_idx + oy, SES_ADD); + } else { + ses.addSequence(*y, y_idx + oy, 0, SES_DELETE); + } + ++y; + ++y_idx; + ++py_idx; + } else if (v[i].y - v[i].x < py_idx - px_idx) { + if (!wasSwapped()) { + ses.addSequence(*x, x_idx + ox, 0, SES_DELETE); + } else { + ses.addSequence(*x, 0, x_idx + ox, SES_ADD); + } + ++x; + ++x_idx; + ++px_idx; + } else { + if (!wasSwapped()) { + lcs.addSequence(*x); + ses.addSequence(*x, x_idx + ox, y_idx + oy, SES_COMMON); + } else { + lcs.addSequence(*y); + ses.addSequence(*y, y_idx + oy, x_idx + ox, SES_COMMON); + } + ++x; + ++y; + ++x_idx; + ++y_idx; + ++px_idx; + ++py_idx; + } + } + if (i == 0) complete = true; + } + + if (x_idx > static_cast(M) && y_idx > static_cast(N)) { + // all recording succeeded + } else { + // trivial difference + if (trivialEnabled()) { + if (!wasSwapped()) { + recordOddSequence(x_idx, M, x, SES_DELETE); + recordOddSequence(y_idx, N, y, SES_ADD); + } else { + recordOddSequence(x_idx, M, x, SES_ADD); + recordOddSequence(y_idx, N, y, SES_DELETE); + } + return true; + } + + // nontrivial difference + sequence A_(A.begin() + (size_t)x_idx - 1, A.end()); + sequence B_(B.begin() + (size_t)y_idx - 1, B.end()); + A = A_; + B = B_; + M = distance(A.begin(), A.end()); + N = distance(B.begin(), B.end()); + delta = N - M; + offset = M + 1; + delete[] fp; + fp = new long long[M + N + 3]; + fill(&fp[0], &fp[M + N + 3], -1); + fill(path.begin(), path.end(), -1); + ox = x_idx - 1; + oy = y_idx - 1; + return false; + } + return true; + } + + /** + * record odd sequence in SES + */ + void inline recordOddSequence (long long idx, long long length, sequence_const_iter it, const edit_t et) { + while(idx < length){ + ses.addSequence(*it, idx, 0, et); + ++it; + ++idx; + ++editDistance; + } + ses.addSequence(*it, idx, 0, et); + ++editDistance; + } + + /** + * join SES vectors + */ + void inline joinSesVec (sesElemVec& s1, sesElemVec& s2) const { + if (!s2.empty()) { + for (sesElemVec_iter vit=s2.begin();vit!=s2.end();++vit) { + s1.push_back(*vit); + } + } + } + + /** + * check if the sequences have been swapped + */ + bool inline wasSwapped () const { + return swapped; + } + + }; +} + +#endif // DTL_DIFF_H + +#ifndef DTL_DIFF3_H +#define DTL_DIFF3_H + +namespace dtl { + + /** + * diff3 class template + * sequence must support random_access_iterator. + */ + template , typename comparator = Compare< elem > > + class Diff3 + { + private: + dtl_typedefs(elem, sequence) + sequence A; + sequence B; + sequence C; + sequence S; + Diff< elem, sequence, comparator > diff_ba; + Diff< elem, sequence, comparator > diff_bc; + bool conflict; + elem csepabegin; + elem csepa; + elem csepaend; + public : + Diff3 () {} + Diff3 (const sequence& a, + const sequence& b, + const sequence& c) : A(a), B(b), C(c), + diff_ba(b, a), diff_bc(b, c), + conflict(false) {} + + ~Diff3 () {} + + bool isConflict () const { + return conflict; + } + + sequence getMergedSequence () const { + return S; + } + + /** + * merge changes B and C into A + */ + bool merge () { + if (diff_ba.getEditDistance() == 0) { // A == B + if (diff_bc.getEditDistance() == 0) { // A == B == C + S = B; + return true; + } + S = C; + return true; + } else { // A != B + if (diff_bc.getEditDistance() == 0) { // A != B == C + S = A; + return true; + } else { // A != B != C + S = merge_(); + if (isConflict()) { // conflict occured + return false; + } + } + } + return true; + } + + /** + * compose differences + */ + void compose () { + diff_ba.compose(); + diff_bc.compose(); + } + + private : + /** + * merge implementation + */ + sequence merge_ () { + elemVec seq; + Ses< elem > ses_ba = diff_ba.getSes(); + Ses< elem > ses_bc = diff_bc.getSes(); + sesElemVec ses_ba_v = ses_ba.getSequence(); + sesElemVec ses_bc_v = ses_bc.getSequence(); + sesElemVec_iter ba_it = ses_ba_v.begin(); + sesElemVec_iter bc_it = ses_bc_v.begin(); + sesElemVec_iter ba_end = ses_ba_v.end(); + sesElemVec_iter bc_end = ses_bc_v.end(); + + while (!isEnd(ba_end, ba_it) || !isEnd(bc_end, bc_it)) { + while (true) { + if (!isEnd(ba_end, ba_it) && + !isEnd(bc_end, bc_it) && + ba_it->first == bc_it->first && + ba_it->second.type == SES_COMMON && + bc_it->second.type == SES_COMMON) { + // do nothing + } else { + break; + } + if (!isEnd(ba_end, ba_it)) seq.push_back(ba_it->first); + else if (!isEnd(bc_end, bc_it)) seq.push_back(bc_it->first); + forwardUntilEnd(ba_end, ba_it); + forwardUntilEnd(bc_end, bc_it); + } + if (isEnd(ba_end, ba_it) || isEnd(bc_end, bc_it)) break; + if ( ba_it->second.type == SES_COMMON + && bc_it->second.type == SES_DELETE) { + forwardUntilEnd(ba_end, ba_it); + forwardUntilEnd(bc_end, bc_it); + } else if (ba_it->second.type == SES_COMMON && + bc_it->second.type == SES_ADD) { + seq.push_back(bc_it->first); + forwardUntilEnd(bc_end, bc_it); + } else if (ba_it->second.type == SES_DELETE && + bc_it->second.type == SES_COMMON) { + forwardUntilEnd(ba_end, ba_it); + forwardUntilEnd(bc_end, bc_it); + } else if (ba_it->second.type == SES_DELETE && + bc_it->second.type == SES_DELETE) { + if (ba_it->first == bc_it->first) { + forwardUntilEnd(ba_end, ba_it); + forwardUntilEnd(bc_end, bc_it); + } else { + // conflict + conflict = true; + return B; + } + } else if (ba_it->second.type == SES_DELETE && + bc_it->second.type == SES_ADD) { + // conflict + conflict = true; + return B; + } else if (ba_it->second.type == SES_ADD && + bc_it->second.type == SES_COMMON) { + seq.push_back(ba_it->first); + forwardUntilEnd(ba_end, ba_it); + } else if (ba_it->second.type == SES_ADD && + bc_it->second.type == SES_DELETE) { + // conflict + conflict = true; + return B; + } else if (ba_it->second.type == SES_ADD && + bc_it->second.type == SES_ADD) { + if (ba_it->first == bc_it->first) { + seq.push_back(ba_it->first); + forwardUntilEnd(ba_end, ba_it); + forwardUntilEnd(bc_end, bc_it); + } else { + // conflict + conflict = true; + return B; + } + } + } + + if (isEnd(ba_end, ba_it)) { + addDecentSequence(bc_end, bc_it, seq); + } else if (isEnd(bc_end, bc_it)) { + addDecentSequence(ba_end, ba_it, seq); + } + + sequence mergedSeq(seq.begin(), seq.end()); + return mergedSeq; + } + + /** + * join elem vectors + */ + void inline joinElemVec (elemVec& s1, elemVec& s2) const { + if (!s2.empty()) { + for (elemVec_iter vit=s2.begin();vit!=s2.end();++vit) { + s1.push_back(*vit); + } + } + } + + /** + * check if sequence is at end + */ + template + bool inline isEnd (const T_iter& end, const T_iter& it) const { + return it == end ? true : false; + } + + /** + * increment iterator until iterator is at end + */ + template + void inline forwardUntilEnd (const T_iter& end, T_iter& it) const { + if (!isEnd(end, it)) ++it; + } + + /** + * add elements whose SES's type is ADD + */ + void inline addDecentSequence (const sesElemVec_iter& end, sesElemVec_iter& it, elemVec& seq) const { + while (!isEnd(end, it)) { + if (it->second.type == SES_ADD) seq.push_back(it->first); + ++it; + } + } + + }; +} + +#endif // DTL_DIFF3_H + +#endif // DTL_H diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp new file mode 100644 index 000000000..a4507423a --- /dev/null +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -0,0 +1,49 @@ +#ifndef EE_UI_TOOLS_UIDIFFVIEW_HPP +#define EE_UI_TOOLS_UIDIFFVIEW_HPP + +#include +#include + +namespace EE { namespace UI { namespace Tools { + +class UIDiffEditorPlugin; + +class EE_API UIDiffView : public UIWidget { + public: + static UIDiffView* New(); + + UIDiffView(); + virtual ~UIDiffView(); + + virtual Uint32 getType() const override; + virtual bool isType( const Uint32& type ) const override; + + void loadFromPatch( const std::string& patchText ); + void loadFromStrings( const std::string& oldText, const std::string& newText ); + void loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ); + + UICodeEditor* getEditor() const { return mEditor; } + + enum class DiffLineType { Common, Added, Removed, Header }; + struct DiffLine { + DiffLineType type{ DiffLineType::Common }; + String text; + Int64 oldLineNum{ 0 }; + Int64 newLineNum{ 0 }; + }; + + const std::vector& getDiffLines() const { return mLines; } + + protected: + virtual void onSizeChange() override; + + UICodeEditor* mEditor{ nullptr }; + std::shared_ptr mPlugin; + std::vector mLines; + + void buildEditor(); +}; + +}}} // namespace EE::UI::Tools + +#endif // EE_UI_TOOLS_UIDIFFVIEW_HPP diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index e1aa2bc5b..0dc2470c7 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -119,6 +119,7 @@ enum UINodeType { UI_TYPE_HTML_TABLE_ROW, UI_TYPE_HTML_TABLE_CELL, UI_TYPE_DROPDOWNMODELLIST, + UI_TYPE_DIFF_VIEW, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, UI_TYPE_USER = 200000, diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp new file mode 100644 index 000000000..fb92efd52 --- /dev/null +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -0,0 +1,277 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { namespace Tools { + +class UIDiffEditorPlugin : public UICodeEditorPlugin { + public: + UIDiffEditorPlugin( UIDiffView* view ) : mView( view ) {} + + std::string getId() override { return "UIDiffEditorPlugin"; } + std::string getTitle() override { return "UIDiffEditorPlugin"; } + std::string getDescription() override { return "Highlights diff added/removed lines."; } + bool isReady() const override { return true; } + + void onRegister( UICodeEditor* editor ) override { + editor->registerGutterSpace( this, PixelDensity::dpToPx( 80 ), 0 ); + } + + void onUnregister( UICodeEditor* editor ) override { editor->unregisterGutterSpace( this ); } + + void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position, + const Float& /*fontSize*/, const Float& lineHeight ) override { + if ( !mView || index < 0 || index >= (Int64)mView->getDiffLines().size() ) + return; + + const auto& lines = mView->getDiffLines(); + const auto& line = lines[index]; + + Color bgColor( Color::Transparent ); + if ( line.type == UIDiffView::DiffLineType::Added ) { + bgColor = Color( 0, 150, 32, 50 ); + } else if ( line.type == UIDiffView::DiffLineType::Removed ) { + bgColor = Color( 180, 0, 32, 50 ); + } else if ( line.type == UIDiffView::DiffLineType::Header ) { + bgColor = Color( 100, 100, 100, 50 ); + } + + if ( bgColor != Color::Transparent ) { + Primitives p; + p.setColor( bgColor ); + p.drawRectangle( Rectf( Vector2f( editor->getScreenPos().x, position.y ), + Sizef( editor->getPixelsSize().getWidth(), lineHeight ) ) ); + } + } + + void drawGutter( UICodeEditor* editor, const Int64& index, const Vector2f& screenStart, + const Float& lineHeight, const Float& gutterWidth, + const Float& fontSize ) override { + if ( !mView || index < 0 || index >= (Int64)mView->getDiffLines().size() ) + return; + + const auto& lines = mView->getDiffLines(); + const auto& line = lines[index]; + + Color bgColor( Color::Transparent ); + if ( line.type == UIDiffView::DiffLineType::Added ) { + bgColor = Color( 0, 150, 32, 50 ); + } else if ( line.type == UIDiffView::DiffLineType::Removed ) { + bgColor = Color( 180, 0, 32, 50 ); + } else if ( line.type == UIDiffView::DiffLineType::Header ) { + bgColor = Color( 100, 100, 100, 50 ); + } + + if ( bgColor != Color::Transparent ) { + Primitives p; + p.setColor( bgColor ); + p.drawRectangle( Rectf( screenStart, Sizef( gutterWidth, lineHeight ) ) ); + } + + String text; + if ( line.type == UIDiffView::DiffLineType::Added ) { + text = String::format( "%5s %5lld", "", (long long)line.newLineNum ); + } else if ( line.type == UIDiffView::DiffLineType::Removed ) { + text = String::format( "%5lld %5s", (long long)line.oldLineNum, "" ); + } else if ( line.type == UIDiffView::DiffLineType::Common ) { + text = String::format( "%5lld %5lld", (long long)line.oldLineNum, + (long long)line.newLineNum ); + } else { + return; + } + + mText.setFont( editor->getFont() ); + mText.setFontSize( fontSize ); + mText.setFillColor( + editor->getColorScheme().getEditorColor( Doc::SyntaxStyleTypes::LineNumber ) ); + mText.setString( text ); + + Float textWidth = mText.getTextWidth(); + Vector2f pos( screenStart.x + ( gutterWidth - textWidth ) * 0.5f, + screenStart.y + ( lineHeight - mText.getTextHeight() ) * 0.5f ); + mText.draw( pos.x, pos.y ); + } + + protected: + UIDiffView* mView; + Text mText; +}; + +UIDiffView* UIDiffView::New() { + return eeNew( UIDiffView, () ); +} + +UIDiffView::UIDiffView() : UIWidget( "diffview" ) { + setFlags( UI_AUTO_SIZE ); + mEditor = UICodeEditor::New(); + mEditor->setParent( this ); + mEditor->setDocument( std::make_shared() ); + mEditor->setLocked( true ); + mEditor->setShowLineNumber( false ); + + mPlugin = std::make_shared( this ); + mEditor->registerPlugin( mPlugin.get() ); +} + +UIDiffView::~UIDiffView() { + if ( mEditor && mPlugin ) { + mEditor->unregisterPlugin( mPlugin.get() ); + } +} + +Uint32 UIDiffView::getType() const { + return UI_TYPE_DIFF_VIEW; +} + +bool UIDiffView::isType( const Uint32& type ) const { + return UIDiffView::getType() == type ? true : UIWidget::isType( type ); +} + +void UIDiffView::onSizeChange() { + if ( mEditor ) + mEditor->setPixelsSize( getPixelsSize() ); + UIWidget::onSizeChange(); +} + +void UIDiffView::loadFromPatch( const std::string& patchText ) { + mLines.clear(); + auto lines = String::split( patchText, '\n' ); + std::string cleanText; + + Int64 oldLineNum = 0; + Int64 newLineNum = 0; + std::string filename; + + for ( const auto& line : lines ) { + if ( String::startsWith( line, "diff " ) || String::startsWith( line, "index " ) || + String::startsWith( line, "--- " ) || String::startsWith( line, "+++ " ) ) { + if ( String::startsWith( line, "+++ " ) ) { + filename = line.substr( 4 ); + if ( String::startsWith( filename, "b/" ) ) + filename = filename.substr( 2 ); + filename = String::trim( filename ); + } + continue; + } + + if ( String::startsWith( line, "@@ " ) ) { + size_t minusPos = line.find( "-" ); + size_t plusPos = line.find( "+" ); + if ( minusPos != std::string::npos && plusPos != std::string::npos ) { + size_t commaPos = line.find( ",", minusPos ); + size_t spacePos = line.find( " ", minusPos ); + if ( commaPos != std::string::npos && commaPos < spacePos ) { + oldLineNum = + std::stoll( line.substr( minusPos + 1, commaPos - minusPos - 1 ) ) - 1; + } else if ( spacePos != std::string::npos ) { + oldLineNum = + std::stoll( line.substr( minusPos + 1, spacePos - minusPos - 1 ) ) - 1; + } + + commaPos = line.find( ",", plusPos ); + spacePos = line.find( " ", plusPos ); + if ( commaPos != std::string::npos && commaPos < spacePos ) { + newLineNum = + std::stoll( line.substr( plusPos + 1, commaPos - plusPos - 1 ) ) - 1; + } else if ( spacePos != std::string::npos ) { + newLineNum = + std::stoll( line.substr( plusPos + 1, spacePos - plusPos - 1 ) ) - 1; + } + } + continue; + } + + DiffLine dline; + dline.text = line; + + if ( String::startsWith( line, "+" ) ) { + dline.type = DiffLineType::Added; + dline.text = line.substr( 1 ); + dline.newLineNum = ++newLineNum; + } else if ( String::startsWith( line, "-" ) ) { + dline.type = DiffLineType::Removed; + dline.text = line.substr( 1 ); + dline.oldLineNum = ++oldLineNum; + } else if ( String::startsWith( line, " " ) ) { + dline.type = DiffLineType::Common; + dline.text = line.substr( 1 ); + dline.oldLineNum = ++oldLineNum; + dline.newLineNum = ++newLineNum; + } else { + dline.type = DiffLineType::Common; + dline.oldLineNum = ++oldLineNum; + dline.newLineNum = ++newLineNum; + } + + cleanText += dline.text + "\n"; + mLines.push_back( dline ); + } + + mEditor->getDocument().reset(); + mEditor->getDocument().textInput( cleanText ); + + if ( !filename.empty() ) { + auto def = Doc::SyntaxDefinitionManager::instance()->getByExtension( filename ); + if ( def.getLanguageIndex() > 1 ) + mEditor->getDocument().setSyntaxDefinition( def ); + } +} + +void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& newText ) { + mLines.clear(); + + std::vector leftLines = String::split( oldText, '\n' ); + std::vector rightLines = String::split( newText, '\n' ); + + dtl::Diff diff( leftLines, rightLines ); + diff.compose(); + auto ranges = diff.getSes().getSequence(); + + size_t leftIndex = 0; + size_t rightIndex = 0; + std::string cleanText; + + for ( auto& pair : ranges ) { + DiffLine dline; + dline.text = pair.first; + switch ( pair.second.type ) { + case dtl::SES_COMMON: + dline.type = DiffLineType::Common; + dline.oldLineNum = ++leftIndex; + dline.newLineNum = ++rightIndex; + break; + case dtl::SES_ADD: + dline.type = DiffLineType::Added; + dline.newLineNum = ++rightIndex; + break; + case dtl::SES_DELETE: + dline.type = DiffLineType::Removed; + dline.oldLineNum = ++leftIndex; + break; + } + mLines.push_back( dline ); + cleanText += dline.text + "\n"; + } + + mEditor->getDocument().reset(); + mEditor->getDocument().textInput( cleanText ); +} + +void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ) { + std::string oldText, newText; + FileSystem::fileGet( oldFilePath, oldText ); + FileSystem::fileGet( newFilePath, newText ); + loadFromStrings( oldText, newText ); + + auto def = Doc::SyntaxDefinitionManager::instance()->getByExtension( oldFilePath ); + if ( def.getLanguageIndex() > 1 ) + mEditor->getDocument().setSyntaxDefinition( def ); +} + +}}} // namespace EE::UI::Tools diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index 7c4a164d7..810ce6b2a 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -97,6 +98,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["stacklayout"] = UIStackLayout::New; registeredWidget["viewpager"] = UIViewPager::New; registeredWidget["codeeditor"] = UICodeEditor::New; + registeredWidget["diffview"] = Tools::UIDiffView::New; registeredWidget["splitter"] = UISplitter::New; registeredWidget["treeview"] = UITreeView::New; registeredWidget["tableview"] = UITableView::New; diff --git a/src/tests/unit_tests/uidiffview_test.cpp b/src/tests/unit_tests/uidiffview_test.cpp new file mode 100644 index 000000000..5cf08c401 --- /dev/null +++ b/src/tests/unit_tests/uidiffview_test.cpp @@ -0,0 +1,101 @@ +#include "utest.h" +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::UI::Tools; + +UTEST( UIDiffView, LoadFromStringsAndVerifyDiffLines ) { + UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } ); + UIDiffView* diffView = UIDiffView::New(); + + std::string oldText = "line 1\nline 2\nline 3\nline 4"; + std::string newText = "line 1\nline 2 changed\nline 3\nline 4 added\nline 5"; + + diffView->loadFromStrings( oldText, newText ); + + const auto& lines = diffView->getDiffLines(); + + ASSERT_EQ( (size_t)7, lines.size() ); + + ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); + ASSERT_TRUE( lines[0].text.toUtf8() == "line 1" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[1].type ); + ASSERT_TRUE( lines[1].text.toUtf8() == "line 2 changed" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[2].type ); + ASSERT_TRUE( lines[2].text.toUtf8() == "line 2" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); + ASSERT_TRUE( lines[3].text.toUtf8() == "line 3" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[4].type ); + ASSERT_TRUE( lines[4].text.toUtf8() == "line 4 added" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[5].type ); + ASSERT_TRUE( lines[5].text.toUtf8() == "line 5" ); + + ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[6].type ); + ASSERT_TRUE( lines[6].text.toUtf8() == "line 4" ); + + const auto& text = diffView->getEditor()->getDocument().getText(); + + std::string expectedCleanText = "line 1\nline 2 changed\nline 2\nline 3\nline 4 added\nline 5\nline 4\n"; + + std::string textUtf8 = text.toUtf8(); + ASSERT_TRUE( expectedCleanText == textUtf8 ); + + eeDelete( diffView ); +} + +UTEST( UIDiffView, LoadFromPatchAndVerifyCleanText ) { + UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } ); + UIDiffView* diffView = UIDiffView::New(); + + std::string patchText = R"patch(+++ b/src/main.cpp +--- a/src/main.cpp +@@ -1,3 +1,3 @@ + int main() { +- return 0; ++ return 1; + } +)patch"; + + diffView->loadFromPatch( patchText ); + + const auto& lines = diffView->getDiffLines(); + + ASSERT_EQ( (size_t)4, lines.size() ); + + ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[0].type ); + ASSERT_TRUE( lines[0].text.toUtf8() == "int main() {" ); + ASSERT_EQ( 1, lines[0].oldLineNum ); + ASSERT_EQ( 1, lines[0].newLineNum ); + + ASSERT_EQ( UIDiffView::DiffLineType::Removed, lines[1].type ); + ASSERT_TRUE( lines[1].text.toUtf8() == " return 0;" ); + ASSERT_EQ( 2, lines[1].oldLineNum ); + + ASSERT_EQ( UIDiffView::DiffLineType::Added, lines[2].type ); + ASSERT_TRUE( lines[2].text.toUtf8() == " return 1;" ); + ASSERT_EQ( 2, lines[2].newLineNum ); + + ASSERT_EQ( UIDiffView::DiffLineType::Common, lines[3].type ); + ASSERT_TRUE( lines[3].text.toUtf8() == "}" ); + ASSERT_EQ( 3, lines[3].oldLineNum ); + ASSERT_EQ( 3, lines[3].newLineNum ); + + const auto& text = diffView->getEditor()->getDocument().getText(); + + std::string expectedCleanText = "int main() {\n return 0;\n return 1;\n}\n"; + + std::string textUtf8 = text.toUtf8(); + ASSERT_TRUE( expectedCleanText == textUtf8 ); + + eeDelete( diffView ); +} diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 40f29b57a..ea7bc37b5 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -11,6 +11,7 @@ #include "uitreeviewfs.hpp" #include "uiwelcomescreen.hpp" #include "version.hpp" +#include #include #include #include @@ -2606,6 +2607,19 @@ void App::loadAudioFromPath( const std::string& path, bool autoPlay ) { audioPlayer->loadFromPath( path, autoPlay ); } +void App::loadDiffFromPath( const std::string& path ) { + auto* diffView = Tools::UIDiffView::New(); + auto [tab, iv] = mSplitter->createWidget( diffView, i18n( "diff_viewer", "Diff Viewer" ) ); + tab->setText( FileSystem::fileNameFromPath( path ) )->setTooltipText( path ); + auto icon = findIcon( "filetype-patch" ); + tab->setIcon( icon ? icon : findIcon( "file" ) ); + + std::string text; + if ( FileSystem::fileGet( path, text ) ) { + diffView->loadFromPatch( text ); + } +} + void App::openFileFromPath( const std::string& path ) { std::string ext = FileSystem::fileExtension( path ); if ( !Image::isImageExtension( path ) && !SoundFileFactory::isKnownFileExtension( path ) && @@ -2658,6 +2672,8 @@ bool App::loadFileFromPath( } else if ( ( SoundFileFactory::isKnownFileExtension( path ) || tryFindMimeType ) && SoundFileFactory::isValidAudioFile( path ) ) { loadAudioFromPath( path ); + } else if ( ext == "diff" || ext == "patch" ) { + loadDiffFromPath( path ); } else if ( !openBinaryAsDocument && PathHelper::isOpenExternalExtension( ext ) ) { Engine::instance()->openURI( path ); } else if ( tryFindMimeType && TextDocument::fileMightBeBinary( path ) ) { diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 02b4cb64d..63e37dfab 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -556,6 +556,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void loadAudioFromPath( const std::string& path, bool autoPlay = true ); + void loadDiffFromPath( const std::string& path ); + void createAndShowRecentFolderPopUpMenu( Node* recentFoldersBut ); void createAndShowRecentFilesPopUpMenu( Node* recentFilesBut ); @@ -630,6 +632,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { const SyntaxColorScheme* getCurrentColorScheme() const; + size_t getMenuIconSize() const { return mMenuIconSize; } + protected: std::vector mArgs; EE::Window::Window* mWindow{ nullptr }; diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index 578534496..ab4d8db00 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1115,14 +1116,18 @@ void GitPlugin::diff( const std::string& file, bool isStaged ) { return; getUISceneNode()->runOnMainThread( [this, file, res] { - auto ret = mManager->getSplitter()->createEditorInNewTab(); - auto doc = ret.second->getDocumentRef(); - doc->setDefaultFileName( FileSystem::fileNameFromPath( file ) + ".diff" ); - doc->textInput( res.result, false ); - doc->moveToStartOfDoc(); - doc->resetUndoRedo(); - ret.second->setSyntaxDefinition( - SyntaxDefinitionManager::instance()->getByLSPName( "diff" ) ); + auto* diffView = Tools::UIDiffView::New(); + auto [tab, iv] = getPluginContext()->getSplitter()->createWidget( + diffView, i18n( "diff_viewer", "Diff Viewer" ) ); + std::string fileName = FileSystem::fileNameFromPath( file ); + tab->setText( fileName )->setTooltipText( file ); + UIIcon* icon = getUISceneNode()->findIcon( + UIIconThemeManager::getIconNameFromFileName( fileName ) ); + if ( !icon ) + icon = getUISceneNode()->findIcon( "file" ); + if ( icon ) + tab->setIcon( icon->getSize( getPluginContext()->getMenuIconSize() ) ); + diffView->loadFromPatch( res.result ); } ); } ); } diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index 92b29c332..9467e9051 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -144,6 +144,8 @@ class PluginContextProvider { virtual void loadImageFromPath( const std::string& path ) = 0; + virtual void loadDiffFromPath( const std::string& path ) = 0; + virtual void loadFolder( std::string path, bool forceNewWindow = false ) = 0; virtual void showGlobalSearch( bool searchAndReplace, @@ -152,6 +154,8 @@ class PluginContextProvider { virtual const std::unordered_map& getStatusBarKeybindings() const = 0; virtual bool projectIsOpen() const = 0; + + virtual size_t getMenuIconSize() const = 0; }; } // namespace ecode