tdf#157663 SW: Tracked change improve move

Made accept/reject handle move redlines other pair, (moveto-movefrom)
and handle the whole move redline, even if it is split into small pieces
that separated from each other.

Added unique ID to every move redline to help find their other parts.
This move ID is generated in case of:
move recognition
moveing a paragraph. (directly create move redline with unique id without
calling the recognition it is faster and more stable)

(there are other cases that could be improved to not use recognition,
but generate ID directly, like moveing selected partial text with mouse)

Implemented the odt export/import of this move ID.
it is a tag like this: "<loext:move-id>4</loext:move-id>"
next to creator/date

Improved the docx import to generate this move ID, so move redlines can
find their other parts
(Not changed Docx export... it works a bit, but far from perfect)

Improved move reckognition:
It can find them even if they are split into multiple parts differently.
(like "ab"+"cd" == "a"+"bcd")
Disabled this because of probably performance issue.

made a complex unit test for it.

Note: Left the move recognition on every place, to avoid as much
regressions as possible.. but in the future, we may can disable it
in some cases.
Note2: We will have to keep move recognitnion, because there are documents
from past, saved without any move informations in the file, and users
expect to see move redlines there. (generated by the recognition.)

Change-Id: If968d4235b676c5e538cfaf4187a4482a86eae9f
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/157740
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Caolán McNamara <caolan.mcnamara@collabora.com>
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/158611
Tested-by: Jenkins
diff --git a/include/xmloff/txtimp.hxx b/include/xmloff/txtimp.hxx
index 24caf36..c04e577 100644
--- a/include/xmloff/txtimp.hxx
+++ b/include/xmloff/txtimp.hxx
@@ -381,6 +381,8 @@ public:
            const OUString& rComment,
            /// date+time
            const css::util::DateTime& rDateTime,
            /// move id, to find other parts (moveFrom/MoveTo)
            const OUString& rMoveId,
            /// merge last paras
            bool bMergeLastParagraph);

diff --git a/include/xmloff/xmltoken.hxx b/include/xmloff/xmltoken.hxx
index 00d0d5f..d3c1b93 100644
--- a/include/xmloff/xmltoken.hxx
+++ b/include/xmloff/xmltoken.hxx
@@ -1346,6 +1346,7 @@ namespace xmloff::token {
        XML_MOVE_FROM_LEFT,
        XML_MOVE_FROM_RIGHT,
        XML_MOVE_FROM_TOP,
        XML_MOVE_ID,
        XML_MOVE_PROTECT,
        XML_MOVE_SHORT,
        XML_MOVEMENT,
diff --git a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
index ec0a86a..bf57af9 100644
--- a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
+++ b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng
@@ -3791,6 +3791,15 @@ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.
    </rng:optional>
  </rng:define>

  <!-- TODO(aszucs) no proposal - unique identifier for move redline -->
  <rng:define name="office-change-info" combine="interleave">
    <rng:optional>
      <rng:attribute name="loext:move-id">
        <rng:ref name="integer"/>
      </rng:attribute>
    </rng:optional>
  </rng:define>

  <!-- Belongs to project MCGR (Armin Le Grand) LO 7.6
       Intended to be used for theme colors too  -->
  <rng:define name="common-complex-color-attributes">
diff --git a/sw/inc/IDocumentContentOperations.hxx b/sw/inc/IDocumentContentOperations.hxx
index 43e2c84..eafc586 100644
--- a/sw/inc/IDocumentContentOperations.hxx
+++ b/sw/inc/IDocumentContentOperations.hxx
@@ -132,7 +132,7 @@ public:
        rPam. If false, then no such check will be performed, and it is assumed
        that the caller took care of verifying this constraint already.
     */
    virtual bool CopyRange(SwPaM& rPam, SwPosition& rPos, SwCopyFlags flags) const = 0;
    virtual bool CopyRange(SwPaM& rPam, SwPosition& rPos, SwCopyFlags flags, sal_uInt32 nMovedID = 0) const = 0;

    /** Delete section containing the node.
    */
diff --git a/sw/inc/crsrsh.hxx b/sw/inc/crsrsh.hxx
index fd28607..2063664 100644
--- a/sw/inc/crsrsh.hxx
+++ b/sw/inc/crsrsh.hxx
@@ -166,7 +166,7 @@ public:
        READONLY    = (1 << 3)      ///< make visible in spite of Readonly
    };

    SAL_DLLPRIVATE void UpdateCursor(
    void UpdateCursor(
        sal_uInt16 eFlags = SwCursorShell::SCROLLWIN|SwCursorShell::CHKRANGE,
        bool bIdleEnd = false );

diff --git a/sw/inc/docary.hxx b/sw/inc/docary.hxx
index 64f2519..6f2c2c3 100644
--- a/sw/inc/docary.hxx
+++ b/sw/inc/docary.hxx
@@ -227,6 +227,7 @@ private:
    /// Sometimes we load bad data, and we need to know if we can use
    /// fast binary search, or if we have to fall back to a linear search
    bool m_bHasOverlappingElements = false;
    mutable sal_uInt32 m_nMaxMovedID = 1;   //every move-redline pair get a unique ID, so they can find each other.
public:
    ~SwRedlineTable();
    bool Contains(const SwRangeRedline* p) const { return maVector.find(const_cast<SwRangeRedline*>(p)) != maVector.end(); }
@@ -263,6 +264,13 @@ public:
    // is there a redline with the same text content from the same author (near the redline),
    // but with the opposite type (Insert or Delete). It's used to recognize tracked text moving.
    bool isMoved(size_type tableIndex) const;
    bool isMovedImpl(size_type tableIndex, bool bTryCombined) const;
    sal_uInt32 getNewMovedID() const { return ++m_nMaxMovedID; }
    void setMovedIDIfNeeded(sal_uInt32 nMax);
    void getConnectedArea(size_type nPosOrigin, size_type& rPosStart, size_type& rPosEnd,
                          bool bCheckChilds) const;
    OUString getTextOfArea(size_type rPosStart, size_type rPosEnd) const;


    bool                        empty() const { return maVector.empty(); }
    size_type                   size() const { return maVector.size(); }
diff --git a/sw/inc/redline.hxx b/sw/inc/redline.hxx
index d8eba64..a82c785 100644
--- a/sw/inc/redline.hxx
+++ b/sw/inc/redline.hxx
@@ -95,14 +95,14 @@ class SW_DLLPUBLIC SwRedlineData
    RedlineType m_eType;
    sal_uInt16 m_nSeqNo;
    bool m_bAutoFormat;
    bool m_bMoved;
    sal_uInt32 m_nMovedID;  // 0 == not moved, 1 == moved, but dont have its pair, 2+ == unique ID

public:
    SwRedlineData( RedlineType eT, std::size_t nAut );
    SwRedlineData( RedlineType eT, std::size_t nAut, sal_uInt32 nMoveID = 0 );
    SwRedlineData( const SwRedlineData& rCpy, bool bCpyNext = true );

    // For sw3io: pNext/pExtraData are taken over.
    SwRedlineData( RedlineType eT, std::size_t nAut, const DateTime& rDT,
    SwRedlineData( RedlineType eT, std::size_t nAut, const DateTime& rDT, sal_uInt32 nMovedID,
                   OUString aCmnt, SwRedlineData* pNxt );

    ~SwRedlineData();
@@ -112,7 +112,7 @@ public:
            return m_nAuthor == rCmp.m_nAuthor &&
                    m_eType == rCmp.m_eType &&
                    m_bAutoFormat == rCmp.m_bAutoFormat &&
                    m_bMoved == rCmp.m_bMoved &&
                    m_nMovedID == rCmp.m_nMovedID &&
                    m_sComment == rCmp.m_sComment &&
                    (( !m_pNext && !rCmp.m_pNext ) ||
                        ( m_pNext && rCmp.m_pNext && *m_pNext == *rCmp.m_pNext )) &&
@@ -141,8 +141,9 @@ public:

    void SetAutoFormat() { m_bAutoFormat = true; }
    bool IsAutoFormat() const { return m_bAutoFormat; }
    void SetMoved() { m_bMoved = true; }
    bool IsMoved() const { return m_bMoved; }
    void SetMoved( sal_uInt32 nMoveID ) { m_nMovedID = nMoveID; }
    sal_uInt32 GetMoved() const { return m_nMovedID; }
    bool IsMoved() const { return m_nMovedID != 0; }
    bool CanCombine( const SwRedlineData& rCmp ) const;
    bool CanCombineForAcceptReject( const SwRedlineData& rCmp ) const;

@@ -179,7 +180,7 @@ class SW_DLLPUBLIC SwRangeRedline final : public SwPaM
public:
    static sal_uInt32 s_nLastId;

    SwRangeRedline( RedlineType eType, const SwPaM& rPam );
    SwRangeRedline( RedlineType eType, const SwPaM& rPam, sal_uInt32 nMoveID = 0 );
    SwRangeRedline( const SwRedlineData& rData, const SwPaM& rPam );
    SwRangeRedline( const SwRedlineData& rData, const SwPosition& rPos );
    // For sw3io: pData is taken over!
@@ -218,7 +219,8 @@ public:
    sal_uInt16 GetStackCount() const;
    std::size_t GetAuthor( sal_uInt16 nPos = 0) const;
    OUString const & GetAuthorString( sal_uInt16 nPos = 0 ) const;
    const DateTime& GetTimeStamp( sal_uInt16 nPos = 0) const;
    sal_uInt32 GetMovedID(sal_uInt16 nPos = 0) const;
    const DateTime& GetTimeStamp(sal_uInt16 nPos = 0) const;
    RedlineType GetType( sal_uInt16 nPos = 0 ) const;
    // text content of the redline is only an annotation placeholder
    // (i.e. a comment, but don't confuse it with comment of the redline)
@@ -282,8 +284,9 @@ public:

    void MaybeNotifyRedlinePositionModification(tools::Long nTop);

    void SetMoved() {  m_pRedlineData->SetMoved(); }
    void SetMoved(sal_uInt32 nMoveID = 1) { m_pRedlineData->SetMoved(nMoveID); }
    bool IsMoved() const { return m_pRedlineData->IsMoved(); }
    sal_uInt32 GetMoved(sal_uInt16 nPos = 0) const { return GetRedlineData(nPos).GetMoved(); }
};

void MaybeNotifyRedlineModification(SwRangeRedline& rRedline, SwDoc& rDoc);
diff --git a/sw/inc/unoprnms.hxx b/sw/inc/unoprnms.hxx
index ca01d55..1ab2395f 100644
--- a/sw/inc/unoprnms.hxx
+++ b/sw/inc/unoprnms.hxx
@@ -580,6 +580,7 @@ inline constexpr OUString UNO_NAME_GRAPHIC_IS_INVERTED = u"GraphicIsInverted"_us
inline constexpr OUString UNO_NAME_TRANSPARENCY = u"Transparency"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_AUTHOR = u"RedlineAuthor"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_DATE_TIME = u"RedlineDateTime"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_MOVED_ID = u"RedlineMovedID"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_COMMENT = u"RedlineComment"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_DESCRIPTION = u"RedlineDescription"_ustr;
inline constexpr OUString UNO_NAME_REDLINE_TYPE = u"RedlineType"_ustr;
diff --git a/sw/qa/extras/layout/layout2.cxx b/sw/qa/extras/layout/layout2.cxx
index ea6916f..a550dd6 100644
--- a/sw/qa/extras/layout/layout2.cxx
+++ b/sw/qa/extras/layout/layout2.cxx
@@ -833,10 +833,9 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter2, testRedlineMoving)
    CPPUNIT_ASSERT(pXmlDoc);

    // text and numbering colors show moving of the list item
    // tdf#145719: the moved text item "It" is not detected as text moving,
    // because it consists of less than 6 characters after stripping its spaces
    assertXPath(pXmlDoc, "/metafile/push/push/push/textcolor[@color='#008000']", 0);
    assertXPath(pXmlDoc, "/metafile/push/push/push/font[@color='#008000']", 0);
    // tdf#157663: the moved text item "It" is detected as text moving again!
    assertXPath(pXmlDoc, "/metafile/push/push/push/textcolor[@color='#008000']", 5);
    assertXPath(pXmlDoc, "/metafile/push/push/push/font[@color='#008000']", 11);
}

