Changeset 238438 in webkit


Ignore:
Timestamp:
Nov 21, 2018 9:03:59 PM (5 years ago)
Author:
Wenson Hsieh
Message:

[Cocoa] [WebKit2] Add support for replacing find-in-page text matches
https://bugs.webkit.org/show_bug.cgi?id=191786
<rdar://problem/45813871>

Reviewed by Ryosuke Niwa.

Source/WebCore:

Add support for replacing Find-in-Page matches. See below for details. Covered by new layout tests as well as a
new API test.

Tests: editing/find/find-and-replace-adjacent-words.html

editing/find/find-and-replace-at-editing-boundary.html
editing/find/find-and-replace-basic.html
editing/find/find-and-replace-in-subframes.html
editing/find/find-and-replace-no-matches.html
editing/find/find-and-replace-noneditable-matches.html
editing/find/find-and-replace-replacement-text-input-events.html

API test: WebKit.FindAndReplace

  • page/Page.cpp:

(WebCore::replaceRanges):
(WebCore::Page::replaceRangesWithText):

Add a helper that, given a list of Ranges, replaces each range with the given text. To do this, we first map
each Range to editing offsets within the topmost editable root for each Range. This results in a map of editable
root to list of editing offsets we need to replace. To apply the replacements, for each editable root in the
map, we iterate over each replacement range (i.e. an offset and length), set the current selection to contain
that replacement range, and use Editor::replaceSelectionWithText. To prevent prior text replacements from
clobbering the offsets of latter text replacement ranges, we also iterate backwards through text replacement
ranges when performing each replacement.

Likewise, we also apply text replacement to each editing container in backwards order: for nodes in the same
frame, we compare their position in the document, and for nodes in different frames, we instead compare their
frames in frame tree traversal order.

We map Ranges to editing offsets and back when performing text replacement because each text replacement may
split or merge text nodes, which causes adjacent Ranges to shrink or extend while replacing text. In an earlier
attempt to implement this, I simply iterated over each Range to replace and carried out text replacement for
each Range. This led to incorrect behavior in some cases, such as replacing adjacent matches. Thus, by computing
the set of text replacement offsets prior to replacing any text, we're able to target the correct ranges for
replacement.

(WebCore::Page::replaceSelectionWithText):

Add a helper method on Page to replace the current selection with some text. This simply calls out to
Editor::replaceSelectionWithText.

  • page/Page.h:

Source/WebCore/PAL:

Add -replaceMatches:withString:inSelectionOnly:resultCollector:.

  • pal/spi/mac/NSTextFinderSPI.h:

Source/WebKit:

  • UIProcess/API/Cocoa/WKWebView.mm:

(-[WKWebView replaceMatches:withString:inSelectionOnly:resultCollector:]):

  • UIProcess/WebPageProxy.cpp:

(WebKit::WebPageProxy::replaceMatches):

  • UIProcess/WebPageProxy.h:
  • UIProcess/mac/WKTextFinderClient.mm:

(-[WKTextFinderClient replaceMatches:withString:inSelectionOnly:resultCollector:]):

Implement this method to opt in to "Replace…" UI on macOS in the find bar. In this API, we're given a list of
matches to replace. We propagate the indices of each match to the web process, where FindController maps them to
corresponding replacement ranges. Currently, the given list of matches is only ever a list containing the first
match, or a list containing all matches.

  • WebProcess/InjectedBundle/API/c/WKBundlePage.cpp:

(WKBundlePageFindStringMatches):
(WKBundlePageReplaceStringMatches):

  • WebProcess/InjectedBundle/API/c/WKBundlePage.h:
  • WebProcess/WebCoreSupport/WebEditorClient.cpp:
  • WebProcess/WebPage/FindController.cpp:

(WebKit::FindController::replaceMatches):

Map match indices to Ranges, and then call into WebCore::Page to do the heavy lifting (see WebCore ChangeLog for
more details). Additionally add a hard find-and-replace limit here to prevent the web process from spinning
indefinitely if there are an enormous number of find matches.

  • WebProcess/WebPage/FindController.h:
  • WebProcess/WebPage/WebPage.cpp:

(WebKit::WebPage::findStringMatchesFromInjectedBundle):
(WebKit::WebPage::replaceStringMatchesFromInjectedBundle):

Add helpers to exercise find and replace in WebKit2.

(WebKit::WebPage::replaceMatches):

  • WebProcess/WebPage/WebPage.h:
  • WebProcess/WebPage/WebPage.messages.in:

Tools:

  • MiniBrowser/mac/WK2BrowserWindowController.m:

(-[WK2BrowserWindowController setFindBarView:]):

Fix a bug in MiniBrowser that prevents AppKit from displaying the "All" button in the find bar after checking
the "Replace" option.

  • TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm:

Add an API test to exercise find-and-replace API using WKWebView.

(replaceMatches):
(TEST):

  • WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
  • WebKitTestRunner/InjectedBundle/TestRunner.cpp:

(WTR::findOptionsFromArray):
(WTR::TestRunner::findString):
(WTR::TestRunner::findStringMatchesInPage):
(WTR::TestRunner::replaceFindMatchesAtIndices):

Add TestRunner hooks to simulate find-in-page and replace.

  • WebKitTestRunner/InjectedBundle/TestRunner.h:

LayoutTests:

Introduce a LayoutTests/editing/find directory to contain tests around FindController, and add 7 new layout
tests. These are currently enabled only for WebKit2 on macOS and iOS.

  • TestExpectations:
  • editing/find/find-and-replace-adjacent-words-expected.txt: Added.
  • editing/find/find-and-replace-adjacent-words.html: Added.

Test find-and-replace with adjacent words.

  • editing/find/find-and-replace-at-editing-boundary-expected.txt: Added.
  • editing/find/find-and-replace-at-editing-boundary.html: Added.

Test find-and-replace when one of the find matches straddles an editing boundary. In this case, we verify that
the replacement does not occur, since only part of the word would be replaced.

  • editing/find/find-and-replace-basic-expected.txt: Added.
  • editing/find/find-and-replace-basic.html: Added.

Add a basic test that exercises a single text replacement, and "replace all".

  • editing/find/find-and-replace-in-subframes-expected.txt: Added.
  • editing/find/find-and-replace-in-subframes.html: Added.

Test find-and-replace when some of the matches are in editable content in subframes. This test additionally
contains matches in shadow content (in this case, text fields) within both the main document and the subframe,
and verifies that text replacement reaches these elements as well.

  • editing/find/find-and-replace-no-matches-expected.txt: Added.
  • editing/find/find-and-replace-no-matches.html: Added.

Test find-and-replace when no replacement matches are specified. In this case, we fall back to inserting the
replacement text at the current selection.

  • editing/find/find-and-replace-noneditable-matches-expected.txt: Added.
  • editing/find/find-and-replace-noneditable-matches.html: Added.

Test find-and-replace when some of the matches to replace are noneditable, others are editable, and others are
editable but are nested within noneditable elements (i.e. contenteditable=false). In this case, "replace all"
should still replace all fully editable matches.

  • editing/find/find-and-replace-replacement-text-input-events-expected.txt: Added.
  • editing/find/find-and-replace-replacement-text-input-events.html: Added.

Tests that find-and-replace emits input events of inputType "insertReplacementText", except when inserting
replacement text at a caret selection.

  • platform/ios-wk2/TestExpectations:
  • platform/mac-wk2/TestExpectations:
