sw/inc/IDocumentMarkAccess.hxx      |    2 
 sw/inc/cmdid.h                      |    2 
 sw/qa/uibase/shells/shells.cxx      |   96 ++++++++++++++++++++++++++++++++++
 sw/qa/uibase/uno/uno.cxx            |   68 ++++++++++++++++++++++++
 sw/sdi/_textsh.sdi                  |   11 +++
 sw/sdi/swriter.sdi                  |   28 ++++++++++
 sw/source/core/doc/docbm.cxx        |   23 ++++++++
 sw/source/core/inc/MarkManager.hxx  |    1 
 sw/source/uibase/shells/textfld.cxx |   54 +++++++++++++++++++
 sw/source/uibase/shells/textsh1.cxx |   86 ++++++++++++++++++++++++++++++
 sw/source/uibase/uno/loktxdoc.cxx   |  100 +++++++++++++++++++++++++++++++++++-
 11 files changed, 470 insertions(+), 1 deletion(-)

New commits:
commit 489ffdb01c121a54a5b242aad33909bece0da689
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Wed Jan 11 15:56:31 2023 +0100
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Thu Feb 2 08:40:25 2023 +0100

    sw: add a new .uno:UpdateBookmark UNO command
    
    It is possible to update all bookmarks (having a certain name prefix)
    and their contet, but one can't update the bookmark under the cursor,
    which is needed for Zotero citation clusters.
    
    Fix the problem by adding a new .uno:UpdateBookmark UNO command that can
    update the (innermost) bookmark under the current cursor.
    
    This can be implemented on top of the recently added
    IDocumentMarkAccess::getBookmarkFor().
    
    The UNO command is intentionally hidden from the customize dialog since
    it only makes sense to invoke it from a macro / API with parameters, not
    interactively.
    
    (cherry picked from commit ea208f6004770eb4b81d28e6930cd0c7bd5d8f12)
    
    Change-Id: I3e750dfb637f50716be1155a94bc986131b84f20

diff --git a/sw/inc/cmdid.h b/sw/inc/cmdid.h
index befe8e26d607..3baa2f050ecc 100644
--- a/sw/inc/cmdid.h
+++ b/sw/inc/cmdid.h
@@ -325,6 +325,7 @@ class SwUINumRuleItem;
 #define FN_UPDATE_BOOKMARKS (FN_INSERT2 + 34)
 #define FN_UPDATE_SECTIONS (FN_INSERT2 + 35)
 #define FN_DELETE_TEXT_FORMFIELDS (FN_INSERT2 + 36)
+#define FN_UPDATE_BOOKMARK (FN_INSERT2 + 37)
 
 // Region: Format
 #define FN_AUTOFORMAT_APPLY     (FN_FORMAT + 1 ) /* apply autoformat options */
diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx
index 8ca38e4a78d5..7bbfd83b6c06 100644
--- a/sw/qa/uibase/shells/shells.cxx
+++ b/sw/qa/uibase/shells/shells.cxx
@@ -749,6 +749,57 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, 
testDeleteFieldmarks)
     CPPUNIT_ASSERT_EQUAL(OUString("result 1result 2"), aActual);
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateBookmark)
+{
+    // Given a document with a bookmarks, covering "BC":
+    createSwDoc();
+    SwDoc* pDoc = getSwDoc();
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->Insert("ABCD");
+    pWrtShell->SttEndDoc(/*bStt=*/true);
+    pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, 
/*bBasicCall=*/false);
+    pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 2, 
/*bBasicCall=*/false);
+    pWrtShell->SetBookmark(vcl::KeyCode(), "ZOTERO_BREF_old");
+
+    // When updating the content of the bookmark under the cursor:
+    pWrtShell->SttEndDoc(/*bStt=*/true);
+    pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 2, 
/*bBasicCall=*/false);
+    std::vector<beans::PropertyValue> aArgsVec = 
comphelper::JsonToPropertyValues(R"json(
+{
+    "BookmarkNamePrefix": {
+        "type": "string",
+        "value": "ZOTERO_BREF_"
+    },
+    "Bookmark": {
+        "type": "[]com.sun.star.beans.PropertyValue",
+        "value": {
+            "Bookmark": {
+                "type": "string",
+                "value": "ZOTERO_BREF_new"
+            },
+            "BookmarkText": {
+                "type": "string",
+                "value": "new result"
+            }
+        }
+    }
+}
+)json");
+    uno::Sequence<beans::PropertyValue> aArgs = 
comphelper::containerToSequence(aArgsVec);
+    dispatchCommand(mxComponent, ".uno:UpdateBookmark", aArgs);
+
+    // Then make sure that the only paragraph is updated correctly:
+    SwCursor* pCursor = pWrtShell->GetCursor();
+    OUString aActual = pCursor->GetPointNode().GetTextNode()->GetText();
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: Anew resultD
+    // - Actual  : ABCD
+    // i.e. it was not possible to update just the bookmark under cursor.
+    CPPUNIT_ASSERT_EQUAL(OUString("Anew resultD"), aActual);
+    auto it = pDoc->getIDocumentMarkAccess()->findMark("ZOTERO_BREF_new");
+    CPPUNIT_ASSERT(it != pDoc->getIDocumentMarkAccess()->getAllMarksEnd());
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/sdi/_textsh.sdi b/sw/sdi/_textsh.sdi
index 8dce73937deb..1098a9f14278 100644
--- a/sw/sdi/_textsh.sdi
+++ b/sw/sdi/_textsh.sdi
@@ -152,6 +152,11 @@ interface BaseText
         StateMethod = GetState ;
         DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
     ]
