tdf#131912 sw: fix spell check correct deleting flys

* SwEditShell::ApplyChangedSentence() should not call
  DeleteAndJoin() + InsertString() but ReplaceRange()

* ReplaceRange() and SwUndoReplace need to set a new flag
  DelContentType::Replace to tell SwUndoSaveContent::DelContentIndex()
  not to delete flys but instead record the previous anchor positions

* SwUndoReplace::UndoImpl() should also not call DeleteAndJoin()
  + InsertString(); instead call ReplaceRange() for the start node
  and then DeleteAndJoin() for any regex "\n" that were inserted

(regression from 28b77c89dfcafae82cf2a6d85731b643ff9290e5)

Change-Id: I485d79510ae233213cb4b208533871934c5e5ec6
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/96201
Tested-by: Jenkins
Reviewed-by: Michael Stahl <michael.stahl@cib.de>
(cherry picked from commit e1629c210ad78310e3d48c0756723134a27b89df)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/96186
diff --git a/sw/inc/undobj.hxx b/sw/inc/undobj.hxx
index 8da9b70..9139444 100644
--- a/sw/inc/undobj.hxx
+++ b/sw/inc/undobj.hxx
@@ -135,12 +135,13 @@ enum class DelContentType : sal_uInt16
    Fly          = 0x02,
    Bkm          = 0x08,
    AllMask      = 0x0b,
    Replace      = 0x10,
    WriterfilterHack = 0x20,
    ExcludeFlyAtStartEnd = 0x40,
    CheckNoCntnt = 0x80,
};
namespace o3tl {
    template<> struct typed_flags<DelContentType> : is_typed_flags<DelContentType, 0xeb> {};
    template<> struct typed_flags<DelContentType> : is_typed_flags<DelContentType, 0xfb> {};
}

/// will DelContentIndex destroy a frame anchored at character at rAnchorPos?
diff --git a/sw/qa/extras/uiwriter/uiwriter2.cxx b/sw/qa/extras/uiwriter/uiwriter2.cxx
index 74f6c5e..99f573b1 100644
--- a/sw/qa/extras/uiwriter/uiwriter2.cxx
+++ b/sw/qa/extras/uiwriter/uiwriter2.cxx
@@ -313,6 +313,154 @@ CPPUNIT_TEST_FIXTURE(SwUiWriterTest2, testTdf132236)
    assertXPath(pXmlDoc, "/root/page[1]/body/txt", 1);
}

CPPUNIT_TEST_FIXTURE(SwUiWriterTest2, testTdf131912)
{
    SwDoc* const pDoc = createDoc();
    SwWrtShell* const pWrtShell = pDoc->GetDocShell()->GetWrtShell();

    sw::UndoManager& rUndoManager = pDoc->GetUndoManager();

    sw::UnoCursorPointer pCursor(
        pDoc->CreateUnoCursor(SwPosition(SwNodeIndex(pDoc->GetNodes().GetEndOfContent(), -1))));

    pDoc->getIDocumentContentOperations().InsertString(*pCursor, "foo");

    {
        SfxItemSet flySet(pDoc->GetAttrPool(),
                          svl::Items<RES_FRM_SIZE, RES_FRM_SIZE, RES_ANCHOR, RES_ANCHOR>{});
        SwFormatAnchor anchor(RndStdIds::FLY_AT_CHAR);
        pWrtShell->StartOfSection(false);
        pWrtShell->Right(CRSR_SKIP_CHARS, /*bSelect=*/false, 2, /*bBasicCall=*/false);
        anchor.SetAnchor(pWrtShell->GetCursor()->GetPoint());
        flySet.Put(anchor);
        SwFormatFrameSize size(SwFrameSize::Minimum, 1000, 1000);
        flySet.Put(size); // set a size, else we get 1 char per line...
        SwFrameFormat const* pFly = pWrtShell->NewFlyFrame(flySet, /*bAnchValid=*/true);
        CPPUNIT_ASSERT(pFly != nullptr);
    }
    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));

    pCursor->SetMark();
    pCursor->GetMark()->nContent.Assign(pCursor->GetContentNode(), 0);
    pCursor->GetPoint()->nContent.Assign(pCursor->GetContentNode(), 3);

    // replace with more text
    pDoc->getIDocumentContentOperations().ReplaceRange(*pCursor, "blahblah", false);

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("blahblah"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Redo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("blahblah"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Undo();

    pCursor->GetMark()->nContent.Assign(pCursor->GetContentNode(), 0);
    pCursor->GetPoint()->nContent.Assign(pCursor->GetContentNode(), 3);

    // replace with less text
    pDoc->getIDocumentContentOperations().ReplaceRange(*pCursor, "x", false);

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("x"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Redo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("x"), pCursor->GetNode().GetTextNode()->GetText());

    rUndoManager.Undo();

    pCursor->GetMark()->nContent.Assign(pCursor->GetContentNode(), 0);
    pCursor->GetPoint()->nContent.Assign(pCursor->GetContentNode(), 3);

    // regex replace with paragraph breaks
    pDoc->getIDocumentContentOperations().ReplaceRange(*pCursor, "xyz\\n\\nquux\\n", true);

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz\n\nquux\n"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pCursor->GetNode().GetTextNode()->GetText());
    pWrtShell->StartOfSection(false);
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Redo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz\n\nquux\n"), pWrtShell->GetCursor()->GetText());

    // regex replace with paragraph join
    pWrtShell->StartOfSection(false);
    pWrtShell->Down(true);
    pDoc->getIDocumentContentOperations().ReplaceRange(*pWrtShell->GetCursor(), "bar", true);

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("bar"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("bar\nquux\n"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz\n\nquux\n"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Redo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("bar"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("bar\nquux\n"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    pWrtShell->StartOfSection(false);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz"),
                         pWrtShell->GetCursor()->GetNode().GetTextNode()->GetText());
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("xyz\n\nquux\n"), pWrtShell->GetCursor()->GetText());

    rUndoManager.Undo();

    CPPUNIT_ASSERT_EQUAL(size_t(1), pDoc->GetFlyCount(FLYCNTTYPE_FRM));
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pCursor->GetNode().GetTextNode()->GetText());
    pWrtShell->StartOfSection(false);
    pWrtShell->EndOfSection(true);
    CPPUNIT_ASSERT_EQUAL(OUString("foo"), pWrtShell->GetCursor()->GetText());
}