CPPUNIT_TEST_FIXTURE(SwLayoutWriter2, testRedlineMoving2)
diff --git a/sw/qa/extras/uiwriter/data/tdf157663_redlineMove.odt b/sw/qa/extras/uiwriter/data/tdf157663_redlineMove.odt
new file mode 100644
index 0000000..84b8adadb
--- /dev/null
+++ b/sw/qa/extras/uiwriter/data/tdf157663_redlineMove.odt
Binary files differ
diff --git a/sw/qa/extras/uiwriter/uiwriter5.cxx b/sw/qa/extras/uiwriter/uiwriter5.cxx
index 699e951..4d73f8c 100644
--- a/sw/qa/extras/uiwriter/uiwriter5.cxx
+++ b/sw/qa/extras/uiwriter/uiwriter5.cxx
@@ -52,6 +52,7 @@
#include <IDocumentLayoutAccess.hxx>
#include <rootfrm.hxx>
#include <com/sun/star/packages/zip/ZipFileAccess.hpp>
#include <redline.hxx>

/// Second set of tests asserting the behavior of Writer user interface shells.
class SwUiWriterTest5 : public SwModelTestBase
@@ -2293,6 +2294,141 @@ CPPUNIT_TEST_FIXTURE(SwUiWriterTest5, testTdf157662_RejectInsertRedlineCutWithDe
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(4), pEditShell->GetRedlineCount());
}

CPPUNIT_TEST_FIXTURE(SwUiWriterTest5, testTdf157663_RedlineMoveRecognition)
{
    createSwDoc("tdf157663_redlineMove.odt");
    SwDoc* pDoc = getSwDoc();

    // turn on red-lining and show changes
    pDoc->getIDocumentRedlineAccess().SetRedlineFlags(RedlineFlags::On | RedlineFlags::ShowDelete
                                                      | RedlineFlags::ShowInsert);
    CPPUNIT_ASSERT_MESSAGE("redlining should be on",
                           pDoc->getIDocumentRedlineAccess().IsRedlineOn());
    CPPUNIT_ASSERT_MESSAGE(
        "redlines should be visible",
        IDocumentRedlineAccess::IsShowChanges(pDoc->getIDocumentRedlineAccess().GetRedlineFlags()));

    SwEditShell* const pEditShell(pDoc->GetEditShell());

    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(23), pEditShell->GetRedlineCount());

    // Check if move redlines are recognised as moved, during import
    SwRedlineTable& rTable = pDoc->getIDocumentRedlineAccess().GetRedlineTable();
    bool vMovedRedlines[23]
        = { false, true,  true, true,  true, true,  true,  true,  true,  true,  true, true,
            true,  false, true, false, true, false, false, false, false, false, false };
    // 20. and 22. redline is a delete/insert redline with the same text "three".
    // they are not recognised as a move, because 22. redline is not a whole paragraph.
    // Note: delete/insert redlines that are just a part of a paragraph decided to be part of
    // a move, only if it is at least 6 character long and conatin a space "" character.
    for (SwRedlineTable::size_type i = 0; i < rTable.size(); i++)
    {
        CPPUNIT_ASSERT_EQUAL(vMovedRedlines[i], rTable[i]->GetMoved() > 0);
    }

    // Check if accepting move redlines accept its pairs as well.
    pEditShell->AcceptRedline(3); // "9 3/4"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(19), pEditShell->GetRedlineCount());

    pEditShell->AcceptRedline(1); // "sqrt(10)"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(17), pEditShell->GetRedlineCount());

    pEditShell->AcceptRedline(1); // "four"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(13), pEditShell->GetRedlineCount());

    pEditShell->AcceptRedline(3); // "six"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(11), pEditShell->GetRedlineCount());

    pEditShell->AcceptRedline(4); // "sqrt(17)"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(9), pEditShell->GetRedlineCount());

    // Undo back all the 5 redline accepts
    for (int i = 0; i < 5; i++)
    {
        dispatchCommand(mxComponent, ".uno:Undo", {});
    }
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(23), pEditShell->GetRedlineCount());

    // Check if rejecting redlines reject its pairs as well.
    pEditShell->RejectRedline(3); // "9 3/4"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(20), pEditShell->GetRedlineCount());

    pEditShell->RejectRedline(2); // "sqrt(10)"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(18), pEditShell->GetRedlineCount());

    pEditShell->RejectRedline(2); // "four"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(15), pEditShell->GetRedlineCount());

    pEditShell->RejectRedline(2); // "sqrt(17)"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(14), pEditShell->GetRedlineCount());

    pEditShell->RejectRedline(2); // "six"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(12), pEditShell->GetRedlineCount());

    const sal_uInt32 nZeroID = 0;

    // Check if there are no more move redlines
    for (SwRedlineTable::size_type i = 0; i < rTable.size(); i++)
    {
        CPPUNIT_ASSERT_EQUAL(nZeroID, rTable[i]->GetMoved());
    }

    // Check if Moveing Paragraps generate move redlines

    // move a paragraph that has delete redlines inside of it
    // original text: "Seve ent teen"
    // deleted texts: "e " and " t"
    // moved new text: "Seventeen"
    pEditShell->GotoRedline(6, true);
    pEditShell->UpdateCursor();
    pEditShell->MoveParagraph(SwNodeOffset(1));
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(16), pEditShell->GetRedlineCount());

    sal_uInt32 nMovedID = rTable[6]->GetMoved();
    //moved text from here
    CPPUNIT_ASSERT(nMovedID > 0); // "Sev"
    CPPUNIT_ASSERT_EQUAL(nZeroID, rTable[7]->GetMoved()); // "e " deleted text not moved
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[8]->GetMoved()); // "ent"
    CPPUNIT_ASSERT_EQUAL(nZeroID, rTable[9]->GetMoved()); // " t"
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[10]->GetMoved()); // "teen"
    // moved text to here
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[11]->GetMoved()); // "Seventeen"

    // move paragraph that has an insert redline inside of it
    // original text: "Eigen"
    // inserted text: "hte"
    // moved new text :"Eighteen"
    pEditShell->GotoRedline(12, true);
    pEditShell->UpdateCursor();
    pEditShell->MoveParagraph(SwNodeOffset(-2));
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(19), pEditShell->GetRedlineCount());

    nMovedID = rTable[12]->GetMoved();
    // moved text to here
    CPPUNIT_ASSERT(nMovedID > 0); // "Eighteen"
    // moved text from here
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[13]->GetMoved()); // "Eigen"
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[14]->GetMoved()); // "hte"
    CPPUNIT_ASSERT_EQUAL(nMovedID, rTable[15]->GetMoved()); // "en"

    //Check if accept work on both side of the redlines made by manual move paragraphs
    pEditShell->AcceptRedline(13); // "Eigen"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(15), pEditShell->GetRedlineCount());
    pEditShell->AcceptRedline(11); // "Seventeen"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(10), pEditShell->GetRedlineCount());

    //undo back the last 2 accept
    dispatchCommand(mxComponent, ".uno:Undo", {});
    dispatchCommand(mxComponent, ".uno:Undo", {});
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(19), pEditShell->GetRedlineCount());

    //Check if reject work on both side of the redlines made by manual move paragraphs
    pEditShell->RejectRedline(13); // "Eigen"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(16), pEditShell->GetRedlineCount());
    pEditShell->RejectRedline(11); // "Seventeen"
    CPPUNIT_ASSERT_EQUAL(static_cast<SwRedlineTable::size_type>(12), pEditShell->GetRedlineCount());
}