+    FN_UPDATE_BOOKMARK
+    [
+        ExecMethod = Execute ;
+        DisableFlags="SfxDisableFlags::SwOnProtectedCursor";
+    ]
     FN_UPDATE_SECTIONS
     [
         ExecMethod = Execute ;
diff --git a/sw/sdi/swriter.sdi b/sw/sdi/swriter.sdi
index 83b29b37232f..23699a4e457a 100644
--- a/sw/sdi/swriter.sdi
+++ b/sw/sdi/swriter.sdi
@@ -2600,6 +2600,20 @@ SfxVoidItem UpdateBookmarks FN_UPDATE_BOOKMARKS
     GroupId = SfxGroupId::Insert;
 ]
 
+SfxVoidItem UpdateBookmark FN_UPDATE_BOOKMARK
+(SfxStringItem BookmarkNamePrefix FN_PARAM_1, SfxUnoAnyItem Bookmark 
FN_PARAM_2)
+[
+    AutoUpdate = FALSE,
+    FastCall = FALSE,
+    ReadOnlyDoc = FALSE,
+    Toggle = FALSE,
+    Container = FALSE,
+    RecordAbsolute = FALSE,
+    RecordPerSet;
+
+    GroupId = SfxGroupId::Insert;
+]
+
 SfxVoidItem UpdateSections FN_UPDATE_SECTIONS
 (SfxStringItem SectionNamePrefix FN_PARAM_1, SfxUnoAnyItem Sections FN_PARAM_2)
 [
diff --git a/sw/source/uibase/shells/textsh1.cxx 
b/sw/source/uibase/shells/textsh1.cxx
index 8faa175ce88a..313b34ca6265 100644
--- a/sw/source/uibase/shells/textsh1.cxx
+++ b/sw/source/uibase/shells/textsh1.cxx
@@ -103,6 +103,7 @@
 #include <bookmark.hxx>
 #include <linguistic/misc.hxx>
 #include <comphelper/sequenceashashmap.hxx>
+#include <comphelper/scopeguard.hxx>
 #include <authfld.hxx>
 #include <config_wasm_strip.h>
 #if !ENABLE_WASM_STRIP_EXTRA
@@ -535,6 +536,83 @@ void UpdateBookmarks(SfxRequest& rReq, SwWrtShell& rWrtSh)
     rWrtSh.EndAction();
     rWrtSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSBOOKMARK, 
nullptr);
 }
