Batch observations and completions of text manipulations
https://bugs.webkit.org/show_bug.cgi?id=208406

Reviewed by Wenson Hsieh.

Source/WebCore:

This patch updates TextManipulationController to notify and replace multiple paragraphs at once.

To allow some form of parallelism for the client application to process items while WebContent's main thread
is finding paragraphs, we notify the client every 128 items.

Tests: TestWebKitAPI.TextManipulation.StartTextManipulationSupportsLegacyDelegateCallback: Added.
       TestWebKitAPI.TextManipulation.LegacyCompleteTextManipulationReplaceSimpleSingleParagraph: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationReplaceMultipleSimpleParagraphsAtOnce: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationShouldBatchItemCallback: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationFailWhenItemIdentifierIsDuplicated: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationCanHandleSubsetOfItemsToFail: Added.

* editing/TextManipulationController.cpp:
(WebCore::TextManipulationController::startObservingParagraphs):
(WebCore::TextManipulationController::scheduleObservartionUpdate):
(WebCore::TextManipulationController::addItem): Added.
(WebCore::TextManipulationController::flushPendingItemsForCallback): Added.
(WebCore::TextManipulationController::completeManipulation): Apply each ManipulationItem changes and return
an array for ManipulationFailure for those that failed.
(WebCore::TextManipulationController::replace):
* editing/TextManipulationController.h:
(WebCore::TextManipulationController::ManipulationItem): Added.
(WebCore::TextManipulationController::ManipulationFailureType): Renamed from ManipulationResult.
(WebCore::TextManipulationController::ManipulationFailure): Added.
(WebCore::TextManipulationController::ManipulationItemData): Renamed from ManipulationItem. 
(WebCore::TextManipulationController::ManipulationItem::encode const): Added.
(WebCore::TextManipulationController::ManipulationItem::decode): Added.
(WebCore::TextManipulationController::ManipulationFailure::encode const): Added.
(WebCore::TextManipulationController::ManipulationFailure::decode): Added.

Source/WebKit:

Update WKWebView's SPI for text manipulations to batch observations and replacements of multiple paragraphs
at once instead of observing and replacing one paragraph at a time.

_WKTextManipulationDelegate now has a new callback, didFindTextManipulationItems, which gets an array of
_WKTextManipulationItem instead of a single _WKTextManipulationItem like didFindTextManipulationItem,
and WKWebView's _completeTextManipulationForItems replaces multiple paragraphs at once, and gives an array
of NSError for each _WKTextManipulationItem that failed to replace the respective content.

Tests: TestWebKitAPI.TextManipulation.StartTextManipulationSupportsLegacyDelegateCallback: Added.
       TestWebKitAPI.TextManipulation.LegacyCompleteTextManipulationReplaceSimpleSingleParagraph: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationReplaceMultipleSimpleParagraphsAtOnce: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationShouldBatchItemCallback: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationFailWhenItemIdentifierIsDuplicated: Added.
       TestWebKitAPI.TextManipulation.CompleteTextManipulationCanHandleSubsetOfItemsToFail: Added.

* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _startTextManipulationsWithConfiguration:completion:]):
(coreTextManipulationItemIdentifierFromString): Added.
(coreTextManipulationTokenIdentifierFromString): Added.
(-[WKWebView _completeTextManipulation:completion:]): Updated to work with the new TextManipulationController
interface.
(makeFailureSetForAllTextManipulationItems): Added.
(wkTextManipulationErrors): Added.
(-[WKWebView _completeTextManipulationForItems:completion:]): Added. This is the new SPI.
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/API/Cocoa/_WKTextManipulationDelegate.h:
* UIProcess/API/Cocoa/_WKTextManipulationItem.h:
(_WKTextManipulationItemErrorDomain): Added.
(_WKTextManipulationItemErrorCode): Added.
(_WKTextManipulationItemErrorItemKey): Added.
* UIProcess/API/Cocoa/_WKTextManipulationItem.mm:
* UIProcess/WebPageProxy.cpp:
* UIProcess/WebPageProxy.h:
* UIProcess/WebPageProxy.messages.in:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::startTextManipulations):
(WebKit::WebPage::completeTextManipulation): Updated per TextManipulationController interface change.
To avoid having to duplicate the code to create a vector of ManipulationFailure, this function's completion
handler has a boolean indicating that all replacements had failed.
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:

Tools:

Updated the existing tests to use new APIs, and added new tests for batching finding & replacing
multiple paragraphs at once.

* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
(-[TextManipulationDelegate _webView:didFindTextManipulationItems:]):
(-[TextManipulationDelegate items]):
(-[LegacyTextManipulationDelegate init]):
(-[LegacyTextManipulationDelegate _webView:didFindTextManipulationItem:]):
(TextManipulation.StartTextManipulationSupportsLegacyDelegateCallback): Added.
(TextManipulation.LegacyCompleteTextManipulationReplaceSimpleSingleParagraph): Added.
(TextManipulation.CompleteTextManipulationReplaceMultipleSimpleParagraphsAtOnce): Added.
(TextManipulation.CompleteTextManipulationShouldBatchItemCallback): Added.
(TextManipulation.CompleteTextManipulationFailWhenItemIdentifierIsDuplicated): Added.
(TextManipulation.CompleteTextManipulationCanHandleSubsetOfItemsToFail): Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@257830 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/editing/TextManipulationController.cpp b/Source/WebCore/editing/TextManipulationController.cpp
index d05af44..140a438 100644
--- a/Source/WebCore/editing/TextManipulationController.cpp
+++ b/Source/WebCore/editing/TextManipulationController.cpp
@@ -136,6 +136,7 @@
     VisiblePosition end = lastPositionInNode(m_document.get());
 
     observeParagraphs(start, end);
