sw/qa/uibase/shells/data/nearby-changes.fodt | 56 ++++++++ sw/qa/uibase/shells/shells.cxx | 93 ++++++++++++++ sw/source/uibase/uno/loktxdoc.cxx | 179 ++++++++++++++++++++------- 3 files changed, 288 insertions(+), 40 deletions(-)
New commits: commit ba0dc60a2ac54887c9cca9b7cb2a0755a7ccce54 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Tue Jul 1 12:26:14 2025 +0500 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Tue Jul 1 14:12:00 2025 +0200 LOK Extract API: normalize textBefore / textAfter in redline extraction We return textBefore / textAfter context for each redline; but for it to make sense, it needs to reflect the text in the state when the redline was created. This change makes sure that for each processed redline, the document is set to the visual state present at the moment current for this redline: all previous changes are shown as if accepted; all future changes are not shown as if rejected. Change-Id: Ie5b9a971c2c98642b49212fbcd57df46a8daaccc Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187221 Tested-by: Jenkins Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> diff --git a/sw/qa/uibase/shells/data/nearby-changes.fodt b/sw/qa/uibase/shells/data/nearby-changes.fodt new file mode 100644 index 000000000000..6996965c63eb --- /dev/null +++ b/sw/qa/uibase/shells/data/nearby-changes.fodt @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:automatic-styles> + <style:style style:name="T1" style:family="text"> + <style:text-properties fo:font-weight="bold"/> + </style:style> + </office:automatic-styles> + <office:body> + <office:text> + <text:tracked-changes text:track-changes="false"> + <text:changed-region xml:id="ct1" text:id="ct1"> + <text:deletion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-07-01T12:00:00</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct2" text:id="ct2"> + <text:insertion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-07-01T12:30:00</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + <text:changed-region xml:id="ct3" text:id="ct3"> + <text:format-change> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-07-01T13:00:00</dc:date> + </office:change-info> + </text:format-change> + </text:changed-region> + <text:changed-region xml:id="ct4" text:id="ct4"> + <text:deletion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-07-01T13:30:00</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct5" text:id="ct5"> + <text:insertion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-07-01T14:00:00</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + </text:tracked-changes> + <text:p>a <text:change-start text:change-id="ct1"/>del1<text:change-end text:change-id="ct1"/> b <text:change-start text:change-id="ct2"/>ins2<text:change-end text:change-id="ct2"/> c <text:change-start text:change-id="ct3"/><text:span text:style-name="T1">fmt3</text:span><text:change-end text:change-id="ct3"/> d <text:change-start text:change-id="ct4"/>del4<text:change-end text:change-id="ct4"/> e <text:change-start text:change-id="ct5"/>ins5<text:change-end text:change-id="ct5"/> f</text:p> + </office:text> + </office:body> +</office:document> \ No newline at end of file diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx index 22da30260421..117928917de9 100644 --- a/sw/qa/uibase/shells/shells.cxx +++ b/sw/qa/uibase/shells/shells.cxx @@ -1024,6 +1024,99 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDocumentStructureExtractRedlines) CPPUNIT_ASSERT(bool(it == docStructure.end())); } +CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDocumentStructureExtractRedlines_textBeforeAfter) +{ + // The document here has a series of insert/delete/format changes having different times. + // This test checks, that for each extracted data, we produce textBefore / textAfter in proper + // form: all newer changes are pretended to never had happened / as if rejected; other changes + // are shown as if accepted. This produces the correct context for each current change. + createSwDoc("nearby-changes.fodt"); + + boost::property_tree::ptree tree; + + // extract using the context length enough to cover the whole document text + tools::JsonWriter aJsonWriter; + std::string_view aCommand(".uno:ExtractDocumentStructure?filter=trackchanges,contextLen:100"); + getSwTextDoc()->getCommandValues(aJsonWriter, aCommand); + + std::stringstream aStream(std::string(aJsonWriter.finishAndGetAsOString())); + boost::property_tree::read_json(aStream, tree); + + boost::property_tree::ptree docStructure = tree.get_child("DocStructure"); + CPPUNIT_ASSERT_EQUAL(size_t(5), docStructure.size()); + auto it = docStructure.begin(); + + // The text before covers older changes. It includes text that was inserted earlier; it + // excludes text that was deleted earlier. + // The text after covers newer changes. It includes text that was deleted later; it + // excludes text that was added later. + // The formatting changes don't affect text. + // The whole text looks like "a del1 b ins2 c fmt3 d del4 e ins5 f" + // where <delN>, <insN>, <fmtN> are successive deletions/insertions/formattings. + + { + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.0"s, name); + CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-07-01T12:00:00"s, change.get<std::string>("dateTime")); + CPPUNIT_ASSERT_EQUAL("del1"s, change.get<std::string>("textChanged")); + CPPUNIT_ASSERT_EQUAL("a "s, change.get<std::string>("textBefore")); + CPPUNIT_ASSERT_EQUAL(" b c fmt3 d del4 e f"s, change.get<std::string>("textAfter")); + ++it; + } + + { + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.1"s, name); + CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-07-01T12:30:00"s, change.get<std::string>("dateTime")); + CPPUNIT_ASSERT_EQUAL("ins2"s, change.get<std::string>("textChanged")); + CPPUNIT_ASSERT_EQUAL("a b "s, change.get<std::string>("textBefore")); + CPPUNIT_ASSERT_EQUAL(" c fmt3 d del4 e f"s, change.get<std::string>("textAfter")); + ++it; + } + + { + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.2"s, name); + CPPUNIT_ASSERT_EQUAL("Format"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-07-01T13:00:00"s, change.get<std::string>("dateTime")); + CPPUNIT_ASSERT_EQUAL("fmt3"s, change.get<std::string>("textChanged")); + CPPUNIT_ASSERT_EQUAL("a b ins2 c "s, change.get<std::string>("textBefore")); + CPPUNIT_ASSERT_EQUAL(" d del4 e f"s, change.get<std::string>("textAfter")); + ++it; + } + + { + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.3"s, name); + CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-07-01T13:30:00"s, change.get<std::string>("dateTime")); + CPPUNIT_ASSERT_EQUAL("del4"s, change.get<std::string>("textChanged")); + CPPUNIT_ASSERT_EQUAL("a b ins2 c fmt3 d "s, change.get<std::string>("textBefore")); + CPPUNIT_ASSERT_EQUAL(" e f"s, change.get<std::string>("textAfter")); + ++it; + } + + { + CPPUNIT_ASSERT(it != docStructure.end()); + const auto & [ name, change ] = *it; + CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.4"s, name); + CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL("2025-07-01T14:00:00"s, change.get<std::string>("dateTime")); + CPPUNIT_ASSERT_EQUAL("ins5"s, change.get<std::string>("textChanged")); + CPPUNIT_ASSERT_EQUAL("a b ins2 c fmt3 d e "s, change.get<std::string>("textBefore")); + CPPUNIT_ASSERT_EQUAL(" f"s, change.get<std::string>("textAfter")); + ++it; + } + + CPPUNIT_ASSERT(bool(it == docStructure.end())); +} + CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateRefmarks) { // Given a document with two refmarks, one is not interesting the other is a citation: diff --git a/sw/source/uibase/uno/loktxdoc.cxx b/sw/source/uibase/uno/loktxdoc.cxx index e638a8142c60..7701e874b9f8 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -33,12 +33,15 @@ #include <sfx2/lokhelper.hxx> #include <IDocumentMarkAccess.hxx> +#include <IDocumentRedlineAccess.hxx> #include <doc.hxx> #include <docsh.hxx> #include <fmtrfmrk.hxx> #include <wrtsh.hxx> #include <txtrfmrk.hxx> #include <ndtxt.hxx> +#include <redline.hxx> +#include <unoredline.hxx> #include <unoredlines.hxx> #include <unoport.hxx> @@ -69,7 +72,8 @@ namespace class PropertyExtractor { public: - PropertyExtractor(uno::Reference<beans::XPropertySet>& xProperties, tools::JsonWriter& rWriter) + PropertyExtractor(const uno::Reference<beans::XPropertySet>& xProperties, + tools::JsonWriter& rWriter) : m_xProperties(xProperties) , m_rWriter(rWriter) { @@ -91,7 +95,7 @@ public: } private: - uno::Reference<beans::XPropertySet>& m_xProperties; + uno::Reference<beans::XPropertySet> m_xProperties; tools::JsonWriter& m_rWriter; }; @@ -854,8 +858,97 @@ void GetDocStructureDocProps(tools::JsonWriter& rJsonWriter, const SwDocShell* p } } +// This class temporarily hides / shows redlines in the document, based on timestamp: when a redline +// is newer than the date, it is "hidden" (the text looks as if that redline were rejected); and +// otherwise the redline is "shown" (the text looks as if the redline is accepted). This allows to +// obtain textBefore / textAfter context attributes for a given redline as it was when the redline +// was created. The state of redlines is restored to original in dtor. +class HideNewerShowOlder +{ +public: + HideNewerShowOlder(DateTime limit, const SwRedlineTable& rTable) + : m_rTable(rTable) + , m_aRedlineShowStateRestore(collectRestoreData(m_rTable)) + { + for (auto pRedline : m_rTable) + { + const auto& data = pRedline->GetRedlineData(); + if (data.GetType() != RedlineType::Insert && data.GetType() != RedlineType::Delete) + continue; + bool hide; + if (limit < data.GetTimeStamp()) + hide = data.GetType() == RedlineType::Insert; + else // not later + hide = data.GetType() == RedlineType::Delete; + + if (hide) + Hide(pRedline, m_rTable); + else + Show(pRedline, m_rTable); + } + } + ~HideNewerShowOlder() + { + // I assume, that only the redlines explicitly handled in ctor would change their visible + // state; so here, only Insert / Delete redlines will be handled. + for (auto[pRedline, visible] : m_aRedlineShowStateRestore) + { + if (visible) + Show(pRedline, m_rTable); + else + Hide(pRedline, m_rTable); + } + } + +private: + static std::unordered_map<SwRangeRedline*, bool> + collectRestoreData(const SwRedlineTable& rTable) + { + std::unordered_map<SwRangeRedline*, bool> aRedlineShowStateRestore; + for (auto pRedline : rTable) + aRedlineShowStateRestore[pRedline] = pRedline->IsVisible(); + return aRedlineShowStateRestore; + } + static void Show(SwRangeRedline* pRedline, const SwRedlineTable& rTable) + { + if (pRedline->IsVisible()) + return; + switch (pRedline->GetType()) + { + case RedlineType::Insert: + case RedlineType::Delete: + pRedline->Show(0, rTable.GetPos(pRedline), true); + pRedline->Show(1, rTable.GetPos(pRedline), true); + break; + default: + assert(!"Trying to show a redline that is not expected to change visibility here"); + } + } + static void Hide(SwRangeRedline* pRedline, const SwRedlineTable& rTable) + { + if (!pRedline->IsVisible()) + return; + switch (pRedline->GetType()) + { + case RedlineType::Insert: + pRedline->ShowOriginal(0, rTable.GetPos(pRedline)); + pRedline->ShowOriginal(1, rTable.GetPos(pRedline)); + break; + case RedlineType::Delete: + pRedline->Hide(0, rTable.GetPos(pRedline)); + pRedline->Hide(1, rTable.GetPos(pRedline)); + break; + default: + assert(!"Trying to hide a redline that is not expected to change visibility here"); + } + } + + const SwRedlineTable& m_rTable; + std::unordered_map<SwRangeRedline*, bool> m_aRedlineShowStateRestore; +}; + /// Implements getCommandValues(".uno:ExtractDocumentStructures") for redlines -void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const SwDocShell* pDocShell, +void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, std::u16string_view filterArguments) { // filter arguments are separated from the filter name by comma, and are name:value pairs @@ -878,16 +971,17 @@ void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const SwDocShel // else unknown filter argument (maybe from a newer API?) - ignore } - auto xRedlinesEnum = pDocShell->GetBaseModel()->getRedlines()->createEnumeration(); - for (sal_Int32 i = 0; xRedlinesEnum->hasMoreElements(); ++i) + SwDoc& rDoc = *pDocShell->GetDoc(); + const SwRedlineTable& rTable = rDoc.getIDocumentRedlineAccess().GetRedlineTable(); + + for (size_t i = 0; i < rTable.size(); ++i) { - auto xRedlineProperties = xRedlinesEnum->nextElement().query<beans::XPropertySet>(); - assert(xRedlineProperties); + rtl::Reference<SwXRedline> pSwXRedline(new SwXRedline(*rTable[i])); auto TrackChangesNode = rJsonWriter.startNode(Concat2View("TrackChanges.ByIndex." + OString::number(i))); - PropertyExtractor extractor{ xRedlineProperties, rJsonWriter }; + PropertyExtractor extractor{ pSwXRedline, rJsonWriter }; extractor.extract<OUString>(UNO_NAME_REDLINE_TYPE, "type"); extractor.extract<css::util::DateTime>(UNO_NAME_REDLINE_DATE_TIME, "dateTime"); @@ -895,39 +989,44 @@ void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const SwDocShel extractor.extract<OUString>(UNO_NAME_REDLINE_DESCRIPTION, "description"); extractor.extract<OUString>(UNO_NAME_REDLINE_COMMENT, "comment"); - auto xStart = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_START) - .query<css::text::XTextRange>(); - auto xEnd = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_END) - .query<css::text::XTextRange>(); - if (xStart) - { - auto xCursor = xStart->getText()->createTextCursorByRange(xStart); - xCursor->goLeft(nContextLen, /*bExpand*/ true); - rJsonWriter.put("textBefore", xCursor->getString()); - } - if (xEnd) - { - auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd); - xCursor->goRight(nContextLen, /*bExpand*/ true); - rJsonWriter.put("textAfter", xCursor->getString()); - } - OUString changeText; - if (xStart && xEnd) - { - // Read the added / formatted text from the main XText - auto xCursor = xStart->getText()->createTextCursorByRange(xStart); - xCursor->gotoRange(xEnd, /*bExpand*/ true); - changeText = xCursor->getString(); - } - if (changeText.isEmpty()) { - // It is unlikely that we get here: the change text will be obtained above, - // even for deletion change - if (auto xRedlineText = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_TEXT) - .query<css::text::XText>()) - changeText = xRedlineText->getString(); + // Set the text into a state according to current redline's timestamp: all older changes + // are shows as if accepted, all newer are shown as if rejected. + HideNewerShowOlder prepare(pSwXRedline->GetRedline()->GetTimeStamp(), rTable); + auto xStart = pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_START) + .query<css::text::XTextRange>(); + auto xEnd = pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_END) + .query<css::text::XTextRange>(); + if (xStart) + { + auto xCursor = xStart->getText()->createTextCursorByRange(xStart); + xCursor->goLeft(nContextLen, /*bExpand*/ true); + rJsonWriter.put("textBefore", xCursor->getString()); + } + if (xEnd) + { + auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd); + xCursor->goRight(nContextLen, /*bExpand*/ true); + rJsonWriter.put("textAfter", xCursor->getString()); + } + OUString changeText; + if (xStart && xEnd) + { + // Read the added / formatted text from the main XText + auto xCursor = xStart->getText()->createTextCursorByRange(xStart); + xCursor->gotoRange(xEnd, /*bExpand*/ true); + changeText = xCursor->getString(); + } + if (changeText.isEmpty()) + { + // It is unlikely that we get here: the change text will be obtained above, + // even for deletion change + if (auto xRedlineText = pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_TEXT) + .query<css::text::XText>()) + changeText = xRedlineText->getString(); + } + rJsonWriter.put("textChanged", changeText); // write unconditionally } - rJsonWriter.put("textChanged", changeText); // write unconditionally // UNO_NAME_REDLINE_IDENTIFIER: OUString (the value of a pointer, not persistent) // UNO_NAME_REDLINE_MOVED_ID: sal_uInt32; 0 == not moved, 1 == moved, but don't have its pair, 2+ == unique ID // UNO_NAME_REDLINE_SUCCESSOR_DATA: uno::Sequence<beans::PropertyValue> @@ -941,7 +1040,7 @@ void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const SwDocShel /// Parameters: /// /// - filter: To filter what document structure types to extract -void GetDocStructure(tools::JsonWriter& rJsonWriter, const SwDocShell* pDocShell, +void GetDocStructure(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, const std::map<OUString, OUString>& rArguments) { auto commentsNode = rJsonWriter.startNode("DocStructure");