CPPUNIT_TEST_FIXTURE(SwUiWriterTest2, testTdf54819)
{
    load(DATA_DIRECTORY, "tdf54819.fodt");
diff --git a/sw/source/core/edit/edlingu.cxx b/sw/source/core/edit/edlingu.cxx
index a2a4adf..15e8532 100644
--- a/sw/source/core/edit/edlingu.cxx
+++ b/sw/source/core/edit/edlingu.cxx
@@ -1141,11 +1141,10 @@ void SwEditShell::ApplyChangedSentence(const svx::SpellPortions& rNewPortions, b
            if(aCurrentNewPortion->sText != aCurrentOldPortion->sText)
            {
                // change text ...
                mxDoc->getIDocumentContentOperations().DeleteAndJoin(*pCursor);
                // ... and apply language if necessary
                if(aCurrentNewPortion->eLanguage != aCurrentOldPortion->eLanguage)
                    SetAttrItem( SvxLanguageItem(aCurrentNewPortion->eLanguage, nLangWhichId) );
                mxDoc->getIDocumentContentOperations().InsertString(*pCursor, aCurrentNewPortion->sText);
                mxDoc->getIDocumentContentOperations().ReplaceRange(*pCursor, aCurrentNewPortion->sText, false);
            }
            else if(aCurrentNewPortion->eLanguage != aCurrentOldPortion->eLanguage)
            {
diff --git a/sw/source/core/undo/undobj.cxx b/sw/source/core/undo/undobj.cxx
index 1da78ab..3a3546c 100644
--- a/sw/source/core/undo/undobj.cxx
+++ b/sw/source/core/undo/undobj.cxx
@@ -971,7 +971,8 @@ void SwUndoSaveContent::DelContentIndex( const SwPosition& rMark,
                            if (!m_pHistory)
                                m_pHistory.reset( new SwHistory );

                            if (IsSelectFrameAnchoredAtPara(*pAPos, *pStt, *pEnd, nDelContentType))
                            if (!(DelContentType::Replace & nDelContentType)
                                && IsSelectFrameAnchoredAtPara(*pAPos, *pStt, *pEnd, nDelContentType))
                            {
                                m_pHistory->AddDeleteFly(*pFormat, nChainInsPos);
                                // reset n so that no Format is skipped
@@ -1002,7 +1003,8 @@ void SwUndoSaveContent::DelContentIndex( const SwPosition& rMark,
                    {
                        if( !m_pHistory )
                            m_pHistory.reset( new SwHistory );
                        if (IsDestroyFrameAnchoredAtChar(
                        if (!(DelContentType::Replace & nDelContentType)
                            && IsDestroyFrameAnchoredAtChar(
                                *pAPos, *pStt, *pEnd, nDelContentType))
                        {
                            m_pHistory->AddDeleteFly(*pFormat, nChainInsPos);
diff --git a/sw/source/core/undo/unins.cxx b/sw/source/core/undo/unins.cxx
index 6fb4e82..6e08fb5 100644
--- a/sw/source/core/undo/unins.cxx
+++ b/sw/source/core/undo/unins.cxx
@@ -597,7 +597,7 @@ SwUndoReplace::Impl::Impl(
    OSL_ENSURE( pNd, "Dude, where's my TextNode?" );

    m_pHistory.reset( new SwHistory );
    DelContentIndex( *rPam.GetMark(), *rPam.GetPoint() );
    DelContentIndex(*rPam.GetMark(), *rPam.GetPoint(), DelContentType::AllMask | DelContentType::Replace);

    m_nSetPos = m_pHistory->Count();

@@ -658,42 +658,39 @@ void SwUndoReplace::Impl::UndoImpl(::sw::UndoRedoContext & rContext)
        pDoc->SetAutoCorrExceptWord( nullptr );
    }

    SwIndex aIdx( pNd, m_nSttCnt );
    // don't look at m_sIns for deletion, maybe it was not completely inserted
    {
        rPam.GetPoint()->nNode = *pNd;
        rPam.GetPoint()->nContent.Assign( pNd, m_nSttCnt );
        rPam.SetMark();
        rPam.GetPoint()->nNode = m_nEndNd - m_nOffset;
        rPam.GetPoint()->nContent.Assign( rPam.GetContentNode(), m_nEndCnt );
        // move it out of the way so it is not registered at deleted node
        aIdx.Assign(nullptr, 0);
        rPam.GetPoint()->nNode = m_nSttNd - m_nOffset;
        rPam.GetPoint()->nContent.Assign(rPam.GetContentNode(), m_nSttNd == m_nEndNd ? m_nEndCnt : pNd->Len());

        pDoc->getIDocumentContentOperations().DeleteAndJoin( rPam );
        // replace only in start node, without regex
        bool const ret = pDoc->getIDocumentContentOperations().ReplaceRange(rPam, m_sOld, false);
        assert(ret); (void)ret;
        if (m_nSttNd != m_nEndNd)
        {   // in case of regex inserting paragraph breaks, join nodes...
            assert(rPam.GetMark()->nContent == rPam.GetMark()->nNode.GetNode().GetTextNode()->Len());
            rPam.GetPoint()->nNode = m_nEndNd - m_nOffset;
            rPam.GetPoint()->nContent.Assign(rPam.GetContentNode(true), m_nEndCnt);
            pDoc->getIDocumentContentOperations().DeleteAndJoin(rPam);
        }
        rPam.DeleteMark();
        pNd = rPam.GetNode().GetTextNode();
        pNd = pDoc->GetNodes()[ m_nSttNd - m_nOffset ]->GetTextNode();
        OSL_ENSURE( pNd, "Dude, where's my TextNode?" );
        aIdx.Assign( pNd, m_nSttCnt );
    }

    if( m_bSplitNext )
    {
        SwPosition aPos( *pNd, aIdx );
        SwPosition aPos(*pNd, pNd->Len());
        pDoc->getIDocumentContentOperations().SplitNode( aPos, false );
        pNd->RestoreMetadata(m_pMetadataUndoEnd);
        pNd = pDoc->GetNodes()[ m_nSttNd - m_nOffset ]->GetTextNode();
        aIdx.Assign( pNd, m_nSttCnt );
        // METADATA: restore
        pNd->RestoreMetadata(m_pMetadataUndoStart);
    }

    if (!m_sOld.isEmpty())
    {
        OUString const ins( pNd->InsertText( m_sOld, aIdx ) );
        assert(ins.getLength() == m_sOld.getLength()); // must succeed
        (void) ins;
    }

    if( m_pHistory )
    {
        if( pNd->GetpSwpHints() )
@@ -720,7 +717,7 @@ void SwUndoReplace::Impl::UndoImpl(::sw::UndoRedoContext & rContext)
    }

    rPam.GetPoint()->nNode = m_nSttNd;
    rPam.GetPoint()->nContent = aIdx;
    rPam.GetPoint()->nContent = m_nSttCnt;
}

void SwUndoReplace::Impl::RedoImpl(::sw::UndoRedoContext & rContext)
@@ -746,7 +743,7 @@ void SwUndoReplace::Impl::RedoImpl(::sw::UndoRedoContext & rContext)
        auto xSave = std::make_unique<SwHistory>();
        std::swap(m_pHistory, xSave);

        DelContentIndex( *rPam.GetMark(), *rPam.GetPoint() );
        DelContentIndex(*rPam.GetMark(), *rPam.GetPoint(), DelContentType::AllMask | DelContentType::Replace);
        m_nSetPos = m_pHistory->Count();

        std::swap(xSave, m_pHistory);
@@ -755,7 +752,7 @@ void SwUndoReplace::Impl::RedoImpl(::sw::UndoRedoContext & rContext)
    else
    {
        m_pHistory.reset( new SwHistory );
        DelContentIndex( *rPam.GetMark(), *rPam.GetPoint() );
        DelContentIndex(*rPam.GetMark(), *rPam.GetPoint(), DelContentType::AllMask | DelContentType::Replace);
        m_nSetPos = m_pHistory->Count();
        if( !m_nSetPos )
        {