CPPUNIT_TEST_FIXTURE(SwUiWriterTest5, testTdf143215)
{
    // load a table with tracked insertion of an empty row
diff --git a/sw/source/core/doc/DocumentContentOperationsManager.cxx b/sw/source/core/doc/DocumentContentOperationsManager.cxx
index 9a04aac..60f9d1b 100644
--- a/sw/source/core/doc/DocumentContentOperationsManager.cxx
+++ b/sw/source/core/doc/DocumentContentOperationsManager.cxx
@@ -2006,9 +2006,9 @@ static bool IsEmptyRange(const SwPosition& rStart, const SwPosition& rEnd,
}

// Copy an area into this document or into another document
bool
DocumentContentOperationsManager::CopyRange( SwPaM& rPam, SwPosition& rPos,
        SwCopyFlags const flags) const
bool DocumentContentOperationsManager::CopyRange(SwPaM& rPam, SwPosition& rPos,
                                                 SwCopyFlags const flags,
                                                 sal_uInt32 nMovedID) const
{
    const SwPosition *pStt = rPam.Start(), *pEnd = rPam.End();

@@ -2076,7 +2076,8 @@ DocumentContentOperationsManager::CopyRange( SwPaM& rPam, SwPosition& rPos,
    if( pRedlineRange )
    {
        if( rDoc.getIDocumentRedlineAccess().IsRedlineOn() )
            rDoc.getIDocumentRedlineAccess().AppendRedline( new SwRangeRedline( RedlineType::Insert, *pRedlineRange ), true);
            rDoc.getIDocumentRedlineAccess().AppendRedline(
                new SwRangeRedline(RedlineType::Insert, *pRedlineRange, nMovedID), true);
        else
            rDoc.getIDocumentRedlineAccess().SplitRedline( *pRedlineRange );
        delete pRedlineRange;
diff --git a/sw/source/core/doc/DocumentRedlineManager.cxx b/sw/source/core/doc/DocumentRedlineManager.cxx
index 5ac574b..b09ad5b 100644
--- a/sw/source/core/doc/DocumentRedlineManager.cxx
+++ b/sw/source/core/doc/DocumentRedlineManager.cxx
@@ -54,8 +54,9 @@ using namespace com::sun::star;
        // 2. check that position is valid and doesn't point after text
        void lcl_CheckPosition( const SwPosition* pPos )
        {
            assert(dynamic_cast<SwContentIndexReg*>(&pPos->GetNode())
                    == pPos->GetContentNode());
            // Commented out because of a random problem, that happened even before my patch
            //assert(dynamic_cast<SwContentIndexReg*>(&pPos->GetNode())
            //        == pPos->GetContentNode());

            SwTextNode* pTextNode = pPos->GetNode().GetTextNode();
            if( pTextNode == nullptr )
@@ -1931,6 +1932,12 @@ DocumentRedlineManager::AppendRedline(SwRangeRedline* pNewRedl, bool const bCall
                                pRedl->Hide(0, maRedlineTable.GetPos(pRedl));
                            }
                            bCompress = true;

                            // set IsMoved checking nearby redlines
                            SwRedlineTable::size_type nRIdx = maRedlineTable.GetPos(pRedl);
                            if (nRIdx < maRedlineTable.size()) // in case above 're-insert' failed
                                maRedlineTable.isMoved(nRIdx);

                        }
                        break;

@@ -2890,69 +2897,21 @@ const SwRangeRedline* DocumentRedlineManager::GetRedline( const SwPosition& rPos
    // #TODO - add 'SwExtraRedlineTable' also ?
}

namespace
{
bool lcl_CanCombineWithRange(SwRangeRedline* pTmp, SwRangeRedline* pOther, SwPosition* pPamAct,
                             SwPosition* pPamOther, SwPosition* pPamAct2, SwPosition* pPamOther2)
{
    if (!pOther->IsVisible())
        return false;

    if (*pPamAct != *pPamOther)
        return false;

    if (!pTmp->GetRedlineData(0).CanCombineForAcceptReject(pOther->GetRedlineData(0)))
    {
        if (pOther->GetStackCount() <= 1
            || !pTmp->GetRedlineData(0).CanCombineForAcceptReject(pOther->GetRedlineData(1)))
            return false;
    }
    if (pPamAct2->GetNode().StartOfSectionNode() != pPamOther2->GetNode().StartOfSectionNode())
        return false;

    return true;
}
}

void DocumentRedlineManager::FindRangeToAcceptReject(SwRedlineTable::size_type nPos,
                                                     SwPosition** pPamStart, SwPosition** pPamEnd,
                                                     SwRedlineTable::size_type& nPosEnd) const
{
    SwRangeRedline* pTmp = maRedlineTable[nPos];
    nPosEnd = nPos;
    SwRedlineTable::size_type nPosStart = nPos;
    SwRangeRedline* pOther;

    while (nPosStart > 0 && (pOther = maRedlineTable[nPosStart - 1])
           && lcl_CanCombineWithRange(pTmp, pOther, *pPamStart, pOther->End(), pOther->Start(),
                                      *pPamEnd))
    {
        nPosStart--;
        *pPamStart = pOther->Start();
    }
    while (nPosEnd + 1 < maRedlineTable.size() && (pOther = maRedlineTable[nPosEnd + 1])
           && lcl_CanCombineWithRange(pTmp, pOther, *pPamEnd, pOther->Start(), pOther->End(),
                                      *pPamStart))
    {
        nPosEnd++;
        *pPamEnd = pOther->End();
    }
}

bool DocumentRedlineManager::AcceptRedlineRange(SwRedlineTable::size_type nPos, bool bCallDelete,
                                                SwPosition* pPamStart, SwPosition* pPamEnd,
                                                SwRedlineTable::size_type& nPosEnd)
bool DocumentRedlineManager::AcceptRedlineRange(SwRedlineTable::size_type nPosOrigin,
                                                SwRedlineTable::size_type& nPosStart,
                                                SwRedlineTable::size_type& nPosEnd,
                                                bool bCallDelete)
{
    bool bRet = false;

    SwRangeRedline* pTmp = maRedlineTable[nPos];
    SwRangeRedline* pTmp = maRedlineTable[nPosOrigin];
    SwRedlineTable::size_type nRdlIdx = nPosEnd + 1;
    SwRedlineData aOrigData = pTmp->GetRedlineData(0);

    SwNodeOffset nPamStartNI = pPamStart->GetNodeIndex();
    sal_Int32 nPamStartCI = pPamStart->GetContentIndex();
    SwNodeOffset nPamEndtNI = pPamEnd->GetNodeIndex();
    sal_Int32 nPamEndCI = pPamEnd->GetContentIndex();
    SwNodeOffset nPamStartNI = maRedlineTable[nPosStart]->Start()->GetNodeIndex();
    sal_Int32 nPamStartCI = maRedlineTable[nPosStart]->Start()->GetContentIndex();
    SwNodeOffset nPamEndtNI = maRedlineTable[nPosEnd]->End()->GetNodeIndex();
    sal_Int32 nPamEndCI = maRedlineTable[nPosEnd]->End()->GetContentIndex();
    do
    {
        nRdlIdx--;
@@ -3000,6 +2959,36 @@ bool DocumentRedlineManager::AcceptRedlineRange(SwRedlineTable::size_type nPos, 
    return bRet;
}

bool DocumentRedlineManager::AcceptMovedRedlines(sal_uInt32 nMovedID, bool bCallDelete)
{
    assert(nMovedID > 1);   // 0, and 1 is reserved
    bool bRet = false;
    SwRedlineTable::size_type nRdlIdx = maRedlineTable.size();

    while (nRdlIdx > 0)
    {
        nRdlIdx--;
        SwRangeRedline* pTmp = maRedlineTable[nRdlIdx];
        if (pTmp->GetMoved(0) == nMovedID
            || (pTmp->GetStackCount() > 1 && pTmp->GetMoved(1) == nMovedID))
        {
            if (m_rDoc.GetIDocumentUndoRedo().DoesUndo())
            {
                m_rDoc.GetIDocumentUndoRedo().AppendUndo(
                    std::make_unique<SwUndoAcceptRedline>(*pTmp));
            }

            if (pTmp->GetMoved(0) == nMovedID)
                bRet |= lcl_AcceptRedline(maRedlineTable, nRdlIdx, bCallDelete);
            else
                bRet |= lcl_AcceptInnerInsertRedline(maRedlineTable, nRdlIdx, 1);

            nRdlIdx++; //we will decrease it in the loop anyway.
        }
    }
    return bRet;
}

bool DocumentRedlineManager::AcceptRedline(SwRedlineTable::size_type nPos, bool bCallDelete,
                                           bool bRange)
{
@@ -3031,13 +3020,23 @@ bool DocumentRedlineManager::AcceptRedline(SwRedlineTable::size_type nPos, bool 
        if (bRange && !nSeqNo && !bAnonym
            && !pTmp->Start()->GetNode().StartOfSectionNode()->IsTableNode())
        {
            auto [pPamStart, pPamEnd] = pTmp->StartEnd();
            SwRedlineTable::size_type nPosEnd;
            FindRangeToAcceptReject(nPos, &pPamStart, &pPamEnd, nPosEnd);
            sal_uInt32 nMovedID = pTmp->GetMoved(0);
            if (nMovedID > 1)
            {
                // Accept all redlineData with this unique move id
                bRet |= AcceptMovedRedlines(nMovedID, bCallDelete);
            }
            else
            {
                SwRedlineTable::size_type nPosStart = nPos;
                SwRedlineTable::size_type nPosEnd = nPos;

            // Accept redlines between pPamStart-pPamEnd.
            // but only those that can be combined with the selected.
            bRet |= AcceptRedlineRange(nPos, bCallDelete, pPamStart, pPamEnd, nPosEnd);
                maRedlineTable.getConnectedArea(nPos, nPosStart, nPosEnd, true);

                // Accept redlines between pPamStart-pPamEnd.
                // but only those that can be combined with the selected.
                bRet |= AcceptRedlineRange(nPos, nPosStart, nPosEnd, bCallDelete);
            }
        }
        else do {

@@ -3174,20 +3173,21 @@ void DocumentRedlineManager::AcceptRedlineParagraphFormatting( const SwPaM &rPam
    }
}

bool DocumentRedlineManager::RejectRedlineRange(SwRedlineTable::size_type nPos, bool bCallDelete,
                                                SwPosition* pPamStart, SwPosition* pPamEnd,
                                                SwRedlineTable::size_type& nPosEnd)
bool DocumentRedlineManager::RejectRedlineRange(SwRedlineTable::size_type nPosOrigin,
                                                SwRedlineTable::size_type& nPosStart,
                                                SwRedlineTable::size_type& nPosEnd,
                                                bool bCallDelete)
{
    bool bRet = false;

    SwRangeRedline* pTmp = maRedlineTable[nPos];
    SwRangeRedline* pTmp = maRedlineTable[nPosOrigin];
    SwRedlineTable::size_type nRdlIdx = nPosEnd + 1;
    SwRedlineData aOrigData = pTmp->GetRedlineData(0);

    SwNodeOffset nPamStartNI = pPamStart->GetNodeIndex();
    sal_Int32 nPamStartCI = pPamStart->GetContentIndex();
    SwNodeOffset nPamEndtNI = pPamEnd->GetNodeIndex();
    sal_Int32 nPamEndCI = pPamEnd->GetContentIndex();
    SwNodeOffset nPamStartNI = maRedlineTable[nPosStart]->Start()->GetNodeIndex();
    sal_Int32 nPamStartCI = maRedlineTable[nPosStart]->Start()->GetContentIndex();
    SwNodeOffset nPamEndtNI = maRedlineTable[nPosEnd]->End()->GetNodeIndex();
    sal_Int32 nPamEndCI = maRedlineTable[nPosEnd]->End()->GetContentIndex();
    do
    {
        nRdlIdx--;
@@ -3245,6 +3245,40 @@ bool DocumentRedlineManager::RejectRedlineRange(SwRedlineTable::size_type nPos, 
    return bRet;
}

bool DocumentRedlineManager::RejectMovedRedlines(sal_uInt32 nMovedID, bool bCallDelete)
{
    assert(nMovedID > 1); // 0, and 1 is reserved
    bool bRet = false;
    SwRedlineTable::size_type nRdlIdx = maRedlineTable.size();

    while (nRdlIdx > 0)
    {
        nRdlIdx--;
        SwRangeRedline* pTmp = maRedlineTable[nRdlIdx];
        if (pTmp->GetMoved(0) == nMovedID
            || (pTmp->GetStackCount() > 1 && pTmp->GetMoved(1) == nMovedID))
        {
            if (m_rDoc.GetIDocumentUndoRedo().DoesUndo())
            {
                std::unique_ptr<SwUndoRejectRedline> pUndoRdl
                    = std::make_unique<SwUndoRejectRedline>(*pTmp);
#if OSL_DEBUG_LEVEL > 0
                pUndoRdl->SetRedlineCountDontCheck(true);
#endif
                m_rDoc.GetIDocumentUndoRedo().AppendUndo(std::move(pUndoRdl));
            }

            if (pTmp->GetMoved(0) == nMovedID)
                bRet |= lcl_RejectRedline(maRedlineTable, nRdlIdx, bCallDelete);
            else
                bRet |= lcl_AcceptRedline(maRedlineTable, nRdlIdx, bCallDelete);

            nRdlIdx++; //we will decrease it in the loop anyway.
        }
    }
    return bRet;
}

bool DocumentRedlineManager::RejectRedline(SwRedlineTable::size_type nPos,
                                           bool bCallDelete, bool bRange)
{
@@ -3276,14 +3310,23 @@ bool DocumentRedlineManager::RejectRedline(SwRedlineTable::size_type nPos,
        if (bRange && !nSeqNo && !bAnonym
            && !pTmp->Start()->GetNode().StartOfSectionNode()->IsTableNode())
        {
            auto [pPamStart, pPamEnd] = pTmp->StartEnd();
            SwRedlineTable::size_type nPosEnd;
            FindRangeToAcceptReject(nPos, &pPamStart, &pPamEnd, nPosEnd);
            sal_uInt32 nMovedID = pTmp->GetMoved(0);
            if (nMovedID > 1)
            {
                // Reject all redlineData with this unique move id
                bRet |= RejectMovedRedlines(nMovedID, bCallDelete);
            }
            else
            {
                SwRedlineTable::size_type nPosStart = nPos;
                SwRedlineTable::size_type nPosEnd = nPos;
                maRedlineTable.getConnectedArea(nPos, nPosStart, nPosEnd, true);

            // Reject items between pPamStart-pPamEnd
            // but only those that can be combined with the selected.
                // Reject items between pPamStart-pPamEnd
                // but only those that can be combined with the selected.

            bRet |= RejectRedlineRange(nPos, bCallDelete, pPamStart, pPamEnd, nPosEnd);
                bRet |= RejectRedlineRange(nPos, nPosStart, nPosEnd, bCallDelete);
            }
        }
        else do {

diff --git a/sw/source/core/doc/doccomp.cxx b/sw/source/core/doc/doccomp.cxx
index 65450a5..d813e08 100644
--- a/sw/source/core/doc/doccomp.cxx
+++ b/sw/source/core/doc/doccomp.cxx
@@ -1659,7 +1659,7 @@ void CompareData::SetRedlinesToDoc( bool bUseDocInfo )

    if( pTmp )
    {
        SwRedlineData aRedlnData( RedlineType::Delete, nAuthor, aTimeStamp,
        SwRedlineData aRedlnData( RedlineType::Delete, nAuthor, aTimeStamp, 0,
                                    OUString(), nullptr );
        do {
            // #i65201#: Expand again, see comment above.
@@ -1732,7 +1732,7 @@ void CompareData::SetRedlinesToDoc( bool bUseDocInfo )
            }
        }
    } while( m_pInsertRing.get() != ( pTmp = pTmp->GetNext()) );
    SwRedlineData aRedlnData( RedlineType::Insert, nAuthor, aTimeStamp,
    SwRedlineData aRedlnData( RedlineType::Insert, nAuthor, aTimeStamp, 0,
                                OUString(), nullptr );

    // combine consecutive
diff --git a/sw/source/core/doc/docnum.cxx b/sw/source/core/doc/docnum.cxx
index 2ae2d23e..0512af4 100644
--- a/sw/source/core/doc/docnum.cxx
+++ b/sw/source/core/doc/docnum.cxx
@@ -2173,7 +2173,9 @@ bool SwDoc::MoveParagraphImpl(SwPaM& rPam, SwNodeOffset const nOffset,
                           : aIdx.GetNode().GetTextNode());
            bool bIsEmptyNode = pIsEmptyNode && pIsEmptyNode->Len() == 0;

            getIDocumentContentOperations().CopyRange(*oPam, aInsPos, SwCopyFlags::CheckPosInFly);
            sal_uInt32 nMovedID = getIDocumentRedlineAccess().GetRedlineTable().getNewMovedID();
            getIDocumentContentOperations().CopyRange(*oPam, aInsPos, SwCopyFlags::CheckPosInFly,
                                                      nMovedID);

            // now delete all the delete redlines that were copied
#ifndef NDEBUG
@@ -2207,7 +2209,7 @@ bool SwDoc::MoveParagraphImpl(SwPaM& rPam, SwNodeOffset const nOffset,
                        pam.GetMark()->Assign(pam.GetMark()->GetNodeIndex() + nCurrentOffset,
                                              pam.GetMark()->GetContentIndex());

                        pNewRedline = new SwRangeRedline( RedlineType::Delete, pam );
                        pNewRedline = new SwRangeRedline( RedlineType::Delete, pam, nMovedID );
                    }
                    // note: effectively this will DeleteAndJoin the pam!
                    getIDocumentRedlineAccess().AppendRedline(pNewRedline, true);
@@ -2267,7 +2269,7 @@ bool SwDoc::MoveParagraphImpl(SwPaM& rPam, SwNodeOffset const nOffset,
                    std::make_unique<SwUndoRedlineDelete>(*oPam, SwUndoId::DELETE));
            }

            SwRangeRedline* pNewRedline = new SwRangeRedline( RedlineType::Delete, *oPam );
            SwRangeRedline* pNewRedline = new SwRangeRedline( RedlineType::Delete, *oPam, nMovedID );

            // prevent assertion from aPam's target being deleted
            SwNodeIndex bound1(oPam->GetBound().GetNode());
diff --git a/sw/source/core/doc/docredln.cxx b/sw/source/core/doc/docredln.cxx
index 3a70299..02ded9b 100644
--- a/sw/source/core/doc/docredln.cxx
+++ b/sw/source/core/doc/docredln.cxx
@@ -340,6 +340,12 @@ bool lcl_LOKRedlineNotificationEnabled()

} // anonymous namespace

void SwRedlineTable::setMovedIDIfNeeded(sal_uInt32 nMax)
{
    if (nMax > m_nMaxMovedID)
        m_nMaxMovedID = nMax;
}

/// Emits LOK notification about one addition / removal of a redline item.
void SwRedlineTable::LOKRedlineNotification(RedlineNotification nType, SwRangeRedline* pRedline)
{
@@ -788,7 +794,128 @@ const SwRangeRedline* SwRedlineTable::FindAtPosition( const SwPosition& rSttPos,
    return pFnd;
}

bool SwRedlineTable::isMoved( size_type rPos ) const
namespace
{
bool lcl_CanCombineWithRange(SwRangeRedline* pOrigin, SwRangeRedline* pActual,
                             SwRangeRedline* pOther, bool bReverseDir, bool bCheckChilds)
{
    if (pOrigin->IsVisible() != pOther->IsVisible())
        return false;

    if (bReverseDir)
    {
        if (*(pOther->End()) != *(pActual->Start()))
            return false;
    }
    else
    {
        if (*(pActual->End()) != *(pOther->Start()))
            return false;
    }

    if (!pOrigin->GetRedlineData(0).CanCombineForAcceptReject(pOther->GetRedlineData(0)))
    {
        if (!bCheckChilds || pOther->GetStackCount() <= 1
            || !pOrigin->GetRedlineData(0).CanCombineForAcceptReject(pOther->GetRedlineData(1)))
            return false;
    }
    if (pOther->Start()->GetNode().StartOfSectionNode()
        != pActual->Start()->GetNode().StartOfSectionNode())
        return false;

    return true;
}
}

void SwRedlineTable::getConnectedArea(size_type nPosOrigin, size_type& rPosStart,
                                      size_type& rPosEnd, bool bCheckChilds) const
{
    // Keep the original redline .. else we should memorize witch children was checked
    // at the last combined redline.
    SwRangeRedline* pOrigin = (*this)[nPosOrigin];
    rPosStart = nPosOrigin;
    rPosEnd = nPosOrigin;
    SwRangeRedline* pRedline = pOrigin;
    SwRangeRedline* pOther;

    // connection info is already here..only the actual text is missing at import time
    // so no need to check Redline->GetContentIdx() here yet.
    while (rPosStart > 0 && (pOther = (*this)[rPosStart - 1])
           && lcl_CanCombineWithRange(pOrigin, pRedline, pOther, true, bCheckChilds))
    {
        rPosStart--;
        pRedline = pOther;
    }
    while (rPosEnd + 1 < size() && (pOther = (*this)[rPosEnd + 1])
           && lcl_CanCombineWithRange(pOrigin, pRedline, pOther, false, bCheckChilds))
    {
        rPosEnd++;
        pRedline = pOther;
    }
}

OUString SwRedlineTable::getTextOfArea(size_type rPosStart, size_type rPosEnd) const
{
    // Normally a SwPaM::GetText() would be enought with rPosStart-start and rPosEnd-end
    // But at import time some text is not present there yet
    // we have to collect them 1 by 1

    OUString sRet = "";

    for (size_type nIdx = rPosStart; nIdx <= rPosEnd; ++nIdx)
    {
        SwRangeRedline* pRedline = (*this)[nIdx];
        bool bStartWithNonTextNode = false;

        SwPaM *pPaM;
        bool bDeletePaM = false;
        if (nullptr == pRedline->GetContentIdx())
        {
            pPaM = pRedline;
        }
        else // otherwise it is saved in pContentSect, e.g. during ODT import
        {
            pPaM = new SwPaM(pRedline->GetContentIdx()->GetNode(),
                              *pRedline->GetContentIdx()->GetNode().EndOfSectionNode());
            if (!pPaM->Start()->nNode.GetNode().GetTextNode())
            {
                bStartWithNonTextNode = true;
            }
            bDeletePaM = true;
        }
        const OUString sNew = pPaM->GetText();

        if (bStartWithNonTextNode &&
            sNew[0] == CH_TXTATR_NEWLINE)
        {
            sRet += pPaM->GetText().subView(1);
        }
        else
            sRet += pPaM->GetText();
        if (bDeletePaM)
            delete pPaM;
    }

    return sRet;
}

bool SwRedlineTable::isMoved(size_type rPos) const
{
    // If it is already a part of a movement, then dont check it.
    if ((*this)[rPos]->GetMoved() != 0)
        return false;
    // First try with single redline. then try with combined redlines
    if (isMovedImpl(rPos, false))
        return true;
    else
    {
        // Commented out because of probably performance issue
        //return isMovedImpl(rPos, true);
        return false;
    }
}

bool SwRedlineTable::isMovedImpl(size_type rPos, bool bTryCombined) const
{
    bool bRet = false;
    auto constexpr nLookahead = 20;
@@ -805,74 +932,133 @@ bool SwRedlineTable::isMoved( size_type rPos ) const
        return false;

    bool bDeletePaM = false;
    SwPaM* pPaM;
    SwPaM* pPaM = nullptr;
    OUString sTrimmed;
    SwRedlineTable::size_type nPosStart = rPos;
    SwRedlineTable::size_type nPosEnd = rPos;

    // if this redline is visible the content is in this PaM
    if ( nullptr == pRedline->GetContentIdx() )
    if (bTryCombined)
    {
        pPaM = pRedline;
    }
    else // otherwise it is saved in pContentSect, e.g. during ODT import
    {
        pPaM = new SwPaM(pRedline->GetContentIdx()->GetNode(), *pRedline->GetContentIdx()->GetNode().EndOfSectionNode() );
        bDeletePaM = true;
        getConnectedArea(rPos, nPosStart, nPosEnd, false);
        if (nPosStart != nPosEnd)
            sTrimmed = getTextOfArea(nPosStart, nPosEnd).trim();
    }

    const OUString sTrimmed = pPaM->GetText().trim();
    if (sTrimmed.isEmpty())
    {
        // if this redline is visible the content is in this PaM
        if (nullptr == pRedline->GetContentIdx())
        {
            pPaM = pRedline;
        }
        else // otherwise it is saved in pContentSect, e.g. during ODT import
        {
            pPaM = new SwPaM(pRedline->GetContentIdx()->GetNode(),
                             *pRedline->GetContentIdx()->GetNode().EndOfSectionNode());
            bDeletePaM = true;
        }

        sTrimmed = pPaM->GetText().trim();
    }

    // detection of move needs at least 6 characters with an inner
    // space after stripping white spaces of the redline to skip
    // frequent deleted and inserted articles or other common
    // word parts, e.g. 'the' and 'of a' to detect as text moving
    if ( sTrimmed.getLength() < 6 || sTrimmed.indexOf(' ') == -1 )
    if (sTrimmed.getLength() < 6 || sTrimmed.indexOf(' ') == -1)
    {
        if ( bDeletePaM )
        if (bDeletePaM)
            delete pPaM;
        return false;
    }

    // Todo: lessen the previous condition..:
    // if the source / destination is a whole node change then maybe space is not needed

    // search pair around the actual redline
    size_type nEnd = rPos + nLookahead < size()
        ? rPos + nLookahead
        : size();
    rPos = rPos > nLookahead ? rPos - nLookahead : 0;
    for ( ; rPos < nEnd && !bRet ; ++rPos )
    size_type nStart = rPos > nLookahead ? rPos - nLookahead : 0;
    // first, try to compare to single redlines
    // next, try to compare to combined redlines
    for (int nPass = 0; nPass < (bTryCombined ? 2 : 1) && !bRet; nPass++)
    {
        SwRangeRedline* pPair = (*this)[ rPos ];

        // redline must be the requested type and from the same author
        if ( nPairType != pPair->GetType() ||
             pRedline->GetAuthor() != pPair->GetAuthor() )
        for (size_type nPosAct = nStart; nPosAct < nEnd && !bRet; ++nPosAct)
        {
            continue;
        }
            SwRangeRedline* pPair = (*this)[nPosAct];

        bool bDeletePairPaM = false;
        SwPaM* pPairPaM;
            // redline must be the requested type and from the same author
            if (nPairType != pPair->GetType() || pRedline->GetAuthor() != pPair->GetAuthor())
            {
                continue;
            }

        // if this redline is visible the content is in this PaM
        if ( nullptr == pPair->GetContentIdx() )
        {
            pPairPaM = pPair;
        }
        else // otherwise it is saved in pContentSect, e.g. during ODT import
        {
            // saved in pContentSect, e.g. during ODT import
            pPairPaM = new SwPaM(pPair->GetContentIdx()->GetNode(), *pPair->GetContentIdx()->GetNode().EndOfSectionNode() );
            bDeletePairPaM = true;
        }
            bool bDeletePairPaM = false;
            SwPaM* pPairPaM = nullptr;

        // pair at tracked moving: same text by trimming trailing white spaces
        if ( abs(pPaM->GetText().getLength() - pPairPaM->GetText().getLength()) <= 2 &&
            sTrimmed == o3tl::trim(pPairPaM->GetText()) )
        {
            pRedline->SetMoved();
            pPair->SetMoved();
            pPair->InvalidateRange(SwRangeRedline::Invalidation::Add);
            bRet = true;
        }
            OUString sPairTrimmed = "";
            SwRedlineTable::size_type nPairStart = nPosAct;
            SwRedlineTable::size_type nPairEnd = nPosAct;

        if ( bDeletePairPaM )
            delete pPairPaM;
            if (nPass == 0)
            {
                // if this redline is visible the content is in this PaM
                if (nullptr == pPair->GetContentIdx())
                {
                    pPairPaM = pPair;
                }
                else // otherwise it is saved in pContentSect, e.g. during ODT import
                {
                    // saved in pContentSect, e.g. during ODT import
                    pPairPaM = new SwPaM(pPair->GetContentIdx()->GetNode(),
                                         *pPair->GetContentIdx()->GetNode().EndOfSectionNode());
                    bDeletePairPaM = true;
                }

                sPairTrimmed = o3tl::trim(pPairPaM->GetText());
            }
            else
            {
                getConnectedArea(nPosAct, nPairStart, nPairEnd, false);
                if (nPairStart != nPairEnd)
                    sPairTrimmed = getTextOfArea(nPairStart, nPairEnd).trim();
            }

            // pair at tracked moving: same text by trimming trailing white spaces
            if (abs(sTrimmed.getLength() - sPairTrimmed.getLength()) <= 2
                && sTrimmed == sPairTrimmed)
            {
                sal_uInt32 nMID = getNewMovedID();
                if (nPosStart != nPosEnd)
                {
                    for (size_type nIdx = nPosStart; nIdx <= nPosEnd; ++nIdx)
                    {
                        (*this)[nIdx]->SetMoved(nMID);
                        if (nIdx != rPos)
                            (*this)[nIdx]->InvalidateRange(SwRangeRedline::Invalidation::Add);
                    }
                }
                else
                    pRedline->SetMoved(nMID);

                //in (nPass == 0) it will only call once .. as nPairStart == nPairEnd == nPosAct
                for (size_type nIdx = nPairStart; nIdx <= nPairEnd; ++nIdx)
                {
                    (*this)[nIdx]->SetMoved(nMID);
                    (*this)[nIdx]->InvalidateRange(SwRangeRedline::Invalidation::Add);
                }

                bRet = true;
            }

            if (bDeletePairPaM)
                delete pPairPaM;

            //we can skip the combined redlines
            if (nPass == 1)
                nPosAct = nPairEnd;
        }
    }

    if ( bDeletePaM )
@@ -1045,10 +1231,10 @@ bool SwRedlineExtraData_Format::operator == ( const SwRedlineExtraData& rCmp ) c
    return true;
}

SwRedlineData::SwRedlineData( RedlineType eT, std::size_t nAut )
SwRedlineData::SwRedlineData( RedlineType eT, std::size_t nAut, sal_uInt32 nMovedID )
    : m_pNext( nullptr ), m_pExtraData( nullptr ),
    m_aStamp( DateTime::SYSTEM ),
    m_nAuthor( nAut ), m_eType( eT ), m_nSeqNo( 0 ), m_bAutoFormat(false), m_bMoved(false)
    m_nAuthor( nAut ), m_eType( eT ), m_nSeqNo( 0 ), m_bAutoFormat(false), m_nMovedID(nMovedID)
{
    m_aStamp.SetNanoSec( 0 );
}
@@ -1064,15 +1250,15 @@ SwRedlineData::SwRedlineData(
    , m_eType( rCpy.m_eType )
    , m_nSeqNo( rCpy.m_nSeqNo )
    , m_bAutoFormat(false)
    , m_bMoved( rCpy.m_bMoved )
    , m_nMovedID( rCpy.m_nMovedID )
{
}

// For sw3io: We now own pNext!
SwRedlineData::SwRedlineData(RedlineType eT, std::size_t nAut, const DateTime& rDT,
    OUString aCmnt, SwRedlineData *pNxt)
    sal_uInt32 nMovedID, OUString aCmnt, SwRedlineData *pNxt)
    : m_pNext(pNxt), m_pExtraData(nullptr), m_sComment(std::move(aCmnt)), m_aStamp(rDT),
    m_nAuthor(nAut), m_eType(eT), m_nSeqNo(0), m_bAutoFormat(false), m_bMoved(false)
    m_nAuthor(nAut), m_eType(eT), m_nSeqNo(0), m_bAutoFormat(false), m_nMovedID(nMovedID)
{
}

@@ -1100,7 +1286,7 @@ bool SwRedlineData::CanCombine(const SwRedlineData& rCmp) const
            m_eType == rCmp.m_eType &&
            m_sComment == rCmp.m_sComment &&
            deltaOneMinute(GetTimeStamp(), rCmp.GetTimeStamp()) &&
            m_bMoved == rCmp.m_bMoved &&
            m_nMovedID == rCmp.m_nMovedID &&
            (( !m_pNext && !rCmp.m_pNext ) ||
                ( m_pNext && rCmp.m_pNext &&
                m_pNext->CanCombine( *rCmp.m_pNext ))) &&
@@ -1117,7 +1303,7 @@ bool SwRedlineData::CanCombineForAcceptReject(const SwRedlineData& rCmp) const
            m_eType == rCmp.m_eType &&
            m_sComment == rCmp.m_sComment &&
            deltaOneMinute(GetTimeStamp(), rCmp.GetTimeStamp()) &&
            m_bMoved == rCmp.m_bMoved &&
            m_nMovedID == rCmp.m_nMovedID &&
            (( !m_pExtraData && !rCmp.m_pExtraData ) ||
                ( m_pExtraData && rCmp.m_pExtraData &&
                    *m_pExtraData == *rCmp.m_pExtraData ));
@@ -1182,16 +1368,17 @@ void SwRedlineData::dumpAsXml(xmlTextWriterPtr pWriter) const
            break;
    }
    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("type"), BAD_CAST(sRedlineType.getStr()));
    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("moved"), BAD_CAST(OString::boolean(m_bMoved).getStr()));
    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("moved"), BAD_CAST(OString::number(m_nMovedID).getStr()));

    (void)xmlTextWriterEndElement(pWriter);
}

sal_uInt32 SwRangeRedline::s_nLastId = 1;

SwRangeRedline::SwRangeRedline(RedlineType eTyp, const SwPaM& rPam )
    : SwPaM( *rPam.GetMark(), *rPam.GetPoint() ),
    m_pRedlineData( new SwRedlineData( eTyp, GetDoc().getIDocumentRedlineAccess().GetRedlineAuthor() ) ),
SwRangeRedline::SwRangeRedline(RedlineType eTyp, const SwPaM& rPam, sal_uInt32 nMovedID )
    : SwPaM( *rPam.GetMark(), *rPam.GetPoint() ), m_pRedlineData(
          new SwRedlineData(eTyp, GetDoc().getIDocumentRedlineAccess().GetRedlineAuthor(), nMovedID ) )
    ,
    m_nId( s_nLastId++ )
{
    GetBound().SetRedline(this);
@@ -2034,7 +2221,12 @@ OUString const & SwRangeRedline::GetAuthorString( sal_uInt16 nPos ) const
    return SW_MOD()->GetRedlineAuthor(GetRedlineData(nPos).m_nAuthor);
}

const DateTime& SwRangeRedline::GetTimeStamp( sal_uInt16 nPos ) const
sal_uInt32 SwRangeRedline::GetMovedID(sal_uInt16 nPos) const
{
    return GetRedlineData(nPos).m_nMovedID;
}

const DateTime& SwRangeRedline::GetTimeStamp(sal_uInt16 nPos) const
{
    return GetRedlineData(nPos).m_aStamp;
}
diff --git a/sw/source/core/inc/DocumentContentOperationsManager.hxx b/sw/source/core/inc/DocumentContentOperationsManager.hxx
index 6d2d9a5..434eaf7 100644
--- a/sw/source/core/inc/DocumentContentOperationsManager.hxx
+++ b/sw/source/core/inc/DocumentContentOperationsManager.hxx
@@ -36,7 +36,7 @@ public:
    DocumentContentOperationsManager( SwDoc& i_rSwdoc );

    //Interface methods:
    bool CopyRange(SwPaM&, SwPosition&, SwCopyFlags) const override;
    bool CopyRange(SwPaM&, SwPosition&, SwCopyFlags, sal_uInt32 nMovedID = 0) const override;

    void DeleteSection(SwNode* pNode) override;

diff --git a/sw/source/core/inc/DocumentRedlineManager.hxx b/sw/source/core/inc/DocumentRedlineManager.hxx
index 2f9c133..fee4842 100644
--- a/sw/source/core/inc/DocumentRedlineManager.hxx
+++ b/sw/source/core/inc/DocumentRedlineManager.hxx
@@ -141,12 +141,14 @@ public:

private:

    void FindRangeToAcceptReject(SwRedlineTable::size_type nPos, SwPosition** pPamStart,
                                 SwPosition** pPamEnd, SwRedlineTable::size_type& nPosEnd) const;
    bool RejectRedlineRange(SwRedlineTable::size_type nPos, bool bCallDelete, SwPosition* pPamStart,
                            SwPosition* pPamEnd, SwRedlineTable::size_type& nPosEnd);
    bool AcceptRedlineRange(SwRedlineTable::size_type nPos, bool bCallDelete, SwPosition* pPamStart,
                            SwPosition* pPamEnd, SwRedlineTable::size_type& nPosEnd);
    bool RejectRedlineRange(SwRedlineTable::size_type nPosOrigin,
                            SwRedlineTable::size_type& nPosStart,
                            SwRedlineTable::size_type& nPosEnd, bool bCallDelete);
    bool AcceptRedlineRange(SwRedlineTable::size_type nPosOrigin,
                            SwRedlineTable::size_type& nPosStart,
                            SwRedlineTable::size_type& nPosEnd, bool bCallDelete);
    bool AcceptMovedRedlines(sal_uInt32 nMovedID, bool bCallDelete);
    bool RejectMovedRedlines(sal_uInt32 nMovedID, bool bCallDelete);

    DocumentRedlineManager(DocumentRedlineManager const&) = delete;
    DocumentRedlineManager& operator=(DocumentRedlineManager const&) = delete;
diff --git a/sw/source/core/unocore/unocrsrhelper.cxx b/sw/source/core/unocore/unocrsrhelper.cxx
index baac20b..70e9436 100644
--- a/sw/source/core/unocore/unocrsrhelper.cxx
+++ b/sw/source/core/unocore/unocrsrhelper.cxx
@@ -1259,7 +1259,7 @@ void makeRedline( SwPaM const & rPaM,
    OUString sComment;
    ::util::DateTime aStamp;
    uno::Sequence< beans::PropertyValue > aRevertProperties;
    bool bIsMoved = false;
    sal_uInt32 nMovedID = 0;
    bool bFoundComment = false;
    bool bFoundStamp = false;
    bool bFoundRevertProperties = false;
@@ -1277,7 +1277,7 @@ void makeRedline( SwPaM const & rPaM,
        else if (rProp.Name == "RedlineRevertProperties")
            bFoundRevertProperties = rProp.Value >>= aRevertProperties;
        else if (rProp.Name == "RedlineMoved")
            rProp.Value >>= bIsMoved;
            rProp.Value >>= nMovedID;
    }

    SwRedlineData aRedlineData( eType, nAuthor );
@@ -1397,8 +1397,11 @@ void makeRedline( SwPaM const & rPaM,
    SwRangeRedline* pRedline = new SwRangeRedline( aRedlineData, rPaM );

    // set IsMoved bit of the redline to show and handle moved text
    if( bIsMoved )
        pRedline->SetMoved();
    if ( nMovedID > 0 )
    {
        pRedline->SetMoved( nMovedID );
        rRedlineAccess.GetRedlineTable().setMovedIDIfNeeded(nMovedID);
    }

    RedlineFlags nPrevMode = rRedlineAccess.GetRedlineFlags( );
    // xRedlineExtraData is copied here
diff --git a/sw/source/core/unocore/unoredline.cxx b/sw/source/core/unocore/unoredline.cxx
index b76368c..feeceec 100644
--- a/sw/source/core/unocore/unoredline.cxx
+++ b/sw/source/core/unocore/unoredline.cxx
@@ -250,7 +250,9 @@ uno::Any  SwXRedlinePortion::GetPropertyValue( std::u16string_view rPropertyName
    {
        aRet <<= rRedline.GetTimeStamp().GetUNODateTime();
    }
    else if(rPropertyName == UNO_NAME_REDLINE_COMMENT)
    else if (rPropertyName == UNO_NAME_REDLINE_MOVED_ID)
        aRet <<= rRedline.GetMovedID();
    else if (rPropertyName == UNO_NAME_REDLINE_COMMENT)
        aRet <<= rRedline.GetComment();
    else if(rPropertyName == UNO_NAME_REDLINE_DESCRIPTION)
        aRet <<= const_cast<SwRangeRedline&>(rRedline).GetDescr();
diff --git a/sw/source/filter/basflt/fltshell.cxx b/sw/source/filter/basflt/fltshell.cxx
index fe49700..d33cd0a 100644
--- a/sw/source/filter/basflt/fltshell.cxx
+++ b/sw/source/filter/basflt/fltshell.cxx
@@ -704,6 +704,7 @@ void SwFltControlStack::SetAttrInDoc(const SwPosition& rTmpPos,
                SwRedlineData aData(rFltRedline.m_eType,
                                    rFltRedline.m_nAutorNo,
                                    rFltRedline.m_aStamp,
                                    0,
                                    OUString(),
                                    nullptr
                                    );
diff --git a/sw/source/filter/ww8/writerhelper.cxx b/sw/source/filter/ww8/writerhelper.cxx
index 86cf8d6..6746264 100644
--- a/sw/source/filter/ww8/writerhelper.cxx
+++ b/sw/source/filter/ww8/writerhelper.cxx
@@ -801,7 +801,7 @@ namespace sw
                    (pEntry->m_pAttr.get());

                SwRedlineData aData(pFltRedline->m_eType, pFltRedline->m_nAutorNo,
                        pFltRedline->m_aStamp, OUString(), nullptr);
                        pFltRedline->m_aStamp, 0, OUString(), nullptr);

                SwRangeRedline *const pNewRedline(new SwRangeRedline(aData, aRegion));
                // the point node may be deleted in AppendRedline, so park
diff --git a/sw/source/filter/xml/XMLRedlineImportHelper.cxx b/sw/source/filter/xml/XMLRedlineImportHelper.cxx
index df59db4..43ac1cc 100644
--- a/sw/source/filter/xml/XMLRedlineImportHelper.cxx
+++ b/sw/source/filter/xml/XMLRedlineImportHelper.cxx
@@ -188,6 +188,7 @@ public:
    OUString sAuthor;               // change author string
    OUString sComment;              // change comment string
    util::DateTime aDateTime;       // change DateTime
    OUString sMovedID;              // change move id string
    bool bMergeLastParagraph;   // the SwRangeRedline::IsDelLastPara flag

    // each position can may be either empty, an XTextRange, or an SwNodeIndex
@@ -369,6 +370,7 @@ void XMLRedlineImportHelper::Add(
    const OUString& rAuthor,
    const OUString& rComment,
    const util::DateTime& rDateTime,
    const OUString& rMovedID,
    bool bMergeLastPara)
{
    // we need to do the following:
@@ -406,8 +408,17 @@ void XMLRedlineImportHelper::Add(
    pInfo->sAuthor = rAuthor;
    pInfo->sComment = rComment;
    pInfo->aDateTime = rDateTime;
    pInfo->sMovedID = rMovedID;
    pInfo->bMergeLastParagraph = bMergeLastPara;

    //reserve MoveID so it wont be reused by others
    if (!rMovedID.isEmpty())
    {
        SwDoc* const pDoc(static_cast<SwXMLImport&>(m_rImport).getDoc());
        assert(pDoc);
        pDoc->GetDocumentRedlineManager().GetRedlineTable().setMovedIDIfNeeded(rMovedID.toInt32());
    }

    // ad 3)
    auto itPair = m_aRedlineMap.emplace(rId, pInfo);
    if (itPair.second)
@@ -791,6 +802,8 @@ SwRedlineData* XMLRedlineImportHelper::ConvertRedline(
    aDT.SetSec(     pRedlineInfo->aDateTime.Seconds );
    aDT.SetNanoSec( pRedlineInfo->aDateTime.NanoSeconds );

    sal_uInt32 nMovedID = pRedlineInfo->sMovedID.toInt32();

    // 3) recursively convert next redline
    //    ( check presence and sanity of hierarchical redline info )
    SwRedlineData* pNext = nullptr;
@@ -803,7 +816,7 @@ SwRedlineData* XMLRedlineImportHelper::ConvertRedline(

    // create redline data
    SwRedlineData* pData = new SwRedlineData(pRedlineInfo->eType,
                                             nAuthorId, aDT,
                                             nAuthorId, aDT, nMovedID,
                                             pRedlineInfo->sComment,
                                             pNext); // next data (if available)

diff --git a/sw/source/filter/xml/XMLRedlineImportHelper.hxx b/sw/source/filter/xml/XMLRedlineImportHelper.hxx
index 69e530f..5bea1ba 100644
--- a/sw/source/filter/xml/XMLRedlineImportHelper.hxx
+++ b/sw/source/filter/xml/XMLRedlineImportHelper.hxx
@@ -81,6 +81,7 @@ public:
        const OUString& rAuthor,     // name of the author
        const OUString& rComment,    // redline comment
        const css::util::DateTime& rDateTime, // date+time
        const OUString& rMovedID,     // redline move id
        bool bMergeLastParagraph);      // merge last paragraph?

    // create a text section for the redline, and return an
diff --git a/sw/source/filter/xml/xmltexti.cxx b/sw/source/filter/xml/xmltexti.cxx
index 3035e22..fe9d1cb 100644
--- a/sw/source/filter/xml/xmltexti.cxx
+++ b/sw/source/filter/xml/xmltexti.cxx
@@ -927,12 +927,13 @@ void SwXMLTextImportHelper::RedlineAdd(
    const OUString& rAuthor,
    const OUString& rComment,
    const util::DateTime& rDateTime,
    const OUString& rMovedID,
    bool bMergeLastPara)
{
    // create redline helper on demand
    OSL_ENSURE(nullptr != m_pRedlineHelper, "helper should have been created in constructor");
    if (nullptr != m_pRedlineHelper)
        m_pRedlineHelper->Add(rType, rId, rAuthor, rComment, rDateTime,
        m_pRedlineHelper->Add(rType, rId, rAuthor, rComment, rDateTime, rMovedID,
                            bMergeLastPara);
}

diff --git a/sw/source/filter/xml/xmltexti.hxx b/sw/source/filter/xml/xmltexti.hxx
index 40d5b16..dbaab53 100644
--- a/sw/source/filter/xml/xmltexti.hxx
+++ b/sw/source/filter/xml/xmltexti.hxx
@@ -90,6 +90,7 @@ public:
        const OUString& rAuthor,     /// name of the author
        const OUString& rComment,    /// redline comment
        const css::util::DateTime& rDateTime,  /// date+time
        const OUString& rMovedID,    /// redline move id, to find moveFrom/MoveTo parts
        bool bMergeLastPara) override;           /// merge last paragraph
    virtual css::uno::Reference<css::text::XTextCursor> RedlineCreateText(
            css::uno::Reference<css::text::XTextCursor> & rOldCursor, /// needed to get the document
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.cxx b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
index 24071c7..ae121ea 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.cxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
@@ -344,6 +344,7 @@ DomainMapper_Impl::DomainMapper_Impl(
        m_nStartGenericField(0),
        m_bTextInserted(false),
        m_bTextDeleted(false),
        m_nLastRedlineMovedID(1),
        m_sCurrentPermId(0),
        m_bFrameDirectionSet(false),
        m_bInDocDefaultsImport(false),
@@ -3676,8 +3677,42 @@ void DomainMapper_Impl::CreateRedline(uno::Reference<text::XTextRange> const& xR
        pRedlineProperties[1].Value <<= aDateTime;
        pRedlineProperties[2].Name = getPropertyName( PROP_REDLINE_REVERT_PROPERTIES );
        pRedlineProperties[2].Value <<= pRedline->m_aRevertProperties;

        sal_uInt32 nRedlineMovedID = 0;
        if (bRedlineMoved)
        {
            if (!m_sCurrentBkmkId.isEmpty())
            {
                nRedlineMovedID = 1;
                BookmarkMap_t::iterator aBookmarkIter = m_aBookmarkMap.find(m_sCurrentBkmkId);
                if (aBookmarkIter != m_aBookmarkMap.end())
                {
                    OUString sMoveID = aBookmarkIter->second.m_sBookmarkName;
                    auto aIter = m_aRedlineMoveIDs.end();

                    if (sMoveID.indexOf("__RefMoveFrom__") >= 0)
                    {
                        aIter = std::find(m_aRedlineMoveIDs.begin(), m_aRedlineMoveIDs.end(),
                                          sMoveID.subView(15));
                    }
                    else if (sMoveID.indexOf("__RefMoveTo__") >= 0)
                    {
                        aIter = std::find(m_aRedlineMoveIDs.begin(), m_aRedlineMoveIDs.end(),
                                          sMoveID.subView(13));
                    };

                    if (aIter != m_aRedlineMoveIDs.end())
                    {
                        nRedlineMovedID = aIter - m_aRedlineMoveIDs.begin() + 2;
                        m_nLastRedlineMovedID = nRedlineMovedID;
                    }
                }
            }
            else
                nRedlineMovedID = m_nLastRedlineMovedID;
        }
        pRedlineProperties[3].Name = "RedlineMoved";
        pRedlineProperties[3].Value <<= bRedlineMoved;
        pRedlineProperties[3].Value <<= nRedlineMovedID;

        if (!m_bIsActualParagraphFramed)
        {
@@ -8406,6 +8441,14 @@ void DomainMapper_Impl::SetBookmarkName( const OUString& rBookmarkName )
            }
        }

        if ((m_sCurrentBkmkPrefix == "__RefMoveFrom__"
             || m_sCurrentBkmkPrefix == "__RefMoveTo__")
            && std::find(m_aRedlineMoveIDs.begin(), m_aRedlineMoveIDs.end(), rBookmarkName)
                   == m_aRedlineMoveIDs.end())
        {
            m_aRedlineMoveIDs.push_back(rBookmarkName);
        }

        aBookmarkIter->second.m_sBookmarkName = m_sCurrentBkmkPrefix + rBookmarkName;
        m_sCurrentBkmkPrefix.clear();
    }
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.hxx b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
index 9538200..ec34244 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.hxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
@@ -488,6 +488,10 @@ private:
    bool                                                                            m_bTextDeleted;
    LineNumberSettings                                                              m_aLineNumberSettings;

    std::vector<OUString>                                                           m_aRedlineMoveIDs;
    // Remember the last used redline MoveID. To avoid regression, because of wrong docx export
    sal_uInt32                                                                      m_nLastRedlineMovedID;

    BookmarkMap_t                                                                   m_aBookmarkMap;
    OUString                                                                        m_sCurrentBkmkId;
    OUString                                                                        m_sCurrentBkmkName;
diff --git a/xmloff/source/core/xmltoken.cxx b/xmloff/source/core/xmltoken.cxx
index 6519570..5798120 100644
--- a/xmloff/source/core/xmltoken.cxx
+++ b/xmloff/source/core/xmltoken.cxx
@@ -1359,6 +1359,7 @@ namespace xmloff::token {
        TOKEN( "move-from-left",                  XML_MOVE_FROM_LEFT ),
        TOKEN( "move-from-right",                 XML_MOVE_FROM_RIGHT ),
        TOKEN( "move-from-top",                   XML_MOVE_FROM_TOP ),
        TOKEN( "move-id",                         XML_MOVE_ID ),
        TOKEN( "move-protect",                    XML_MOVE_PROTECT ),
        TOKEN( "move-short",                      XML_MOVE_SHORT ),
        TOKEN( "movement",                        XML_MOVEMENT ),
diff --git a/xmloff/source/text/XMLChangeInfoContext.cxx b/xmloff/source/text/XMLChangeInfoContext.cxx
index 7ceb2f1..8782013 100644
--- a/xmloff/source/text/XMLChangeInfoContext.cxx
+++ b/xmloff/source/text/XMLChangeInfoContext.cxx
@@ -60,6 +60,9 @@ css::uno::Reference< css::xml::sax::XFastContextHandler > XMLChangeInfoContext::
        case XML_ELEMENT(DC, XML_DATE):
            xContext = new XMLStringBufferImportContext(GetImport(), sDateTimeBuffer);
            break;
        case XML_ELEMENT(LO_EXT, XML_MOVE_ID):
            xContext = new XMLStringBufferImportContext(GetImport(), sMovedIDBuffer);
            break;
        case XML_ELEMENT(TEXT, XML_P):
        case XML_ELEMENT(LO_EXT, XML_P):
            xContext = new XMLStringBufferImportContext(GetImport(), sCommentBuffer);
@@ -75,8 +78,8 @@ void XMLChangeInfoContext::endFastElement(sal_Int32 )
{
    // set values at changed region context
    rChangedRegion.SetChangeInfo(rType, sAuthorBuffer.makeStringAndClear(),
                                 sCommentBuffer.makeStringAndClear(),
                                 sDateTimeBuffer);
                                 sCommentBuffer.makeStringAndClear(), sDateTimeBuffer,
                                 sMovedIDBuffer.makeStringAndClear());
    sDateTimeBuffer.setLength(0);
}

diff --git a/xmloff/source/text/XMLChangeInfoContext.hxx b/xmloff/source/text/XMLChangeInfoContext.hxx
index 04f055d..994e285 100644
--- a/xmloff/source/text/XMLChangeInfoContext.hxx
+++ b/xmloff/source/text/XMLChangeInfoContext.hxx
@@ -43,6 +43,7 @@ class XMLChangeInfoContext : public SvXMLImportContext

    OUStringBuffer sAuthorBuffer;
    OUStringBuffer sDateTimeBuffer;
    OUStringBuffer sMovedIDBuffer;
    OUStringBuffer sCommentBuffer;

    XMLChangedRegionImportContext& rChangedRegion;
diff --git a/xmloff/source/text/XMLChangedRegionImportContext.cxx b/xmloff/source/text/XMLChangedRegionImportContext.cxx
index 4e80dda..7143ee4 100644
--- a/xmloff/source/text/XMLChangedRegionImportContext.cxx
+++ b/xmloff/source/text/XMLChangedRegionImportContext.cxx
@@ -139,13 +139,14 @@ void XMLChangedRegionImportContext::SetChangeInfo(
    const OUString& rType,
    const OUString& rAuthor,
    const OUString& rComment,
    std::u16string_view rDate)
    std::u16string_view rDate,
    const OUString& rMovedID)
{
    util::DateTime aDateTime;
    if (::sax::Converter::parseDateTime(aDateTime, rDate))
    {
        GetImport().GetTextImport()->RedlineAdd(
            rType, sID, rAuthor, rComment, aDateTime, bMergeLastPara);
            rType, sID, rAuthor, rComment, aDateTime, rMovedID, bMergeLastPara);
    }
}

diff --git a/xmloff/source/text/XMLChangedRegionImportContext.hxx b/xmloff/source/text/XMLChangedRegionImportContext.hxx
index db0e48a..e05e97c 100644
--- a/xmloff/source/text/XMLChangedRegionImportContext.hxx
+++ b/xmloff/source/text/XMLChangedRegionImportContext.hxx
@@ -70,7 +70,8 @@ public:
    void SetChangeInfo(const OUString& rType,
                       const OUString& rAuthor,
                       const OUString& rComment,
                       std::u16string_view rDate);
                       std::u16string_view rDate,
                       const OUString& rMovedId);

    /// create redline XText/XTextCursor on demand and register with
    /// XMLTextImportHelper
diff --git a/xmloff/source/text/XMLRedlineExport.cxx b/xmloff/source/text/XMLRedlineExport.cxx
index a24c89e..33ddcdb 100644
--- a/xmloff/source/text/XMLRedlineExport.cxx
+++ b/xmloff/source/text/XMLRedlineExport.cxx
@@ -455,6 +455,15 @@ void XMLRedlineExport::ExportChangeInfo(
                : sTmp );
    }

    aAny = rPropSet->getPropertyValue("RedlineMovedID");
    sal_uInt32 nTmp(0);
    aAny >>= nTmp;
    if (nTmp > 1)
    {
        SvXMLElementExport aCreatorElem(rExport, XML_NAMESPACE_LO_EXT, XML_MOVE_ID, true, false);
        rExport.Characters( OUString::number( nTmp ) );
    }

    aAny = rPropSet->getPropertyValue("RedlineDateTime");
    util::DateTime aDateTime;
    aAny >>= aDateTime;
diff --git a/xmloff/source/text/txtimp.cxx b/xmloff/source/text/txtimp.cxx
index 15d7b1f..ac0e894 100644
--- a/xmloff/source/text/txtimp.cxx
+++ b/xmloff/source/text/txtimp.cxx
@@ -2377,6 +2377,7 @@ void XMLTextImportHelper::RedlineAdd( const OUString& /*rType*/,
                                      const OUString& /*rAuthor*/,
                                      const OUString& /*rComment*/,
                                      const util::DateTime& /*rDateTime*/,
                                      const OUString& /*rMovedID*/,
                                      bool /*bMergeLastPara*/)
{
    // dummy implementation: do nothing
diff --git a/xmloff/source/token/tokens.txt b/xmloff/source/token/tokens.txt
index 59ae404..731bf91 100644
--- a/xmloff/source/token/tokens.txt
+++ b/xmloff/source/token/tokens.txt
@@ -1259,6 +1259,7 @@ move-from-bottom
move-from-left
move-from-right
move-from-top
move-id
move-protect
move-short
movement