+
+void UpdateBookmark(SfxRequest& rReq, SwWrtShell& rWrtSh)
+{
+    if 
(rWrtSh.getIDocumentSettingAccess().get(DocumentSettingId::PROTECT_BOOKMARKS))
+    {
+        return;
+    }
+
+    OUString aBookmarkNamePrefix;
+    const SfxStringItem* pBookmarkNamePrefix = 
rReq.GetArg<SfxStringItem>(FN_PARAM_1);
+    if (pBookmarkNamePrefix)
+    {
+        aBookmarkNamePrefix = pBookmarkNamePrefix->GetValue();
+    }
+
+    uno::Sequence<beans::PropertyValue> aBookmark;
+    const SfxUnoAnyItem* pBookmarks = rReq.GetArg<SfxUnoAnyItem>(FN_PARAM_2);
+    if (pBookmarks)
+    {
+        pBookmarks->GetValue() >>= aBookmark;
+    }
+
+    rWrtSh.GetDoc()->GetIDocumentUndoRedo().StartUndo(SwUndoId::INSBOOKMARK, 
nullptr);
+    rWrtSh.StartAction();
+    comphelper::ScopeGuard g(
+        [&rWrtSh]
+        {
+            rWrtSh.EndAction();
+            
rWrtSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSBOOKMARK, nullptr);
+        });
+
+    IDocumentMarkAccess& rIDMA = *rWrtSh.GetDoc()->getIDocumentMarkAccess();
+    SwPosition& rCursor = *rWrtSh.GetCursor()->GetPoint();
+    auto pBookmark = 
dynamic_cast<sw::mark::Bookmark*>(rIDMA.getBookmarkFor(rCursor));
+    if (!pBookmark || !pBookmark->GetName().startsWith(aBookmarkNamePrefix))
+    {
+        return;
+    }
+
+    comphelper::SequenceAsHashMap aMap(aBookmark);
+    if (aMap["Bookmark"].get<OUString>() != pBookmark->GetName())
+    {
+        rIDMA.renameMark(pBookmark, aMap["Bookmark"].get<OUString>());
+    }
+
+    OUString aBookmarkText = aMap["BookmarkText"].get<OUString>();
+
+    // Insert markers to remember where the paste positions are.
+    SwPaM aMarkers(pBookmark->GetMarkEnd());
+    IDocumentContentOperations& rIDCO = 
rWrtSh.GetDoc()->getIDocumentContentOperations();
+    if (!rIDCO.InsertString(aMarkers, "XY"))
+    {
+        return;
+    }
+
+    SwPaM aPasteEnd(pBookmark->GetMarkEnd());
+    aPasteEnd.Move(fnMoveForward, GoInContent);
+
+    // Paste HTML content.
+    SwPaM* pCursorPos = rWrtSh.GetCursor();
+    *pCursorPos = aPasteEnd;
+    SwTranslateHelper::PasteHTMLToPaM(rWrtSh, pCursorPos, 
aBookmarkText.toUtf8(), true);
+
+    // Update the bookmark to point to the new content.
+    SwPaM aPasteStart(pBookmark->GetMarkEnd());
+    aPasteStart.Move(fnMoveForward, GoInContent);
+    SwPaM aStartMarker(pBookmark->GetMarkStart(), *aPasteStart.GetPoint());
+    SwPaM aEndMarker(*aPasteEnd.GetPoint(), *aPasteEnd.GetPoint());
+    aEndMarker.GetMark()->AdjustContent(1);
+    pBookmark->SetMarkPos(*aPasteStart.GetPoint());
+    pBookmark->SetOtherMarkPos(*aPasteEnd.GetPoint());
+
+    // Remove markers. the start marker includes the old content as well.
+    rIDCO.DeleteAndJoin(aStartMarker);
+    rIDCO.DeleteAndJoin(aEndMarker);
+    rIDMA.assureSortedMarkContainers();
+}
 }
 
 void SwTextShell::Execute(SfxRequest &rReq)
@@ -916,9 +994,17 @@ void SwTextShell::Execute(SfxRequest &rReq)
         }
         case FN_UPDATE_BOOKMARKS:
         {
+            // This updates all bookmarks in the document that match the 
conditions specified in
+            // rReq.
             UpdateBookmarks(rReq, rWrtSh);
             break;
         }
+        case FN_UPDATE_BOOKMARK:
+        {
+            // This updates the bookmark under the cursor.
+            UpdateBookmark(rReq, rWrtSh);
+            break;
+        }
         case FN_DELETE_BOOKMARK:
         {
             if (pItem && 
!rWrtSh.getIDocumentSettingAccess().get(DocumentSettingId::PROTECT_BOOKMARKS))
commit 6fea284655358d459e2c0725512b463d7424823e
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Thu Jan 12 15:13:00 2023 +0100
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Thu Feb 2 08:39:17 2023 +0100

    sw lok: expose name of refmark under cursor
    
    This is similar to commit 4bcb66ec7b417fbe113267f2615e78fe47eb55ca (sw
    lok: expose name of bookmark under cursor, 2023-01-11), but that was for
    the bookmark under the cursor and this is for refmarks.
    
    (cherry picked from commit 81f690ec0cb2a6dc0d6ca0f6de3adcc07eb7bc12)
    
    Change-Id: I0acf0181d0acfdc087e1ed737b3c18ab3736031a

diff --git a/sw/qa/uibase/uno/uno.cxx b/sw/qa/uibase/uno/uno.cxx
index a91de1c2aaa1..d6625b3c4dc7 100644
--- a/sw/qa/uibase/uno/uno.cxx
+++ b/sw/qa/uibase/uno/uno.cxx
@@ -475,6 +475,41 @@ CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetBookmark)
     CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_BREF_1"), 