Location:
trunk
Files:
15 added
27 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r238430 r238438  
     12018-11-21  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [Cocoa] [WebKit2] Add support for replacing find-in-page text matches
     4        https://bugs.webkit.org/show_bug.cgi?id=191786
     5        <rdar://problem/45813871>
     6
     7        Reviewed by Ryosuke Niwa.
     8
     9        Introduce a `LayoutTests/editing/find` directory to contain tests around `FindController`, and add 7 new layout
     10        tests. These are currently enabled only for WebKit2 on macOS and iOS.
     11
     12        * TestExpectations:
     13        * editing/find/find-and-replace-adjacent-words-expected.txt: Added.
     14        * editing/find/find-and-replace-adjacent-words.html: Added.
     15
     16        Test find-and-replace with adjacent words.
     17
     18        * editing/find/find-and-replace-at-editing-boundary-expected.txt: Added.
     19        * editing/find/find-and-replace-at-editing-boundary.html: Added.
     20
     21        Test find-and-replace when one of the find matches straddles an editing boundary. In this case, we verify that
     22        the replacement does not occur, since only part of the word would be replaced.
     23
     24        * editing/find/find-and-replace-basic-expected.txt: Added.
     25        * editing/find/find-and-replace-basic.html: Added.
     26
     27        Add a basic test that exercises a single text replacement, and "replace all".
     28
     29        * editing/find/find-and-replace-in-subframes-expected.txt: Added.
     30        * editing/find/find-and-replace-in-subframes.html: Added.
     31
     32        Test find-and-replace when some of the matches are in editable content in subframes. This test additionally
     33        contains matches in shadow content (in this case, text fields) within both the main document and the subframe,
     34        and verifies that text replacement reaches these elements as well.
     35
     36        * editing/find/find-and-replace-no-matches-expected.txt: Added.
     37        * editing/find/find-and-replace-no-matches.html: Added.
     38
     39        Test find-and-replace when no replacement matches are specified. In this case, we fall back to inserting the
     40        replacement text at the current selection.
     41
     42        * editing/find/find-and-replace-noneditable-matches-expected.txt: Added.
     43        * editing/find/find-and-replace-noneditable-matches.html: Added.
     44
     45        Test find-and-replace when some of the matches to replace are noneditable, others are editable, and others are
     46        editable but are nested within noneditable elements (i.e. `contenteditable=false`). In this case, "replace all"
     47        should still replace all fully editable matches.
     48
     49        * editing/find/find-and-replace-replacement-text-input-events-expected.txt: Added.
     50        * editing/find/find-and-replace-replacement-text-input-events.html: Added.
     51
     52        Tests that find-and-replace emits input events of `inputType` "insertReplacementText", except when inserting
     53        replacement text at a caret selection.
     54
     55        * platform/ios-wk2/TestExpectations:
     56        * platform/mac-wk2/TestExpectations:
     57
    1582018-11-21  Zalan Bujtas  <zalan@apple.com>
    259
  • trunk/LayoutTests/TestExpectations

    r238274 r238438  
    1616editing/mac [ Skip ]
    1717editing/caret/ios [ Skip ]
     18editing/find [ Skip ]
    1819editing/pasteboard/gtk [ Skip ]
    1920editing/selection/ios [ Skip ]
  • trunk/LayoutTests/platform/ios-wk2/TestExpectations

    r238166 r238438  
    1515tiled-drawing/ios [ Pass ]
    1616fast/web-share [ Pass ]
     17editing/find [ Pass ]
    1718
    1819editing/selection/character-granularity-rect.html [ Failure ]
  • trunk/LayoutTests/platform/mac-wk2/TestExpectations

    r238375 r238438  
    1111swipe [ Pass ]
    1212fast/web-share [ Pass ]
     13editing/find [ Pass ]
    1314
    1415fast/events/autoscroll-when-zoomed.html [ Pass ]
  • trunk/Source/WebCore/ChangeLog

    r238434 r238438  
     12018-11-21  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [Cocoa] [WebKit2] Add support for replacing find-in-page text matches
     4        https://bugs.webkit.org/show_bug.cgi?id=191786
     5        <rdar://problem/45813871>
     6
     7        Reviewed by Ryosuke Niwa.
     8
     9        Add support for replacing Find-in-Page matches. See below for details. Covered by new layout tests as well as a
     10        new API test.
     11
     12        Tests: editing/find/find-and-replace-adjacent-words.html
     13               editing/find/find-and-replace-at-editing-boundary.html
     14               editing/find/find-and-replace-basic.html
     15               editing/find/find-and-replace-in-subframes.html
     16               editing/find/find-and-replace-no-matches.html
     17               editing/find/find-and-replace-noneditable-matches.html
     18               editing/find/find-and-replace-replacement-text-input-events.html
     19
     20        API test: WebKit.FindAndReplace
     21
     22        * page/Page.cpp:
     23        (WebCore::replaceRanges):
     24        (WebCore::Page::replaceRangesWithText):
     25
     26        Add a helper that, given a list of Ranges, replaces each range with the given text. To do this, we first map
     27        each Range to editing offsets within the topmost editable root for each Range. This results in a map of editable
     28        root to list of editing offsets we need to replace. To apply the replacements, for each editable root in the
     29        map, we iterate over each replacement range (i.e. an offset and length), set the current selection to contain
     30        that replacement range, and use `Editor::replaceSelectionWithText`. To prevent prior text replacements from
     31        clobbering the offsets of latter text replacement ranges, we also iterate backwards through text replacement
     32        ranges when performing each replacement.
     33
     34        Likewise, we also apply text replacement to each editing container in backwards order: for nodes in the same
     35        frame, we compare their position in the document, and for nodes in different frames, we instead compare their
     36        frames in frame tree traversal order.
     37
     38        We map Ranges to editing offsets and back when performing text replacement because each text replacement may
     39        split or merge text nodes, which causes adjacent Ranges to shrink or extend while replacing text. In an earlier
     40        attempt to implement this, I simply iterated over each Range to replace and carried out text replacement for
     41        each Range. This led to incorrect behavior in some cases, such as replacing adjacent matches. Thus, by computing
     42        the set of text replacement offsets prior to replacing any text, we're able to target the correct ranges for
     43        replacement.
     44
     45        (WebCore::Page::replaceSelectionWithText):
     46
     47        Add a helper method on Page to replace the current selection with some text. This simply calls out to
     48        `Editor::replaceSelectionWithText`.
     49
     50        * page/Page.h:
     51
    1522018-11-21  Andy Estes  <aestes@apple.com>
    253
  • trunk/Source/WebCore/PAL/ChangeLog

    r238434 r238438  
     12018-11-21  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [Cocoa] [WebKit2] Add support for replacing find-in-page text matches
     4        https://bugs.webkit.org/show_bug.cgi?id=191786
     5        <rdar://problem/45813871>
     6
     7        Reviewed by Ryosuke Niwa.
     8
     9        Add `-replaceMatches:withString:inSelectionOnly:resultCollector:`.
     10
     11        * pal/spi/mac/NSTextFinderSPI.h:
     12
    1132018-11-21  Andy Estes  <aestes@apple.com>
    214
  • trunk/Source/WebCore/PAL/pal/spi/mac/NSTextFinderSPI.h

    r220979 r238438  
    5252- (void)getSelectedText:(void (^)(NSString *selectedTextString))completionHandler;
    5353- (void)selectFindMatch:(id <NSTextFinderAsynchronousDocumentFindMatch>)findMatch completionHandler:(void (^)(void))completionHandler;
     54- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector;
    5455
    5556@end
  • trunk/Source/WebCore/page/Page.cpp

    r238192 r238438  
    4444#include "DocumentTimeline.h"
    4545#include "DragController.h"
     46#include "Editing.h"
    4647#include "Editor.h"
    4748#include "EditorClient.h"
     
    110111#include "StyleScope.h"
    111112#include "SubframeLoader.h"
     113#include "TextIterator.h"
    112114#include "TextResourceDecoder.h"
    113115#include "UserContentProvider.h"
     
    763765{
    764766    return findMatchesForText(target, options, maxMatchCount, DoNotHighlightMatches, DoNotMarkMatches);
     767}
     768
     769struct FindReplacementRange {
     770    RefPtr<ContainerNode> root;
     771    size_t location { notFound };
     772    size_t length { 0 };
     773};
     774
     775static void replaceRanges(Page& page, Vector<FindReplacementRange>&& ranges, const String& replacementText)
     776{
     777    HashMap<RefPtr<ContainerNode>, Vector<FindReplacementRange>> rangesByContainerNode;
     778    for (auto& range : ranges) {
     779        auto& rangeList = rangesByContainerNode.ensure(range.root, [] {
     780            return Vector<FindReplacementRange> { };
     781        }).iterator->value;
     782
     783        // Ensure that ranges are sorted by their end offsets, per editing container.
     784        auto endOffsetForRange = range.location + range.length;
     785        auto insertionIndex = rangeList.size();
     786        for (auto iterator = rangeList.rbegin(); iterator != rangeList.rend(); ++iterator) {
     787            auto endOffsetBeforeInsertionIndex = iterator->location + iterator->length;
     788            if (endOffsetForRange >= endOffsetBeforeInsertionIndex)
     789                break;
     790            insertionIndex--;
     791        }
     792        rangeList.insert(insertionIndex, range);
     793    }
     794
     795    HashMap<RefPtr<Frame>, unsigned> frameToTraversalIndexMap;
     796    unsigned currentFrameTraversalIndex = 0;
     797    for (Frame* frame = &page.mainFrame(); frame; frame = frame->tree().traverseNext())
     798        frameToTraversalIndexMap.set(frame, currentFrameTraversalIndex++);
     799
     800    // Likewise, iterate backwards (in document and frame order) through editing containers that contain text matches,
     801    // so that we're consistent with our backwards iteration behavior per editing container when replacing text.
     802    auto containerNodesInOrderOfReplacement = copyToVector(rangesByContainerNode.keys());
     803    std::sort(containerNodesInOrderOfReplacement.begin(), containerNodesInOrderOfReplacement.end(), [frameToTraversalIndexMap] (auto& firstNode, auto& secondNode) {
     804        if (firstNode == secondNode)
     805            return false;
     806
     807        auto firstFrame = makeRefPtr(firstNode->document().frame());
     808        if (!firstFrame)
     809            return true;
     810
     811        auto secondFrame = makeRefPtr(secondNode->document().frame());
     812        if (!secondFrame)
     813            return false;
     814
     815        if (firstFrame == secondFrame) {
     816            // comparePositions is used here instead of Node::compareDocumentPosition because some editing roots may exist inside shadow roots.
     817            return comparePositions({ firstNode.get(), Position::PositionIsBeforeChildren }, { secondNode.get(), Position::PositionIsBeforeChildren }) > 0;
     818        }
     819        return frameToTraversalIndexMap.get(firstFrame) > frameToTraversalIndexMap.get(secondFrame);
     820    });
     821
     822    for (auto container : containerNodesInOrderOfReplacement) {
     823        auto frame = makeRefPtr(container->document().frame());
     824        if (!frame)
     825            continue;
     826
     827        // Iterate backwards through ranges when replacing text, such that earlier text replacements don't clobber replacement ranges later on.
     828        auto& ranges = rangesByContainerNode.find(container)->value;
     829        for (auto iterator = ranges.rbegin(); iterator != ranges.rend(); ++iterator) {
     830            auto range = TextIterator::rangeFromLocationAndLength(container.get(), iterator->location, iterator->length);
     831            if (!range || range->collapsed())
     832                continue;
     833
     834            frame->selection().setSelectedRange(range.get(), DOWNSTREAM, true);
     835            frame->editor().replaceSelectionWithText(replacementText, true, false, EditAction::InsertReplacement);
     836        }
     837    }
     838}
     839
     840uint32_t Page::replaceRangesWithText(Vector<Ref<Range>>&& rangesToReplace, const String& replacementText, bool selectionOnly)
     841{
     842    // FIXME: In the future, we should respect the `selectionOnly` flag by checking whether each range being replaced is
     843    // contained within its frame's selection.
     844    UNUSED_PARAM(selectionOnly);
     845
     846    Vector<FindReplacementRange> replacementRanges;
     847    replacementRanges.reserveInitialCapacity(rangesToReplace.size());
     848
     849    for (auto& range : rangesToReplace) {
     850        auto highestRoot = makeRefPtr(highestEditableRoot(range->startPosition()));
     851        if (!highestRoot || highestRoot != highestEditableRoot(range->endPosition()))
     852            continue;
     853
     854        auto frame = makeRefPtr(highestRoot->document().frame());
     855        if (!frame)
     856            continue;
     857
     858        size_t replacementLocation = notFound;
     859        size_t replacementLength = 0;
     860        if (!TextIterator::getLocationAndLengthFromRange(highestRoot.get(), range.ptr(), replacementLocation, replacementLength))
     861            continue;
     862
     863        if (replacementLocation == notFound || !replacementLength)
     864            continue;
     865
     866        replacementRanges.append({ WTFMove(highestRoot), replacementLocation, replacementLength });
     867    }
     868
     869    replaceRanges(*this, WTFMove(replacementRanges), replacementText);
     870    return rangesToReplace.size();
     871}
     872
     873uint32_t Page::replaceSelectionWithText(const String& replacementText)
     874{
     875    auto frame = makeRef(focusController().focusedOrMainFrame());
     876    auto selection = frame->selection().selection();
     877    if (!selection.isContentEditable())
     878        return 0;
     879
     880    auto editAction = selection.isRange() ? EditAction::InsertReplacement : EditAction::Insert;
     881    frame->editor().replaceSelectionWithText(replacementText, true, false, editAction);
     882    return 1;
    765883}
    766884
  • trunk/Source/WebCore/page/Page.h

    r238049 r238438  
    279279
    280280    WEBCORE_EXPORT bool findString(const String&, FindOptions, DidWrap* = nullptr);
     281    WEBCORE_EXPORT uint32_t replaceRangesWithText(Vector<Ref<Range>>&& rangesToReplace, const String& replacementText, bool selectionOnly);
     282    WEBCORE_EXPORT uint32_t replaceSelectionWithText(const String& replacementText);
    281283
    282284    WEBCORE_EXPORT RefPtr<Range> rangeOfString(const String&, Range*, FindOptions);
  • trunk/Source/WebKit/ChangeLog

    r238434 r238438  
     12018-11-21  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [Cocoa] [WebKit2] Add support for replacing find-in-page text matches
     4        https://bugs.webkit.org/show_bug.cgi?id=191786
     5        <rdar://problem/45813871>
     6
     7        Reviewed by Ryosuke Niwa.
     8
     9        * UIProcess/API/Cocoa/WKWebView.mm:
     10        (-[WKWebView replaceMatches:withString:inSelectionOnly:resultCollector:]):
     11        * UIProcess/WebPageProxy.cpp:
     12        (WebKit::WebPageProxy::replaceMatches):
     13        * UIProcess/WebPageProxy.h:
     14        * UIProcess/mac/WKTextFinderClient.mm:
     15        (-[WKTextFinderClient replaceMatches:withString:inSelectionOnly:resultCollector:]):
     16
     17        Implement this method to opt in to "Replace…" UI on macOS in the find bar. In this API, we're given a list of
     18        matches to replace. We propagate the indices of each match to the web process, where FindController maps them to
     19        corresponding replacement ranges. Currently, the given list of matches is only ever a list containing the first
     20        match, or a list containing all matches.
     21
     22        * WebProcess/InjectedBundle/API/c/WKBundlePage.cpp:
     23        (WKBundlePageFindStringMatches):
     24        (WKBundlePageReplaceStringMatches):
     25        * WebProcess/InjectedBundle/API/c/WKBundlePage.h:
     26        * WebProcess/WebCoreSupport/WebEditorClient.cpp:
     27        * WebProcess/WebPage/FindController.cpp:
     28        (WebKit::FindController::replaceMatches):
     29
     30        Map match indices to Ranges, and then call into WebCore::Page to do the heavy lifting (see WebCore ChangeLog for
     31        more details). Additionally add a hard find-and-replace limit here to prevent the web process from spinning
     32        indefinitely if there are an enormous number of find matches.
     33
     34        * WebProcess/WebPage/FindController.h:
     35        * WebProcess/WebPage/WebPage.cpp:
     36        (WebKit::WebPage::findStringMatchesFromInjectedBundle):
     37        (WebKit::WebPage::replaceStringMatchesFromInjectedBundle):
     38
     39        Add helpers to exercise find and replace in WebKit2.
     40
     41        (WebKit::WebPage::replaceMatches):
     42        * WebProcess/WebPage/WebPage.h:
     43        * WebProcess/WebPage/WebPage.messages.in:
     44
    1452018-11-21  Andy Estes  <aestes@apple.com>
    246
  • trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm

    r238388 r238438  
    40904090}
    40914091
     4092- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector
     4093{
     4094    [[self _ensureTextFinderClient] replaceMatches:matches withString:replacementString inSelectionOnly:selectionOnly resultCollector:resultCollector];
     4095}
     4096
    40924097- (NSView *)documentContainerView
    40934098{
  • trunk/Source/WebKit/UIProcess/WebPageProxy.cpp

    r238388 r238438  
    33093309}
    33103310
     3311void WebPageProxy::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, Function<void(uint64_t, CallbackBase::Error)>&& callback)
     3312{
     3313    if (!isValid()) {
     3314        callback(0, CallbackBase::Error::Unknown);
     3315        return;
     3316    }
     3317
     3318    auto callbackID = m_callbacks.put(WTFMove(callback), m_process->throttler().backgroundActivityToken());
     3319    m_process->send(Messages::WebPage::ReplaceMatches(WTFMove(matchIndices), replacementText, selectionOnly, callbackID), m_pageID);
     3320}
     3321
    33113322void WebPageProxy::runJavaScriptInMainFrame(const String& script, bool forceUserGesture, WTF::Function<void (API::SerializedScriptValue*, bool hadException, const ExceptionDetails&, CallbackBase::Error)>&& callbackFunction)
    33123323{
  • trunk/Source/WebKit/UIProcess/WebPageProxy.h

    r238388 r238438  
    914914    void hideFindUI();
    915915    void countStringMatches(const String&, FindOptions, unsigned maxMatchCount);
     916    void replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, Function<void(uint64_t, CallbackBase::Error)>&&);
    916917    void didCountStringMatches(const String&, uint32_t matchCount);
    917918    void setTextIndicator(const WebCore::TextIndicatorData&, uint64_t /* WebCore::TextIndicatorWindowLifetime */ lifetime = 0 /* Permanent */);
  • trunk/Source/WebKit/UIProcess/mac/WKTextFinderClient.mm

    r235265 r238438  
    3535#import <algorithm>
    3636#import <pal/spi/mac/NSTextFinderSPI.h>
     37#import <wtf/BlockPtr.h>
    3738#import <wtf/Deque.h>
    3839
    39 // FIXME: Implement support for replace.
    4040// FIXME: Implement scrollFindMatchToVisible.
    4141// FIXME: The NSTextFinder overlay doesn't move with scrolling; we should have a mode where we manage the overlay.
     
    171171
    172172#pragma mark - NSTextFinderClient SPI
     173
     174- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementText inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector
     175{
     176    Vector<uint32_t> matchIndices;
     177    matchIndices.reserveCapacity(matches.count);
     178    for (id match in matches) {
     179        if ([match isKindOfClass:WKTextFinderMatch.class])
     180            matchIndices.uncheckedAppend([(WKTextFinderMatch *)match index]);
     181    }
     182    _page->replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly, [collector = makeBlockPtr(resultCollector)] (uint64_t numberOfReplacements, auto error) {
     183        collector(error == WebKit::CallbackBase::Error::None ? numberOfReplacements : 0);
     184    });
     185}
    173186
    174187- (void)findMatchesForString:(NSString *)targetString relativeToMatch:(id <NSTextFinderAsynchronousDocumentFindMatch>)relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector
  • trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.cpp

    r237266 r238438  
    450450}
    451451
     452void WKBundlePageFindStringMatches(WKBundlePageRef pageRef, WKStringRef target, WKFindOptions findOptions)
     453{
     454    toImpl(pageRef)->findStringMatchesFromInjectedBundle(toWTFString(target), toFindOptions(findOptions));
     455}
     456
     457void WKBundlePageReplaceStringMatches(WKBundlePageRef pageRef, WKArrayRef matchIndicesRef, WKStringRef replacementText, bool selectionOnly)
     458{
     459    auto* matchIndices = toImpl(matchIndicesRef);
     460
     461    Vector<uint32_t> indices;
     462    indices.reserveInitialCapacity(matchIndices->size());
     463
     464    for (size_t arrayIndex = 0; arrayIndex < matchIndices->size(); ++arrayIndex) {
     465        if (auto* indexAsObject = matchIndices->at<API::UInt64>(arrayIndex))
     466            indices.uncheckedAppend(indexAsObject->value());
     467    }
     468    toImpl(pageRef)->replaceStringMatchesFromInjectedBundle(WTFMove(indices), toWTFString(replacementText), selectionOnly);
     469}
     470
    452471WKImageRef WKBundlePageCreateSnapshotWithOptions(WKBundlePageRef pageRef, WKRect rect, WKSnapshotOptions options)
    453472{
  • trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.h

    r237205 r238438  
    9393
    9494WK_EXPORT bool WKBundlePageFindString(WKBundlePageRef page, WKStringRef target, WKFindOptions findOptions);
     95WK_EXPORT void WKBundlePageFindStringMatches(WKBundlePageRef page, WKStringRef target, WKFindOptions findOptions);
     96WK_EXPORT void WKBundlePageReplaceStringMatches(WKBundlePageRef page, WKArrayRef matchIndices, WKStringRef replacementText, bool selectionOnly);
    9597
    9698WK_EXPORT WKImageRef WKBundlePageCreateSnapshotWithOptions(WKBundlePageRef page, WKRect rect, WKSnapshotOptions options);
  • trunk/Source/WebKit/WebProcess/WebPage/FindController.cpp

    r237565 r238438  
    100100}
    101101
     102uint32_t FindController::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly)
     103{
     104    if (matchIndices.isEmpty())
     105        return m_webPage->corePage()->replaceSelectionWithText(replacementText);
     106
     107    // FIXME: This is an arbitrary cap on the maximum number of matches to try and replace, to prevent the web process from
     108    // hanging while replacing an enormous amount of matches. In the future, we should handle replacement in batches, and
     109    // periodically update an NSProgress in the UI process when a batch of find-in-page matches are replaced.
     110    const uint32_t maximumNumberOfMatchesToReplace = 1000;
     111
     112    Vector<Ref<Range>> rangesToReplace;
     113    rangesToReplace.reserveCapacity(std::min<uint32_t>(maximumNumberOfMatchesToReplace, matchIndices.size()));
     114    for (auto index : matchIndices) {
     115        if (index < m_findMatches.size())
     116            rangesToReplace.uncheckedAppend(*m_findMatches[index]);
     117        if (rangesToReplace.size() >= maximumNumberOfMatchesToReplace)
     118            break;
     119    }
     120    return m_webPage->corePage()->replaceRangesWithText(WTFMove(rangesToReplace), replacementText, selectionOnly);
     121}
     122
    102123static Frame* frameWithSelection(Page* page)
    103124{
  • trunk/Source/WebKit/WebProcess/WebPage/FindController.h

    r237565 r238438  
    6262    void hideFindUI();
    6363    void countStringMatches(const String&, FindOptions, unsigned maxMatchCount);
     64    uint32_t replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly);
    6465   
    6566    void hideFindIndicator();
  • trunk/Source/WebKit/WebProcess/WebPage/WebPage.cpp

    r238330 r238438  
    37923792}
    37933793
     3794void WebPage::findStringMatchesFromInjectedBundle(const String& target, FindOptions options)
     3795{
     3796    findController().findStringMatches(target, options, 0);
     3797}
     3798
     3799void WebPage::replaceStringMatchesFromInjectedBundle(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly)
     3800{
     3801    findController().replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly);
     3802}
     3803
    37943804void WebPage::findString(const String& string, uint32_t options, uint32_t maxMatchCount)
    37953805{
     
    38203830{
    38213831    findController().countStringMatches(string, static_cast<FindOptions>(options), maxMatchCount);
     3832}
     3833
     3834void WebPage::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, CallbackID callbackID)
     3835{
     3836    auto numberOfReplacements = findController().replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly);
     3837    send(Messages::WebPageProxy::UnsignedCallback(numberOfReplacements, callbackID));
    38223838}
    38233839
  • trunk/Source/WebKit/WebProcess/WebPage/WebPage.h

    r238330 r238438  
    409409
    410410    bool findStringFromInjectedBundle(const String&, FindOptions);
     411    void findStringMatchesFromInjectedBundle(const String&, FindOptions);
     412    void replaceStringMatchesFromInjectedBundle(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly);
    411413
    412414    WebFrame* mainWebFrame() const { return m_mainFrame.get(); }
     
    13051307    void hideFindUI();
    13061308    void countStringMatches(const String&, uint32_t findOptions, uint32_t maxMatchCount);
     1309    void replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, CallbackID);
    13071310
    13081311#if USE(COORDINATED_GRAPHICS)
  • trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in

    r238330 r238438  
    267267    HideFindUI()
    268268    CountStringMatches(String string, uint32_t findOptions, unsigned maxMatchCount)
     269    ReplaceMatches(Vector<uint32_t> matchIndices, String replacementText, bool selectionOnly, WebKit::CallbackID callbackID)
    269270   
    270271    AddMIMETypeWithCustomContentProvider(String mimeType)
  • trunk/Tools/ChangeLog

    r238430 r238438  
     12018-11-21  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [Cocoa] [WebKit2] Add support for replacing find-in-page text matches
     4        https://bugs.webkit.org/show_bug.cgi?id=191786
     5        <rdar://problem/45813871>
     6
     7        Reviewed by Ryosuke Niwa.
     8
     9        * MiniBrowser/mac/WK2BrowserWindowController.m:
     10        (-[WK2BrowserWindowController setFindBarView:]):
     11
     12        Fix a bug in MiniBrowser that prevents AppKit from displaying the "All" button in the find bar after checking
     13        the "Replace" option.
     14
     15        * TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm:
     16
     17        Add an API test to exercise find-and-replace API using WKWebView.
     18
     19        (replaceMatches):
     20        (TEST):
     21        * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
     22        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
     23        (WTR::findOptionsFromArray):
     24        (WTR::TestRunner::findString):
     25        (WTR::TestRunner::findStringMatchesInPage):
     26        (WTR::TestRunner::replaceFindMatchesAtIndices):
     27
     28        Add TestRunner hooks to simulate find-in-page and replace.
     29
     30        * WebKitTestRunner/InjectedBundle/TestRunner.h:
     31
    1322018-11-21  Zalan Bujtas  <zalan@apple.com>
    233
  • trunk/Tools/MiniBrowser/mac/WK2BrowserWindowController.m

    r236913 r238438  
    764764- (void)setFindBarView:(NSView *)findBarView
    765765{
    766     if (_textFindBarView)
    767         [_textFindBarView removeFromSuperview];
    768766    _textFindBarView = findBarView;
    769767    _findBarVisible = YES;
    770     [containerView addSubview:_textFindBarView];
    771768    [_textFindBarView setFrame:NSMakeRect(0, 0, containerView.bounds.size.width, _textFindBarView.frame.size.height)];
    772769}
  • trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm

    r237266 r238438  
    2828#import "PlatformUtilities.h"
    2929#import "TestNavigationDelegate.h"
     30#import "TestWKWebView.h"
    3031#import <WebKit/WKWebViewPrivate.h>
    3132#import <wtf/RetainPtr.h>
     
    5354
    5455- (void)findMatchesForString:(NSString *)targetString relativeToMatch:(FindMatch)relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector;
     56- (void)replaceMatches:(NSArray<FindMatch> *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector;
    5557
    5658@end
     
    7476    TestWebKitAPI::Util::run(&done);
    7577
     78    return result;
     79}
     80
     81static NSUInteger replaceMatches(WKWebView *webView, NSArray<FindMatch> *matchesToReplace, NSString *replacementText)
     82{
     83    __block NSUInteger result;
     84    __block bool done = false;
     85
     86    [webView replaceMatches:matchesToReplace withString:replacementText inSelectionOnly:NO resultCollector:^(NSUInteger replacementCount) {
     87        result = replacementCount;
     88        done = true;
     89    }];
     90
     91    TestWebKitAPI::Util::run(&done);
    7692    return result;
    7793}
     
    206222}
    207223
    208 #endif
     224TEST(WebKit, FindAndReplace)
     225{
     226    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
     227    [webView synchronouslyLoadHTMLString:@"<body contenteditable><input id='first' value='hello'>hello world<input id='second' value='world'></body>"];
     228
     229    auto result = findMatches(webView.get(), @"hello");
     230    EXPECT_EQ(2U, [result.matches count]);
     231    EXPECT_EQ(2U, replaceMatches(webView.get(), result.matches.get(), @"hi"));
     232    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
     233    EXPECT_WK_STREQ("world", [webView stringByEvaluatingJavaScript:@"second.value"]);
     234    EXPECT_WK_STREQ("hi world", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
     235
     236    result = findMatches(webView.get(), @"world");
     237    EXPECT_EQ(2U, [result.matches count]);
     238    EXPECT_EQ(1U, replaceMatches(webView.get(), @[ [result.matches firstObject] ], @"hi"));
     239    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
     240    EXPECT_WK_STREQ("world", [webView stringByEvaluatingJavaScript:@"second.value"]);
     241    EXPECT_WK_STREQ("hi hi", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
     242
     243    result = findMatches(webView.get(), @"world");
     244    EXPECT_EQ(1U, [result.matches count]);
     245    EXPECT_EQ(1U, replaceMatches(webView.get(), @[ [result.matches firstObject] ], @"hi"));
     246    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
     247    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"second.value"]);
     248    EXPECT_WK_STREQ("hi hi", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
     249}
     250
     251#endif // WK_API_ENABLED && !PLATFORM(IOS_FAMILY)
  • trunk/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl

    r237905 r238438  
    147147    // Text search testing.
    148148    boolean findString(DOMString target, object optionsArray);
     149    void findStringMatchesInPage(DOMString target, object optionsArray);
     150    void replaceFindMatchesAtIndices(object matchIndicesArray, DOMString replacementText, boolean selectionOnly);
    149151
    150152    // Evaluating script in a special context.
  • trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp

    r238166 r238438  
    4646#include <WebKit/WKBundleScriptWorld.h>
    4747#include <WebKit/WKData.h>
     48#include <WebKit/WKNumber.h>
    4849#include <WebKit/WKPagePrivate.h>
    4950#include <WebKit/WKRetainPtr.h>
     
    5152#include <WebKit/WebKit2_C.h>
    5253#include <wtf/HashMap.h>
     54#include <wtf/Optional.h>
    5355#include <wtf/StdLibExtras.h>
    5456#include <wtf/text/CString.h>
     
    300302}
    301303
    302 bool TestRunner::findString(JSStringRef target, JSValueRef optionsArrayAsValue)
    303 {
    304     WKFindOptions options = 0;
    305 
     304static std::optional<WKFindOptions> findOptionsFromArray(JSValueRef optionsArrayAsValue)
     305{
    306306    auto& injectedBundle = InjectedBundle::singleton();
    307307    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(injectedBundle.page()->page());
     
    311311    JSValueRef lengthValue = JSObjectGetProperty(context, optionsArray, lengthPropertyName.get(), 0);
    312312    if (!JSValueIsNumber(context, lengthValue))
    313         return false;
    314 
     313        return std::nullopt;
     314
     315    WKFindOptions options = 0;
    315316    size_t length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
    316317    for (size_t i = 0; i < length; ++i) {
     
    335336        }
    336337    }
    337 
    338     return WKBundlePageFindString(injectedBundle.page()->page(), toWK(target).get(), options);
     338    return options;
     339}
     340
     341bool TestRunner::findString(JSStringRef target, JSValueRef optionsArrayAsValue)
     342{
     343    if (auto options = findOptionsFromArray(optionsArrayAsValue))
     344        return WKBundlePageFindString(InjectedBundle::singleton().page()->page(), toWK(target).get(), *options);
     345
     346    return false;
     347}
     348
     349void TestRunner::findStringMatchesInPage(JSStringRef target, JSValueRef optionsArrayAsValue)
     350{
     351    if (auto options = findOptionsFromArray(optionsArrayAsValue))
     352        return WKBundlePageFindStringMatches(InjectedBundle::singleton().page()->page(), toWK(target).get(), *options);
     353}
     354
     355void TestRunner::replaceFindMatchesAtIndices(JSValueRef matchIndicesAsValue, JSStringRef replacementText, bool selectionOnly)
     356{
     357    auto& bundle = InjectedBundle::singleton();
     358    auto mainFrame = WKBundlePageGetMainFrame(bundle.page()->page());
     359    auto context = WKBundleFrameGetJavaScriptContext(mainFrame);
     360    auto lengthPropertyName = adopt(JSStringCreateWithUTF8CString("length"));
     361    auto matchIndicesObject = JSValueToObject(context, matchIndicesAsValue, 0);
     362    auto lengthValue = JSObjectGetProperty(context, matchIndicesObject, lengthPropertyName.get(), 0);
     363    if (!JSValueIsNumber(context, lengthValue))
     364        return;
     365
     366    auto indices = adoptWK(WKMutableArrayCreate());
     367    auto length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
     368    for (size_t i = 0; i < length; ++i) {
     369        auto value = JSObjectGetPropertyAtIndex(context, matchIndicesObject, i, 0);
     370        if (!JSValueIsNumber(context, value))
     371            continue;
     372
     373        auto index = adoptWK(WKUInt64Create(std::round(JSValueToNumber(context, value, nullptr))));
     374        WKArrayAppendItem(indices.get(), index.get());
     375    }
     376    WKBundlePageReplaceStringMatches(bundle.page()->page(), indices.get(), toWK(replacementText).get(), selectionOnly);
    339377}
    340378
  • trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h

    r238098 r238438  
    155155    // Text search testing.
    156156    bool findString(JSStringRef, JSValueRef optionsArray);
     157    void findStringMatchesInPage(JSStringRef, JSValueRef optionsArray);
     158    void replaceFindMatchesAtIndices(JSValueRef matchIndices, JSStringRef replacementText, bool selectionOnly);
    157159
    158160    // Local storage
Note: See TracChangeset for help on using the changeset viewer.