+    flushPendingItemsForCallback();
 }
 
 void TextManipulationController::observeParagraphs(VisiblePosition& start, VisiblePosition& end)
@@ -258,30 +259,58 @@
 
             controller->observeParagraphs(start, end);
         }
+        controller->flushPendingItemsForCallback();
     });
 }
 
 void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens)
 {
+    const unsigned itemCallbackBatchingSize = 128;
+
     ASSERT(m_document);
-    auto result = m_items.add(m_itemIdentifier.generate(), ManipulationItem { startOfParagraph, endOfParagraph, WTFMove(tokens) });
-    m_callback(*m_document, result.iterator->key, result.iterator->value.tokens);
+    auto newID = m_itemIdentifier.generate();
+    m_pendingItemsForCallback.append(ManipulationItem {
+        newID,
+        tokens.map([](auto& token) { return token; })
+    });
+    m_items.add(newID, ManipulationItemData { startOfParagraph, endOfParagraph, WTFMove(tokens) });
+
+    if (m_pendingItemsForCallback.size() >= itemCallbackBatchingSize)
+        flushPendingItemsForCallback();
 }
 
-auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
+void TextManipulationController::flushPendingItemsForCallback()
 {
-    if (!itemIdentifier)
-        return ManipulationResult::InvalidItem;
+    m_callback(*m_document, m_pendingItemsForCallback);
+    m_pendingItemsForCallback.clear();
+}
 
-    auto itemIterator = m_items.find(itemIdentifier);
-    if (itemIterator == m_items.end())
-        return ManipulationResult::InvalidItem;
+auto TextManipulationController::completeManipulation(const Vector<WebCore::TextManipulationController::ManipulationItem>& completionItems) -> Vector<ManipulationFailure>
+{
+    Vector<ManipulationFailure> failures;
+    for (unsigned i = 0; i < completionItems.size(); ++i) {
+        auto& itemToComplete = completionItems[i];
+        auto identifier = itemToComplete.identifier;
+        if (!identifier) {
+            failures.append(ManipulationFailure { identifier, i, ManipulationFailureType::InvalidItem });
+            continue;
+        }
 
-    ManipulationItem item;
-    std::exchange(item, itemIterator->value);
-    m_items.remove(itemIterator);
+        auto itemDataIterator = m_items.find(identifier);
+        if (itemDataIterator == m_items.end()) {
+            failures.append(ManipulationFailure { identifier, i, ManipulationFailureType::InvalidItem });
+            continue;
+        }
 
-    return replace(item, replacementTokens);
+        ManipulationItemData itemData;
+        std::exchange(itemData, itemDataIterator->value);
+        m_items.remove(itemDataIterator);
+
+        auto failureOrNullopt = replace(itemData, itemToComplete.tokens);
+        if (failureOrNullopt)
+            failures.append(ManipulationFailure { identifier, i, *failureOrNullopt });
+    }
+    return failures;
 }
 
 struct TokenExchangeData {
@@ -301,10 +330,10 @@
     Ref<Node> child;
 };
 
-auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
+auto TextManipulationController::replace(const ManipulationItemData& item, const Vector<ManipulationToken>& replacementTokens) -> Optional<ManipulationFailureType>
 {
     if (item.start.isOrphan() || item.end.isOrphan())
-        return ManipulationResult::ContentChanged;
+        return ManipulationFailureType::ContentChanged;
 
     TextIterator iterator { item.start, item.end };
     size_t currentTokenIndex = 0;
@@ -314,10 +343,10 @@
     while (!iterator.atEnd()) {
         auto string = iterator.text().toString();
         if (currentTokenIndex >= item.tokens.size())
-            return ManipulationResult::ContentChanged;
+            return ManipulationFailureType::ContentChanged;
         auto& currentToken = item.tokens[currentTokenIndex];
         if (iterator.text() != currentToken.content)
-            return ManipulationResult::ContentChanged;
+            return ManipulationFailureType::ContentChanged;
 
         auto currentNode = makeRefPtr(iterator.node());
         tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { currentNode.copyRef(), currentToken.content, currentToken.isExcluded });
@@ -358,17 +387,17 @@
     for (auto& newToken : replacementTokens) {
         auto it = tokenExchangeMap.find(newToken.identifier);
         if (it == tokenExchangeMap.end())
-            return ManipulationResult::InvalidToken;
+            return ManipulationFailureType::InvalidToken;
 
         auto& exchangeData = it->value;
 
         RefPtr<Node> contentNode;
         if (exchangeData.isExcluded) {
             if (exchangeData.isConsumed)
-                return ManipulationResult::ExclusionViolation;
+                return ManipulationFailureType::ExclusionViolation;
             exchangeData.isConsumed = true;
             if (!newToken.content.isNull() && newToken.content != exchangeData.originalContent)
-                return ManipulationResult::ExclusionViolation;
+                return ManipulationFailureType::ExclusionViolation;
             contentNode = Text::create(commonAncestor->document(), exchangeData.originalContent);
         } else
             contentNode = Text::create(commonAncestor->document(), newToken.content);
@@ -423,7 +452,7 @@
             m_manipulatedElements.add(downcast<Element>(insertion.child.get()));
     }
 
-    return ManipulationResult::Success;
+    return WTF::nullopt;
 }
 
 } // namespace WebCore