aBookmark.get<std::string>("name"));
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetField)
+{
+    // Given a document with a refmark:
+    createSwDoc();
+    uno::Sequence<css::beans::PropertyValue> aArgs = {
+        comphelper::makePropertyValue("TypeName", 
uno::Any(OUString("SetRef"))),
+        comphelper::makePropertyValue("Name",
+                                      uno::Any(OUString("ZOTERO_ITEM 
CSL_CITATION {} refmark"))),
+        comphelper::makePropertyValue("Content", 
uno::Any(OUString("content"))),
+    };
+    dispatchCommand(mxComponent, ".uno:InsertField", aArgs);
+
+    // When in the refmark with the cursor and getting the command value for 
.uno:Field:
+    SwDoc* pDoc = getSwDoc();
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->SttEndDoc(/*bStt=*/false);
+    pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, 
/*bBasicCall=*/false);
+    tools::JsonWriter aJsonWriter;
+    std::string_view 
aCommand(".uno:Field?typeName=SetRef&namePrefix=ZOTERO_ITEM%20CSL_CITATION");
+    auto pXTextDocument = dynamic_cast<SwXTextDocument*>(mxComponent.get());
+    pXTextDocument->getCommandValues(aJsonWriter, aCommand);
+
+    // Then make sure we find the inserted refmark:
+    std::unique_ptr<char[], o3tl::free_delete> 
pJSON(aJsonWriter.extractData());
+    std::stringstream aStream(pJSON.get());
+    boost::property_tree::ptree aTree;
+    boost::property_tree::read_json(aStream, aTree);
+    boost::property_tree::ptree aBookmark = aTree.get_child("setRef");
+    // Without the accompanying fix in place, this test would have failed with:
+    // - No such node (setRef)
+    // i.e. the returned JSON was an empty object.
+    CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} refmark"),
+                         aBookmark.get<std::string>("name"));
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/uibase/uno/loktxdoc.cxx 
b/sw/source/uibase/uno/loktxdoc.cxx
index fd3fbbec01d5..51f5a9ca851d 100644
--- a/sw/source/uibase/uno/loktxdoc.cxx
+++ b/sw/source/uibase/uno/loktxdoc.cxx
@@ -326,6 +326,58 @@ void GetFields(tools::JsonWriter& rJsonWriter, SwDocShell* 
pDocShell,
     }
 }
 
+/// Implements getCommandValues(".uno:Field").
+///
+/// Parameters:
+///
+/// - typeName: field type condition to not return all fields
+/// - namePrefix: field name prefix to not return all fields
+void GetField(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell,
+              const std::map<OUString, OUString>& rArguments)
+{
+    OUString aTypeName;
+    {
+        auto it = rArguments.find("typeName");
+        if (it != rArguments.end())
+        {
+            aTypeName = it->second;
+        }
+    }
+    // See SwFieldTypeFromString().
+    if (aTypeName != "SetRef")
+    {
+        return;
+    }
+
+    OUString aNamePrefix;
+    {
+        auto it = rArguments.find("namePrefix");
+        if (it != rArguments.end())
+        {
+            aNamePrefix = it->second;
+        }
+    }
+
+    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
+    SwPosition& rCursor = *pWrtShell->GetCursor()->GetPoint();
+    SwTextNode* pTextNode = rCursor.GetNode().GetTextNode();
+    std::vector<SwTextAttr*> aAttrs
+        = pTextNode->GetTextAttrsAt(rCursor.GetContentIndex(), 
RES_TXTATR_REFMARK);
+    tools::ScopedJsonWriterNode aRefmark = rJsonWriter.startNode("setRef");
+    if (aAttrs.empty())
+    {
+        return;
+    }
+
+    const SwFormatRefMark& rRefmark = aAttrs[0]->GetRefMark();
+    if (!rRefmark.GetRefName().startsWith(aNamePrefix))
+    {
+        return;
+    }
+
+    rJsonWriter.put("name", rRefmark.GetRefName());
+}
+
 /// Implements getCommandValues(".uno:Sections").
 ///
 /// Parameters:
@@ -361,8 +413,9 @@ void GetSections(tools::JsonWriter& rJsonWriter, 
SwDocShell* pDocShell,
 bool SwXTextDocument::supportsCommand(std::u16string_view rCommand)
 {
     static const std::initializer_list<std::u16string_view> vForward
-        = { u"TextFormFields", u"TextFormField", u"SetDocumentProperties", 
u"Bookmarks", u"Fields",
-            u"Sections",       u"Bookmark" };
+        = { u"TextFormFields", u"TextFormField", u"SetDocumentProperties",
+            u"Bookmarks",      u"Fields",        u"Sections",
+            u"Bookmark",       u"Field" };
 
     return std::find(vForward.begin(), vForward.end(), rCommand) != 
vForward.end();
 }
@@ -378,6 +431,7 @@ void SwXTextDocument::getCommandValues(tools::JsonWriter& 
rJsonWriter, std::stri
     static constexpr OStringLiteral aFields(".uno:Fields");
     static constexpr OStringLiteral aSections(".uno:Sections");
     static constexpr OStringLiteral aBookmark(".uno:Bookmark");
+    static constexpr OStringLiteral aField(".uno:Field");
 
     INetURLObject aParser(OUString::fromUtf8(rCommand));
     OUString aArguments = aParser.GetParam();
@@ -429,6 +483,10 @@ void SwXTextDocument::getCommandValues(tools::JsonWriter& 
rJsonWriter, std::stri
     {
         GetBookmark(rJsonWriter, m_pDocShell, aMap);
     }
+    else if (o3tl::starts_with(rCommand, aField))
+    {
+        GetField(rJsonWriter, m_pDocShell, aMap);
+    }
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit bb03119463d2f01ff3f5567b43aaf61e69169d34
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Wed Jan 11 10:38:05 2023 +0100
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Thu Feb 2 08:32:54 2023 +0100

    sw lok: expose name of bookmark under cursor
    
    It was possible to get the names of all bookmarks, but you could not get
    the name of the (innermost) bookmark under the current cursor.
    
    Getting the name of the current bookmark is useful for Zotero: if we
    already have a citation and want to insert one more, then we should turn
    the current citation into a citation cluster.
    
    Fix the problem by adding an API similar to what commit
    bb20dee2ef1b0804065e1cda2c834d257fdd90ed (sw lok: expose field type &
    command of fieldmark under cursor, 2023-01-05) did, but here we deal
    with bookmarks, not fieldmarks.
    
    Handle the actual bookmark lookup in MarkManager, such functionality
    looks useful outside LOK as well.
    
    (cherry picked from commit 4bcb66ec7b417fbe113267f2615e78fe47eb55ca)
    
    Change-Id: Ic5b9b36fda243c5d7d360fa03745b3e121b67b06

diff --git a/sw/inc/IDocumentMarkAccess.hxx b/sw/inc/IDocumentMarkAccess.hxx
index 93690fb2b305..20f876d16495 100644
--- a/sw/inc/IDocumentMarkAccess.hxx
+++ b/sw/inc/IDocumentMarkAccess.hxx
@@ -313,6 +313,8 @@ class IDocumentMarkAccess
         */
         virtual const_iterator_t findFirstBookmarkStartsAfter(const 
SwPosition& rPos) const =0;
 
+        /// Get the innermost bookmark that contains rPos.
+        virtual sw::mark::IMark* getBookmarkFor(const SwPosition& rPos) const 
= 0;
 
         // Fieldmarks
         /** returns a STL-like random access iterator to the begin of the 
sequence of fieldmarks.
diff --git a/sw/qa/uibase/uno/uno.cxx b/sw/qa/uibase/uno/uno.cxx
index 0a398aa87555..a91de1c2aaa1 100644
--- a/sw/qa/uibase/uno/uno.cxx
+++ b/sw/qa/uibase/uno/uno.cxx
@@ -442,6 +442,39 @@ CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetSections)
     CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), 
aTree.get_child("sections").count(""));
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetBookmark)
+{
+    // Given a document with a bookmark:
+    createSwDoc();
+    uno::Sequence<css::beans::PropertyValue> aArgs = {
+        comphelper::makePropertyValue("Bookmark", 
uno::Any(OUString("ZOTERO_BREF_1"))),
+        comphelper::makePropertyValue("BookmarkText", 
uno::Any(OUString("<p>aaa</p><p>bbb</p>"))),
+    };
+    dispatchCommand(mxComponent, ".uno:InsertBookmark", aArgs);
+
+    // When stepping into the bookmark with the cursor and getting the command 
value for
+    // .uno:Bookmark:
+    SwDoc* pDoc = getSwDoc();
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->SttEndDoc(/*bStt=*/false);
+    pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, 
/*bBasicCall=*/false);
+    tools::JsonWriter aJsonWriter;
+    std::string_view aCommand(".uno:Bookmark?namePrefix=ZOTERO_BREF_");
+    auto pXTextDocument = dynamic_cast<SwXTextDocument*>(mxComponent.get());
+    pXTextDocument->getCommandValues(aJsonWriter, aCommand);
+
+    // Then make sure we find the inserted bookmark:
+    std::unique_ptr<char[], o3tl::free_delete> 
pJSON(aJsonWriter.extractData());
+    std::stringstream aStream(pJSON.get());
+    boost::property_tree::ptree aTree;
+    boost::property_tree::read_json(aStream, aTree);
+    boost::property_tree::ptree aBookmark = aTree.get_child("bookmark");
+    // Without the accompanying fix in place, this test would have failed with:
+    // - No such node (bookmark)
+    // i.e. the returned JSON was an empty object.
+    CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_BREF_1"), 
aBookmark.get<std::string>("name"));
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/doc/docbm.cxx b/sw/source/core/doc/docbm.cxx
index 8e4d2ef72e95..7152c2a2314a 100644
--- a/sw/source/core/doc/docbm.cxx
+++ b/sw/source/core/doc/docbm.cxx
@@ -1437,6 +1437,29 @@ namespace sw::mark
         return dynamic_cast<IFieldmark*>(pFieldmark);
     }
 
+    IMark* MarkManager::getBookmarkFor(const SwPosition& rPos) const
+    {
+        auto it = std::find_if(m_vBookmarks.begin(), m_vBookmarks.end(),
+                               [&rPos](const sw::mark::MarkBase* pMark)
+                               { return pMark->IsCoveringPosition(rPos); });
+        if (it == m_vBookmarks.end())
+        {
+            return nullptr;
+        }
+        sw::mark::IMark* pBookmark = *it;
+        for (; it != m_vBookmarks.end() && (*it)->GetMarkStart() <= rPos; ++it)
+        {
+            // Find the innermost bookmark.
+            if (rPos < (*it)->GetMarkEnd()
+                && (pBookmark->GetMarkStart() < (*it)->GetMarkStart()
+                    || (*it)->GetMarkEnd() < pBookmark->GetMarkEnd()))
+            {
+                pBookmark = *it;
+            }
+        }
+        return pBookmark;
+    }
+
     void MarkManager::deleteFieldmarkAt(const SwPosition& rPos)
     {
         auto const pFieldmark = dynamic_cast<Fieldmark*>(getFieldmarkAt(rPos));
diff --git a/sw/source/core/inc/MarkManager.hxx 
b/sw/source/core/inc/MarkManager.hxx
index d362036b5464..1599996ae055 100644
--- a/sw/source/core/inc/MarkManager.hxx
+++ b/sw/source/core/inc/MarkManager.hxx
@@ -88,6 +88,7 @@ namespace sw::mark {
             virtual sal_Int32 getBookmarksCount() const override;
             virtual const_iterator_t findBookmark(const OUString& rName) const 
override;
             virtual const_iterator_t findFirstBookmarkStartsAfter(const 
SwPosition& rPos) const override;
+            virtual ::sw::mark::IMark* getBookmarkFor(const SwPosition& rPos) 
const override;
 
             // Fieldmarks
             virtual const_iterator_t getFieldmarksBegin() const override;
diff --git a/sw/source/uibase/uno/loktxdoc.cxx 
b/sw/source/uibase/uno/loktxdoc.cxx
index b4350bb430de..fd3fbbec01d5 100644
--- a/sw/source/uibase/uno/loktxdoc.cxx
+++ b/sw/source/uibase/uno/loktxdoc.cxx
@@ -230,6 +230,41 @@ void GetBookmarks(tools::JsonWriter& rJsonWriter, 
SwDocShell* pDocShell,
     }
 }
 
+/// Implements getCommandValues(".uno:Bookmark").
+///
+/// Parameters:
+///
+/// - namePrefix: bookmark name prefix to not return all bookmarks
+void GetBookmark(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell,
+                 const std::map<OUString, OUString>& rArguments)
+{
+    OUString aNamePrefix;
+    {
+        auto it = rArguments.find("namePrefix");
+        if (it != rArguments.end())
+        {
+            aNamePrefix = it->second;
+        }
+    }
+
+    IDocumentMarkAccess& rIDMA = 
*pDocShell->GetDoc()->getIDocumentMarkAccess();
+    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
+    SwPosition& rCursor = *pWrtShell->GetCursor()->GetPoint();
+    sw::mark::IMark* pBookmark = rIDMA.getBookmarkFor(rCursor);
+    tools::ScopedJsonWriterNode aBookmark = rJsonWriter.startNode("bookmark");
+    if (!pBookmark)
+    {
+        return;
+    }
+
+    if (!pBookmark->GetName().startsWith(aNamePrefix))
+    {
+        return;
+    }
+
+    rJsonWriter.put("name", pBookmark->GetName());
+}
+
 /// Implements getCommandValues(".uno:Fields").
 ///
 /// Parameters:
@@ -326,8 +361,8 @@ void GetSections(tools::JsonWriter& rJsonWriter, 
SwDocShell* pDocShell,
 bool SwXTextDocument::supportsCommand(std::u16string_view rCommand)
 {
     static const std::initializer_list<std::u16string_view> vForward
-        = { u"TextFormFields", u"TextFormField", u"SetDocumentProperties",
-            u"Bookmarks",      u"Fields",        u"Sections" };
+        = { u"TextFormFields", u"TextFormField", u"SetDocumentProperties", 
u"Bookmarks", u"Fields",
+            u"Sections",       u"Bookmark" };
 
     return std::find(vForward.begin(), vForward.end(), rCommand) != 
vForward.end();
 }
@@ -342,6 +377,7 @@ void SwXTextDocument::getCommandValues(tools::JsonWriter& 
rJsonWriter, std::stri
     static constexpr OStringLiteral aBookmarks(".uno:Bookmarks");
     static constexpr OStringLiteral aFields(".uno:Fields");
     static constexpr OStringLiteral aSections(".uno:Sections");
+    static constexpr OStringLiteral aBookmark(".uno:Bookmark");
 
     INetURLObject aParser(OUString::fromUtf8(rCommand));
     OUString aArguments = aParser.GetParam();
@@ -389,6 +425,10 @@ void SwXTextDocument::getCommandValues(tools::JsonWriter& 
rJsonWriter, std::stri
     {
         GetSections(rJsonWriter, m_pDocShell, aMap);
     }
+    else if (o3tl::starts_with(rCommand, aBookmark))
+    {
+        GetBookmark(rJsonWriter, m_pDocShell, aMap);
+    }
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit a52ecb10636f465e106e1329dfbb748a005c6e4c
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Tue Jan 10 15:40:34 2023 +0100
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Thu Feb 2 08:31:08 2023 +0100

    sw: add a new .uno:DeleteTextFormFields UNO command
    
    Users sometimes want to "unlink" their citations, which means deleting
    the fieldmarks (if fieldmarks are used to represent citations, e.g.
    with Zotero), which means keeping the field result as-is, but removing
    the field metadata and the actual field start/seprator/end characters.
    
    Do this similar to .uno:TextFormFields, which can do an update of such
    fieldmarks, i.e. add the ability to filter for a certain type and field
    command prefix. This is meant to allow removal af all fieldmark that
    belongs to one one feature, e.g. Zotero.
    
    This is similar to 7765b442e13048f857fd7ee49ced1731caee297e (sw: add a
    new .uno:TextFormFields UNO command, 2022-11-28), but this is about
    deleting (the field commands, not the result), while that was about
    updating.
    
    The same for bookmarks & refmarks are not yet supported.
    
    (cherry picked from commit c68d06dfa1498f862923eaddf3e5d247650a53d5)
    
    Change-Id: I02548b030b1822f7b36d3bc5ff9553d728f065c2

diff --git a/sw/inc/cmdid.h b/sw/inc/cmdid.h
index bec1acbe5446..befe8e26d607 100644
--- a/sw/inc/cmdid.h
+++ b/sw/inc/cmdid.h
@@ -324,6 +324,7 @@ class SwUINumRuleItem;
 #define FN_EDIT_BOOKMARK        (FN_INSERT2 + 33 )  /* Bookmark */
 #define FN_UPDATE_BOOKMARKS (FN_INSERT2 + 34)
 #define FN_UPDATE_SECTIONS (FN_INSERT2 + 35)
+#define FN_DELETE_TEXT_FORMFIELDS (FN_INSERT2 + 36)
 
 // Region: Format
 #define FN_AUTOFORMAT_APPLY     (FN_FORMAT + 1 ) /* apply autoformat options */
diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx
index 93f9351313af..8ca38e4a78d5 100644
--- a/sw/qa/uibase/shells/shells.cxx
+++ b/sw/qa/uibase/shells/shells.cxx
@@ -704,6 +704,51 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, 
testUpdateSections)
     CPPUNIT_ASSERT_EQUAL(OUString("new content"), aActualResult);
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDeleteFieldmarks)
+{
+    // Given a document with 2 fieldmarks:
+    createSwDoc();
+    {
+        uno::Sequence<css::beans::PropertyValue> aArgs = {
+            comphelper::makePropertyValue("FieldType", 
uno::Any(OUString(ODF_UNHANDLED))),
+            comphelper::makePropertyValue("FieldCommand",
+                                          uno::Any(OUString("ADDIN ZOTERO_ITEM 
old command 1"))),
+            comphelper::makePropertyValue("FieldResult", 
uno::Any(OUString("result 1"))),
+        };
+        dispatchCommand(mxComponent, ".uno:TextFormField", aArgs);
+    }
+    {
+        uno::Sequence<css::beans::PropertyValue> aArgs = {
+            comphelper::makePropertyValue("FieldType", 
uno::Any(OUString(ODF_UNHANDLED))),
+            comphelper::makePropertyValue("FieldCommand",
+                                          uno::Any(OUString("ADDIN ZOTERO_ITEM 
old command 2"))),
+            comphelper::makePropertyValue("FieldResult", 
uno::Any(OUString("result 2"))),
+        };
+        dispatchCommand(mxComponent, ".uno:TextFormField", aArgs);
+    }
+
+    // When deleting those fieldmarks:
+    uno::Sequence<css::beans::PropertyValue> aArgs
+        = { comphelper::makePropertyValue("FieldType", 
uno::Any(OUString(ODF_UNHANDLED))),
+            comphelper::makePropertyValue("FieldCommandPrefix",
+                                          uno::Any(OUString("ADDIN 
ZOTERO_ITEM"))) };
+    dispatchCommand(mxComponent, ".uno:DeleteTextFormFields", aArgs);
+
+    // Then make sure that the document doesn't contain fields anymore:
+    SwDoc* pDoc = getSwDoc();
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: 0
+    // - Actual  : 2
+    // i.e. the fieldmarks were not deleted.
+    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(0),
+                         pDoc->getIDocumentMarkAccess()->getAllMarksCount());
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->SttEndDoc(/*bStt=*/true);
+    SwCursor* pCursor = pWrtShell->GetCursor();
+    OUString aActual = pCursor->Start()->GetNode().GetTextNode()->GetText();
+    CPPUNIT_ASSERT_EQUAL(OUString("result 1result 2"), aActual);
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/sdi/_textsh.sdi b/sw/sdi/_textsh.sdi
index 90f60c6f756e..8dce73937deb 100644
--- a/sw/sdi/_textsh.sdi
+++ b/sw/sdi/_textsh.sdi
@@ -1814,6 +1814,12 @@ interface BaseText
         StateMethod = StateField ;
     ]
 
+    FN_DELETE_TEXT_FORMFIELDS
+    [
+        ExecMethod = ExecField ;
+        StateMethod = StateField ;
+    ]
+
     FN_PROTECT_FIELDS
     [
         ExecMethod = Execute ;
diff --git a/sw/sdi/swriter.sdi b/sw/sdi/swriter.sdi
index e9699450ce80..83b29b37232f 100644
--- a/sw/sdi/swriter.sdi
+++ b/sw/sdi/swriter.sdi
@@ -8330,6 +8330,20 @@ SfxVoidItem TextFormFields FN_UPDATE_TEXT_FORMFIELDS
     GroupId = SfxGroupId::Controls;
 ]
 
+SfxVoidItem DeleteTextFormFields FN_DELETE_TEXT_FORMFIELDS
+(SfxStringItem FieldType FN_PARAM_1, SfxStringItem FieldCommandPrefix 
FN_PARAM_2)
+[
+    AutoUpdate = TRUE,
+    FastCall = FALSE,
+    ReadOnlyDoc = FALSE,
+    Toggle = FALSE,
+    Container = FALSE,
+    RecordAbsolute = FALSE,
+    RecordPerSet;
+
+    GroupId = SfxGroupId::Controls;
+]
+
 SfxVoidItem UpdateTextFormField FN_UPDATE_TEXT_FORMFIELD
 (SfxStringItem FieldType FN_PARAM_1, SfxStringItem FieldCommandPrefix 
FN_PARAM_2, SfxUnoAnyItem Field FN_PARAM_3)
 [
diff --git a/sw/source/uibase/shells/textfld.cxx 
b/sw/source/uibase/shells/textfld.cxx
index 71e57ba0ae47..7e10b7a8673d 100644
--- a/sw/source/uibase/shells/textfld.cxx
+++ b/sw/source/uibase/shells/textfld.cxx
@@ -933,6 +933,60 @@ FIELD_INSERT:
         
rSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSERT_FORM_FIELD, 
nullptr);
     }
     break;
+    case FN_DELETE_TEXT_FORMFIELDS:
+    {
+        // This deletes all fieldmarks that match the provided field type & 
field command prefix.
+        OUString aFieldType;
+        const SfxStringItem* pFieldType = 
rReq.GetArg<SfxStringItem>(FN_PARAM_1);
+        if (pFieldType)
+        {
+            aFieldType = pFieldType->GetValue();
+        }
+        OUString aFieldCommandPrefix;
+        const SfxStringItem* pFieldCommandPrefix = 
rReq.GetArg<SfxStringItem>(FN_PARAM_2);
+        if (pFieldCommandPrefix)
+        {
+            aFieldCommandPrefix = pFieldCommandPrefix->GetValue();
+        }
+        
rSh.GetDoc()->GetIDocumentUndoRedo().StartUndo(SwUndoId::INSERT_FORM_FIELD, 
nullptr);
+        rSh.StartAction();
+
+        IDocumentMarkAccess* pMarkAccess = 
rSh.GetDoc()->getIDocumentMarkAccess();
+        std::vector<sw::mark::IMark*> aRemovals;
+        for (auto it = pMarkAccess->getFieldmarksBegin(); it != 
pMarkAccess->getFieldmarksEnd(); ++it)
+        {
+            auto pFieldmark = dynamic_cast<sw::mark::IFieldmark*>(*it);
+            assert(pFieldmark);
+            if (pFieldmark->GetFieldname() != aFieldType)
+            {
+                continue;
+            }
+
+            auto itParam = pFieldmark->GetParameters()->find(ODF_CODE_PARAM);
+            if (itParam == pFieldmark->GetParameters()->end())
+            {
+                continue;
+            }
+
+            OUString aCommand;
+            itParam->second >>= aCommand;
+            if (!aCommand.startsWith(aFieldCommandPrefix))
+            {
+                continue;
+            }
+
+            aRemovals.push_back(pFieldmark);
+        }
+
+        for (const auto& pMark : aRemovals)
+        {
+            pMarkAccess->deleteMark(pMark);
+        }
+
+        rSh.EndAction();
+        
rSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSERT_FORM_FIELD, 
nullptr);
+    }
+    break;
     case FN_PGNUMBER_WIZARD:
     {
         SwAbstractDialogFactory* pFact = SwAbstractDialogFactory::Create();

Reply via email to