Title: [238438] trunk
Revision
238438
Author
wenson_hs...@apple.com
Date
2018-11-21 21:03:59 -0800 (Wed, 21 Nov 2018)

Log 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:

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (238437 => 238438)


--- trunk/LayoutTests/ChangeLog	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/LayoutTests/ChangeLog	2018-11-22 05:03:59 UTC (rev 238438)
@@ -1,3 +1,60 @@
+2018-11-21  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [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.
+
+        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:
+
 2018-11-21  Zalan Bujtas  <za...@apple.com>
 
         [LFC][IFC] Horizontal margins should be considered as non-breakable space

Modified: trunk/LayoutTests/TestExpectations (238437 => 238438)


--- trunk/LayoutTests/TestExpectations	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/LayoutTests/TestExpectations	2018-11-22 05:03:59 UTC (rev 238438)
@@ -15,6 +15,7 @@
 displaylists [ Skip ]
 editing/mac [ Skip ]
 editing/caret/ios [ Skip ]
+editing/find [ Skip ]
 editing/pasteboard/gtk [ Skip ]
 editing/selection/ios [ Skip ]
 tiled-drawing [ Skip ]

Added: trunk/LayoutTests/editing/find/find-and-replace-adjacent-words-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-adjacent-words-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-adjacent-words-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,11 @@
+Verifies that find and replace can be used to replace adjacent words in an editable area. This test requires WebKitTestRunner.
+
+After replacing 'apple' with an empty string:
+| <#selection-caret>
+| <br>
+
+After replacing 'apple' with 'appleapple':
+| "<#selection-anchor>appleapple<#selection-focus>appleappleappleapple"
+
+After replacing 'apple' with 'APPLE':
+| "<#selection-anchor>APPLE<#selection-focus>APPLEAPPLE"

Added: trunk/LayoutTests/editing/find/find-and-replace-adjacent-words.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-adjacent-words.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-adjacent-words.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+    <div id="editor" contenteditable>appleappleapple</div>
+</body>
+<script>
+function reset() {
+    editor.textContent = "appleappleapple";
+}
+
+Markup.description("Verifies that find and replace can be used to replace adjacent words in an editable area. This test requires WebKitTestRunner.");
+
+testRunner.findStringMatchesInPage("apple", []);
+testRunner.replaceFindMatchesAtIndices([0, 1, 2], "", false);
+Markup.dump("editor", "After replacing 'apple' with an empty string");
+
+reset();
+
+testRunner.findStringMatchesInPage("apple", []);
+testRunner.replaceFindMatchesAtIndices([0, 1, 2], "appleapple", false);
+Markup.dump("editor", "After replacing 'apple' with 'appleapple'");
+
+reset();
+
+testRunner.findStringMatchesInPage("apple", []);
+testRunner.replaceFindMatchesAtIndices([0, 1, 2], "APPLE", false);
+Markup.dump("editor", "After replacing 'apple' with 'APPLE'");
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,12 @@
+Verifies that find and replace ignores matches that span editing boundaries. This test requires WebKitTestRunner.
+
+After replacing 'apple' with 'pear':
+| "
+        "
+| <span>
+|   contenteditable=""
+|   "<#selection-anchor>pear<#selection-focus> ap"
+| <span>
+|   "ple apple"
+| "
+    "

Added: trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-at-editing-boundary.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<style>
+    p[contenteditable] {
+        border: 1px solid red;
+    }
+</style>
+</head>
+<body>
+    <div id="container">
+        <span contenteditable>apple ap</span><span>ple apple</span>
+    </div>
+</body>
+<script>
+Markup.description("Verifies that find and replace ignores matches that span editing boundaries. This test requires WebKitTestRunner.");
+testRunner.findStringMatchesInPage("apple", []);
+testRunner.replaceFindMatchesAtIndices([0], "pear", false);
+Markup.dump("container", "After replacing 'apple' with 'pear'");
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-basic-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-basic-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-basic-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,25 @@
+Verifies that find and replace can be used to replace words in an editable area. This test requires WebKitTestRunner.
+
+After replacing 'orange' with 'apricot':
+| "
+        "
+| <p>
+|   "Apple banana <#selection-anchor>apricot<#selection-focus>."
+| "
+        "
+| <p>
+|   "Kiwi banana pear."
+| "
+    "
+
+After replacing 'banana' with 'watermelon':
+| "
+        "
+| <p>
+|   "Apple <#selection-anchor>watermelon<#selection-focus> apricot."
+| "
+        "
+| <p>
+|   "Kiwi watermelon pear."
+| "
+    "

Added: trunk/LayoutTests/editing/find/find-and-replace-basic.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-basic.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-basic.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+    <div id="editor" contenteditable>
+        <p>Apple banana orange.</p>
+        <p>Kiwi banana pear.</p>
+    </div>
+</body>
+<script>
+Markup.description("Verifies that find and replace can be used to replace words in an editable area. This test requires WebKitTestRunner.");
+
+testRunner.findStringMatchesInPage("orange", []);
+testRunner.replaceFindMatchesAtIndices([0], "apricot", false);
+Markup.dump("editor", "After replacing 'orange' with 'apricot'");
+
+testRunner.findStringMatchesInPage("banana", []);
+testRunner.replaceFindMatchesAtIndices([0, 1], "watermelon", false);
+Markup.dump("editor", "After replacing 'banana' with 'watermelon'");
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-in-subframes-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-in-subframes-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-in-subframes-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,139 @@
+Verifies that find and replace can be used to replace words in different frames on the same page, as well as inside text fields. This test requires WebKitTestRunner.
+
+After replacing 'bar' with 'hello':
+| "
+        "
+| <p>
+|   "foo <#selection-anchor>hello<#selection-focus> baz"
+| "
+        "
+| <iframe>
+|   srcdoc="<body contenteditable>foo bar baz
+            <iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <input value='foo bar baz'></input></body>"
+| "
+        "
+| <iframe>
+|   srcdoc="<iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <iframe srcdoc='<input value=bar>'></iframe>
+            <div contenteditable>foo bar bar</div><textarea>foo bar bar</textarea>"
+| "
+        "
+| <input>
+|   value="foo bar baz"
+|   this.value="foo hello baz"
+|   <shadow:root>
+|     <div>
+|       contenteditable="plaintext-only"
+|       "foo hello baz"
+| "
+    "
+
+FRAME 0:
+| <head>
+| <body>
+|   contenteditable=""
+|   "foo <#selection-anchor>hello<#selection-focus> baz
+            "
+|   <iframe>
+|     srcdoc="<body contenteditable>bar</body>"
+|   "
+            "
+|   <input>
+|     value="foo bar baz"
+|     this.value="foo hello baz"
+|     <shadow:root>
+|       <div>
+|         contenteditable="plaintext-only"
+|         "foo hello baz"
+
+FRAME 1:
+| <head>
+| <body>
+|   <iframe>
+|     srcdoc="<body contenteditable>bar</body>"
+|   "
+            "
+|   <iframe>
+|     srcdoc="<input value=bar>"
+|   "
+            "
+|   <div>
+|     contenteditable=""
+|     "foo <#selection-anchor>hello<#selection-focus> hello"
+|   <textarea>
+|     this.value="foo hello hello"
+|     "foo bar bar"
+|     <shadow:root>
+|       <div>
+|         contenteditable="plaintext-only"
+|         "foo hello hello"
+
+After replacing the first occurrence of 'foo' with 'world':
+| "
+        "
+| <p>
+|   "<#selection-anchor>world<#selection-focus> hello baz"
+| "
+        "
+| <iframe>
+|   srcdoc="<body contenteditable>foo bar baz
+            <iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <input value='foo bar baz'></input></body>"
+| "
+        "
+| <iframe>
+|   srcdoc="<iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <iframe srcdoc='<input value=bar>'></iframe>
+            <div contenteditable>foo bar bar</div><textarea>foo bar bar</textarea>"
+| "
+        "
+| <input>
+|   value="foo bar baz"
+|   this.value="foo hello baz"
+|   <shadow:root>
+|     <div>
+|       contenteditable="plaintext-only"
+|       "foo hello baz"
+| "
+    "
+
+FRAME 0:
+| <head>
+| <body>
+|   contenteditable=""
+|   "foo <#selection-anchor>hello<#selection-focus> baz
+            "
+|   <iframe>
+|     srcdoc="<body contenteditable>bar</body>"
+|   "
+            "
+|   <input>
+|     value="foo bar baz"
+|     this.value="foo hello baz"
+|     <shadow:root>
+|       <div>
+|         contenteditable="plaintext-only"
+|         "foo hello baz"
+
+FRAME 1:
+| <head>
+| <body>
+|   <iframe>
+|     srcdoc="<body contenteditable>bar</body>"
+|   "
+            "
+|   <iframe>
+|     srcdoc="<input value=bar>"
+|   "
+            "
+|   <div>
+|     contenteditable=""
+|     "foo <#selection-anchor>hello<#selection-focus> hello"
+|   <textarea>
+|     this.value="foo hello hello"
+|     "foo bar bar"
+|     <shadow:root>
+|       <div>
+|         contenteditable="plaintext-only"
+|         "foo hello hello"

Added: trunk/LayoutTests/editing/find/find-and-replace-in-subframes.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-in-subframes.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-in-subframes.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+    <div contenteditable id="editor">
+        <p>foo bar baz</p>
+        <iframe srcdoc="<body contenteditable>foo bar baz
+            <iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <input value='foo bar baz'></input></body>"></iframe>
+        <iframe srcdoc="<iframe srcdoc='<body contenteditable>bar</body>'></iframe>
+            <iframe srcdoc='<input value=bar>'></iframe>
+            <div contenteditable>foo bar bar</div><textarea>foo bar bar</textarea>"></iframe>
+        <input value='foo bar baz'>
+    </div>
+</body>
+<script>
+Markup.waitUntilDone();
+Markup.description("Verifies that find and replace can be used to replace words in different frames on the same page, as well as inside text fields. This test requires WebKitTestRunner.");
+
+addEventListener("load", () => {
+    testRunner.findStringMatchesInPage("bar", []);
+    testRunner.replaceFindMatchesAtIndices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "hello", false);
+    Markup.dump("editor", "After replacing 'bar' with 'hello'");
+
+    testRunner.findStringMatchesInPage("foo", []);
+    testRunner.replaceFindMatchesAtIndices([0], "world", false);
+    Markup.dump("editor", "After replacing the first occurrence of 'foo' with 'world'");
+
+    Markup.notifyDone();
+});
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-no-matches-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-no-matches-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-no-matches-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,29 @@
+Verifies that find and replace will insert the replacement text at the selection, if no matches are specified. This test requires WebKitTestRunner.
+
+After replacing 'banana' with 'pear':
+| "
+        "
+| <p>
+|   id="p1"
+|   "Apple <#selection-anchor>pear<#selection-focus> orange."
+| "
+        "
+| <p>
+|   id="p2"
+|   "Kiwi banana pear."
+| "
+    "
+
+After inserting 'watermelon' after 'Kiwi':
+| "
+        "
+| <p>
+|   id="p1"
+|   "Apple pear orange."
+| "
+        "
+| <p>
+|   id="p2"
+|   "Kiwi<#selection-anchor>watermelon<#selection-focus> banana pear."
+| "
+    "

Added: trunk/LayoutTests/editing/find/find-and-replace-no-matches.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-no-matches.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-no-matches.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+    <div id="editor" contenteditable>
+        <p id="p1">Apple banana orange.</p>
+        <p id="p2">Kiwi banana pear.</p>
+    </div>
+</body>
+<script>
+Markup.description("Verifies that find and replace will insert the replacement text at the selection, if no matches are specified. This test requires WebKitTestRunner.");
+
+getSelection().setBaseAndExtent(p1.childNodes[0], 6, p1.childNodes[0], 12);
+testRunner.replaceFindMatchesAtIndices([], "pear", false);
+Markup.dump("editor", "After replacing 'banana' with 'pear'");
+
+getSelection().setPosition(p2.childNodes[0], 4);
+testRunner.replaceFindMatchesAtIndices([], "watermelon", false);
+Markup.dump("editor", "After inserting 'watermelon' after 'Kiwi'");
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,33 @@
+Verifies that find and replace does not change matches in noneditable content. This test requires WebKitTestRunner.
+
+After replacing 'eta' with '_eta_':
+| "
+        "
+| <p>
+|   contenteditable="false"
+|   "alpha beta."
+| "
+        "
+| <p>
+|   "gamma b<#selection-anchor>_eta_<#selection-focus> phi."
+| "
+        "
+| <div>
+|   contenteditable="false"
+|   "
+            "
+|   <p>
+|     "alpha kappa eta."
+|   "
+            "
+|   <p>
+|     contenteditable="true"
+|     "_eta_ kappa nu."
+|   "
+        "
+| "
+        "
+| <p>
+|   "b_eta_ phi delta."
+| "
+    "

Added: trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-noneditable-matches.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<style>
+    *[contenteditable="true"] {
+        border: 3px green solid;
+    }
+
+    *[contenteditable="false"] {
+        border: 1px red dashed;
+        background-color: rgba(200, 0, 0, 0.25);
+    }
+</style>
+</head>
+<body>
+    <div contenteditable="true" id="editor">
+        <p contenteditable="false">alpha beta.</p>
+        <p>gamma beta phi.</p>
+        <div contenteditable="false">
+            <p>alpha kappa eta.</p>
+            <p contenteditable="true">eta kappa nu.</p>
+        </div>
+        <p>beta phi delta.</p>
+    </div>
+</body>
+<script>
+Markup.description("Verifies that find and replace does not change matches in noneditable content. This test requires WebKitTestRunner.");
+testRunner.findStringMatchesInPage("eta", []);
+testRunner.replaceFindMatchesAtIndices([0, 1, 2, 3, 4], "_eta_", false);
+Markup.dump("editor", "After replacing 'eta' with '_eta_'");
+</script>
+</html>

Added: trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events-expected.txt (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events-expected.txt	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,70 @@
+Verifies that find and replace fires input events of type "insertReplacementText". This test requires WebKitTestRunner.
+
+Pineapple
+
+Kiwi watermelon pear.
+
+
+(field):
+        type=beforeinput,
+        inputType=insertReplacementText,
+        range=null,
+        data=""
+        dataTransfer=null
+(field):
+        type=input,
+        inputType=insertReplacementText,
+        range=null,
+        data=""
+        dataTransfer=null
+(editor):
+        type=beforeinput,
+        inputType=insertReplacementText,
+        range=([object Text]#5,[object Text]#11),
+        data=""
+        dataTransfer=[object DataTransfer]
+(editor):
+        type=input,
+        inputType=insertReplacementText,
+        range=null,
+        data=""
+        dataTransfer=[object DataTransfer]
+(editor):
+        type=beforeinput,
+        inputType=insertReplacementText,
+        range=([object Text]#6,[object Text]#12),
+        data=""
+        dataTransfer=[object DataTransfer]
+(editor):
+        type=input,
+        inputType=insertReplacementText,
+        range=null,
+        data=""
+        dataTransfer=[object DataTransfer]
+---
+(editor):
+        type=beforeinput,
+        inputType=insertReplacementText,
+        range=([object Text]#0,[object Text]#24),
+        data=""
+        dataTransfer=[object DataTransfer]
+(editor):
+        type=input,
+        inputType=insertReplacementText,
+        range=null,
+        data=""
+        dataTransfer=[object DataTransfer]
+---
+(field):
+        type=beforeinput,
+        inputType=insertText,
+        range=null,
+        data=""
+        dataTransfer=null
+(field):
+        type=input,
+        inputType=insertText,
+        range=null,
+        data=""
+        dataTransfer=null
+

Added: trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events.html (0 => 238438)


--- trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events.html	                        (rev 0)
+++ trunk/LayoutTests/editing/find/find-and-replace-replacement-text-input-events.html	2018-11-22 05:03:59 UTC (rev 238438)
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<body>
+    <p>Verifies that find and replace fires input events of type "insertReplacementText". This test requires WebKitTestRunner.</p>
+    <div id="editor" contenteditable>
+        <p>Apple banana orange.</p>
+        <p>Kiwi banana pear.</p>
+    </div>
+    <input type="text" id="field" value="Apple banana orange.">
+    <pre id="output"></pre>
+</body>
+<script>
+let write = s => output.innerHTML += s + "<br>";
+
+function toString(range) {
+    if (!range)
+        return "null";
+
+    return `(${range.startContainer}#${range.startOffset},${range.endContainer}#${range.endOffset})`;
+}
+
+function logInputEvent(event) {
+    write(`(${event.target.id}):
+        type=${event.type},
+        inputType=${event.inputType},
+        range=${toString(event.getTargetRanges()[0])},
+        data=""
+        dataTransfer=${event.dataTransfer}`);
+}
+
+editor.addEventListener("beforeinput", logInputEvent);
+editor.addEventListener("input", logInputEvent);
+field.addEventListener("beforeinput", logInputEvent);
+field.addEventListener("input", logInputEvent);
+
+if (window.testRunner) {
+    testRunner.dumpAsText();
+    testRunner.findStringMatchesInPage("banana", []);
+    testRunner.replaceFindMatchesAtIndices([0, 1, 2], "watermelon", false);
+
+    write("---");
+
+    getSelection().setBaseAndExtent(editor.children[0], 0, editor.children[0], 1);
+    testRunner.replaceFindMatchesAtIndices([], "Pineapple", false);
+
+    write("---");
+
+    field.focus();
+    field.setSelectionRange(0, 0);
+    testRunner.replaceFindMatchesAtIndices([], "Guava", false);
+}
+</script>
+</html>

Modified: trunk/LayoutTests/platform/ios-wk2/TestExpectations (238437 => 238438)


--- trunk/LayoutTests/platform/ios-wk2/TestExpectations	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/LayoutTests/platform/ios-wk2/TestExpectations	2018-11-22 05:03:59 UTC (rev 238438)
@@ -14,6 +14,7 @@
 scrollingcoordinator/ios [ Pass ]
 tiled-drawing/ios [ Pass ]
 fast/web-share [ Pass ]
+editing/find [ Pass ]
 
 editing/selection/character-granularity-rect.html [ Failure ]
 

Modified: trunk/LayoutTests/platform/mac-wk2/TestExpectations (238437 => 238438)


--- trunk/LayoutTests/platform/mac-wk2/TestExpectations	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/LayoutTests/platform/mac-wk2/TestExpectations	2018-11-22 05:03:59 UTC (rev 238438)
@@ -10,6 +10,7 @@
 fast/visual-viewport/tiled-drawing [ Pass ]
 swipe [ Pass ]
 fast/web-share [ Pass ]
+editing/find [ Pass ]
 
 fast/events/autoscroll-when-zoomed.html [ Pass ]
 fast/events/autoscroll-main-document.html [ Pass ]

Modified: trunk/Source/WebCore/ChangeLog (238437 => 238438)


--- trunk/Source/WebCore/ChangeLog	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebCore/ChangeLog	2018-11-22 05:03:59 UTC (rev 238438)
@@ -1,3 +1,54 @@
+2018-11-21  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [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.
+
+        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:
+
 2018-11-21  Andy Estes  <aes...@apple.com>
 
         [Cocoa] Create a soft-linking file for PassKit

Modified: trunk/Source/WebCore/PAL/ChangeLog (238437 => 238438)


--- trunk/Source/WebCore/PAL/ChangeLog	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebCore/PAL/ChangeLog	2018-11-22 05:03:59 UTC (rev 238438)
@@ -1,3 +1,15 @@
+2018-11-21  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [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.
+
+        Add `-replaceMatches:withString:inSelectionOnly:resultCollector:`.
+
+        * pal/spi/mac/NSTextFinderSPI.h:
+
 2018-11-21  Andy Estes  <aes...@apple.com>
 
         [Cocoa] Create a soft-linking file for PassKit

Modified: trunk/Source/WebCore/PAL/pal/spi/mac/NSTextFinderSPI.h (238437 => 238438)


--- trunk/Source/WebCore/PAL/pal/spi/mac/NSTextFinderSPI.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebCore/PAL/pal/spi/mac/NSTextFinderSPI.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -51,6 +51,7 @@
 - (NSView *)documentContainerView;
 - (void)getSelectedText:(void (^)(NSString *selectedTextString))completionHandler;
 - (void)selectFindMatch:(id <NSTextFinderAsynchronousDocumentFindMatch>)findMatch completionHandler:(void (^)(void))completionHandler;
+- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector;
 
 @end
 

Modified: trunk/Source/WebCore/page/Page.cpp (238437 => 238438)


--- trunk/Source/WebCore/page/Page.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebCore/page/Page.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -43,6 +43,7 @@
 #include "DocumentMarkerController.h"
 #include "DocumentTimeline.h"
 #include "DragController.h"
+#include "Editing.h"
 #include "Editor.h"
 #include "EditorClient.h"
 #include "EmptyClients.h"
@@ -109,6 +110,7 @@
 #include "StyleResolver.h"
 #include "StyleScope.h"
 #include "SubframeLoader.h"
+#include "TextIterator.h"
 #include "TextResourceDecoder.h"
 #include "UserContentProvider.h"
 #include "UserInputBridge.h"
@@ -764,6 +766,122 @@
     return findMatchesForText(target, options, maxMatchCount, DoNotHighlightMatches, DoNotMarkMatches);
 }
 
+struct FindReplacementRange {
+    RefPtr<ContainerNode> root;
+    size_t location { notFound };
+    size_t length { 0 };
+};
+
+static void replaceRanges(Page& page, Vector<FindReplacementRange>&& ranges, const String& replacementText)
+{
+    HashMap<RefPtr<ContainerNode>, Vector<FindReplacementRange>> rangesByContainerNode;
+    for (auto& range : ranges) {
+        auto& rangeList = rangesByContainerNode.ensure(range.root, [] {
+            return Vector<FindReplacementRange> { };
+        }).iterator->value;
+
+        // Ensure that ranges are sorted by their end offsets, per editing container.
+        auto endOffsetForRange = range.location + range.length;
+        auto insertionIndex = rangeList.size();
+        for (auto iterator = rangeList.rbegin(); iterator != rangeList.rend(); ++iterator) {
+            auto endOffsetBeforeInsertionIndex = iterator->location + iterator->length;
+            if (endOffsetForRange >= endOffsetBeforeInsertionIndex)
+                break;
+            insertionIndex--;
+        }
+        rangeList.insert(insertionIndex, range);
+    }
+
+    HashMap<RefPtr<Frame>, unsigned> frameToTraversalIndexMap;
+    unsigned currentFrameTraversalIndex = 0;
+    for (Frame* frame = &page.mainFrame(); frame; frame = frame->tree().traverseNext())
+        frameToTraversalIndexMap.set(frame, currentFrameTraversalIndex++);
+
+    // Likewise, iterate backwards (in document and frame order) through editing containers that contain text matches,
+    // so that we're consistent with our backwards iteration behavior per editing container when replacing text.
+    auto containerNodesInOrderOfReplacement = copyToVector(rangesByContainerNode.keys());
+    std::sort(containerNodesInOrderOfReplacement.begin(), containerNodesInOrderOfReplacement.end(), [frameToTraversalIndexMap] (auto& firstNode, auto& secondNode) {
+        if (firstNode == secondNode)
+            return false;
+
+        auto firstFrame = makeRefPtr(firstNode->document().frame());
+        if (!firstFrame)
+            return true;
+
+        auto secondFrame = makeRefPtr(secondNode->document().frame());
+        if (!secondFrame)
+            return false;
+
+        if (firstFrame == secondFrame) {
+            // comparePositions is used here instead of Node::compareDocumentPosition because some editing roots may exist inside shadow roots.
+            return comparePositions({ firstNode.get(), Position::PositionIsBeforeChildren }, { secondNode.get(), Position::PositionIsBeforeChildren }) > 0;
+        }
+        return frameToTraversalIndexMap.get(firstFrame) > frameToTraversalIndexMap.get(secondFrame);
+    });
+
+    for (auto container : containerNodesInOrderOfReplacement) {
+        auto frame = makeRefPtr(container->document().frame());
+        if (!frame)
+            continue;
+
+        // Iterate backwards through ranges when replacing text, such that earlier text replacements don't clobber replacement ranges later on.
+        auto& ranges = rangesByContainerNode.find(container)->value;
+        for (auto iterator = ranges.rbegin(); iterator != ranges.rend(); ++iterator) {
+            auto range = TextIterator::rangeFromLocationAndLength(container.get(), iterator->location, iterator->length);
+            if (!range || range->collapsed())
+                continue;
+
+            frame->selection().setSelectedRange(range.get(), DOWNSTREAM, true);
+            frame->editor().replaceSelectionWithText(replacementText, true, false, EditAction::InsertReplacement);
+        }
+    }
+}
+
+uint32_t Page::replaceRangesWithText(Vector<Ref<Range>>&& rangesToReplace, const String& replacementText, bool selectionOnly)
+{
+    // FIXME: In the future, we should respect the `selectionOnly` flag by checking whether each range being replaced is
+    // contained within its frame's selection.
+    UNUSED_PARAM(selectionOnly);
+
+    Vector<FindReplacementRange> replacementRanges;
+    replacementRanges.reserveInitialCapacity(rangesToReplace.size());
+
+    for (auto& range : rangesToReplace) {
+        auto highestRoot = makeRefPtr(highestEditableRoot(range->startPosition()));
+        if (!highestRoot || highestRoot != highestEditableRoot(range->endPosition()))
+            continue;
+
+        auto frame = makeRefPtr(highestRoot->document().frame());
+        if (!frame)
+            continue;
+
+        size_t replacementLocation = notFound;
+        size_t replacementLength = 0;
+        if (!TextIterator::getLocationAndLengthFromRange(highestRoot.get(), range.ptr(), replacementLocation, replacementLength))
+            continue;
+
+        if (replacementLocation == notFound || !replacementLength)
+            continue;
+
+        replacementRanges.append({ WTFMove(highestRoot), replacementLocation, replacementLength });
+    }
+
+    replaceRanges(*this, WTFMove(replacementRanges), replacementText);
+    return rangesToReplace.size();
+}
+
+uint32_t Page::replaceSelectionWithText(const String& replacementText)
+{
+    auto frame = makeRef(focusController().focusedOrMainFrame());
+    auto selection = frame->selection().selection();
+    if (!selection.isContentEditable())
+        return 0;
+
+    auto editAction = selection.isRange() ? EditAction::InsertReplacement : EditAction::Insert;
+    frame->editor().replaceSelectionWithText(replacementText, true, false, editAction);
+    return 1;
+}
+
 void Page::unmarkAllTextMatches()
 {
     Frame* frame = &mainFrame();

Modified: trunk/Source/WebCore/page/Page.h (238437 => 238438)


--- trunk/Source/WebCore/page/Page.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebCore/page/Page.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -278,6 +278,8 @@
     bool tabKeyCyclesThroughElements() const { return m_tabKeyCyclesThroughElements; }
 
     WEBCORE_EXPORT bool findString(const String&, FindOptions, DidWrap* = nullptr);
+    WEBCORE_EXPORT uint32_t replaceRangesWithText(Vector<Ref<Range>>&& rangesToReplace, const String& replacementText, bool selectionOnly);
+    WEBCORE_EXPORT uint32_t replaceSelectionWithText(const String& replacementText);
 
     WEBCORE_EXPORT RefPtr<Range> rangeOfString(const String&, Range*, FindOptions);
 

Modified: trunk/Source/WebKit/ChangeLog (238437 => 238438)


--- trunk/Source/WebKit/ChangeLog	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/ChangeLog	2018-11-22 05:03:59 UTC (rev 238438)
@@ -1,3 +1,47 @@
+2018-11-21  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [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.
+
+        * 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:
+
 2018-11-21  Andy Estes  <aes...@apple.com>
 
         [Cocoa] Create a soft-linking file for PassKit

Modified: trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm (238437 => 238438)


--- trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm	2018-11-22 05:03:59 UTC (rev 238438)
@@ -4089,6 +4089,11 @@
     [[self _ensureTextFinderClient] findMatchesForString:targetString relativeToMatch:relativeMatch findOptions:findOptions maxResults:maxResults resultCollector:resultCollector];
 }
 
+- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector
+{
+    [[self _ensureTextFinderClient] replaceMatches:matches withString:replacementString inSelectionOnly:selectionOnly resultCollector:resultCollector];
+}
+
 - (NSView *)documentContainerView
 {
     return self;

Modified: trunk/Source/WebKit/UIProcess/WebPageProxy.cpp (238437 => 238438)


--- trunk/Source/WebKit/UIProcess/WebPageProxy.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/UIProcess/WebPageProxy.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -3308,6 +3308,17 @@
     m_process->send(Messages::WebPage::CountStringMatches(string, options, maxMatchCount), m_pageID);
 }
 
+void WebPageProxy::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, Function<void(uint64_t, CallbackBase::Error)>&& callback)
+{
+    if (!isValid()) {
+        callback(0, CallbackBase::Error::Unknown);
+        return;
+    }
+
+    auto callbackID = m_callbacks.put(WTFMove(callback), m_process->throttler().backgroundActivityToken());
+    m_process->send(Messages::WebPage::ReplaceMatches(WTFMove(matchIndices), replacementText, selectionOnly, callbackID), m_pageID);
+}
+
 void WebPageProxy::runJavaScriptInMainFrame(const String& script, bool forceUserGesture, WTF::Function<void (API::SerializedScriptValue*, bool hadException, const ExceptionDetails&, CallbackBase::Error)>&& callbackFunction)
 {
     if (!isValid()) {

Modified: trunk/Source/WebKit/UIProcess/WebPageProxy.h (238437 => 238438)


--- trunk/Source/WebKit/UIProcess/WebPageProxy.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/UIProcess/WebPageProxy.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -913,6 +913,7 @@
     void didGetImageForFindMatch(const ShareableBitmap::Handle& contentImageHandle, uint32_t matchIndex);
     void hideFindUI();
     void countStringMatches(const String&, FindOptions, unsigned maxMatchCount);
+    void replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, Function<void(uint64_t, CallbackBase::Error)>&&);
     void didCountStringMatches(const String&, uint32_t matchCount);
     void setTextIndicator(const WebCore::TextIndicatorData&, uint64_t /* WebCore::TextIndicatorWindowLifetime */ lifetime = 0 /* Permanent */);
     void setTextIndicatorAnimationProgress(float);

Modified: trunk/Source/WebKit/UIProcess/mac/WKTextFinderClient.mm (238437 => 238438)


--- trunk/Source/WebKit/UIProcess/mac/WKTextFinderClient.mm	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/UIProcess/mac/WKTextFinderClient.mm	2018-11-22 05:03:59 UTC (rev 238438)
@@ -34,9 +34,9 @@
 #import "WebPageProxy.h"
 #import <algorithm>
 #import <pal/spi/mac/NSTextFinderSPI.h>
+#import <wtf/BlockPtr.h>
 #import <wtf/Deque.h>
 
-// FIXME: Implement support for replace.
 // FIXME: Implement scrollFindMatchToVisible.
 // FIXME: The NSTextFinder overlay doesn't move with scrolling; we should have a mode where we manage the overlay.
 
@@ -171,6 +171,19 @@
 
 #pragma mark - NSTextFinderClient SPI
 
+- (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementText inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector
+{
+    Vector<uint32_t> matchIndices;
+    matchIndices.reserveCapacity(matches.count);
+    for (id match in matches) {
+        if ([match isKindOfClass:WKTextFinderMatch.class])
+            matchIndices.uncheckedAppend([(WKTextFinderMatch *)match index]);
+    }
+    _page->replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly, [collector = makeBlockPtr(resultCollector)] (uint64_t numberOfReplacements, auto error) {
+        collector(error == WebKit::CallbackBase::Error::None ? numberOfReplacements : 0);
+    });
+}
+
 - (void)findMatchesForString:(NSString *)targetString relativeToMatch:(id <NSTextFinderAsynchronousDocumentFindMatch>)relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector
 {
     // Limit the number of results, for performance reasons; NSTextFinder always

Modified: trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.cpp (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -449,6 +449,25 @@
     return toImpl(pageRef)->findStringFromInjectedBundle(toWTFString(target), toFindOptions(findOptions));
 }
 
+void WKBundlePageFindStringMatches(WKBundlePageRef pageRef, WKStringRef target, WKFindOptions findOptions)
+{
+    toImpl(pageRef)->findStringMatchesFromInjectedBundle(toWTFString(target), toFindOptions(findOptions));
+}
+
+void WKBundlePageReplaceStringMatches(WKBundlePageRef pageRef, WKArrayRef matchIndicesRef, WKStringRef replacementText, bool selectionOnly)
+{
+    auto* matchIndices = toImpl(matchIndicesRef);
+
+    Vector<uint32_t> indices;
+    indices.reserveInitialCapacity(matchIndices->size());
+
+    for (size_t arrayIndex = 0; arrayIndex < matchIndices->size(); ++arrayIndex) {
+        if (auto* indexAsObject = matchIndices->at<API::UInt64>(arrayIndex))
+            indices.uncheckedAppend(indexAsObject->value());
+    }
+    toImpl(pageRef)->replaceStringMatchesFromInjectedBundle(WTFMove(indices), toWTFString(replacementText), selectionOnly);
+}
+
 WKImageRef WKBundlePageCreateSnapshotWithOptions(WKBundlePageRef pageRef, WKRect rect, WKSnapshotOptions options)
 {
     RefPtr<WebImage> webImage = toImpl(pageRef)->scaledSnapshotWithOptions(toIntRect(rect), 1, toSnapshotOptions(options));

Modified: trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.h (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -92,6 +92,8 @@
 WK_EXPORT bool WKBundlePageCanHandleRequest(WKURLRequestRef request);
 
 WK_EXPORT bool WKBundlePageFindString(WKBundlePageRef page, WKStringRef target, WKFindOptions findOptions);
+WK_EXPORT void WKBundlePageFindStringMatches(WKBundlePageRef page, WKStringRef target, WKFindOptions findOptions);
+WK_EXPORT void WKBundlePageReplaceStringMatches(WKBundlePageRef page, WKArrayRef matchIndices, WKStringRef replacementText, bool selectionOnly);
 
 WK_EXPORT WKImageRef WKBundlePageCreateSnapshotWithOptions(WKBundlePageRef page, WKRect rect, WKSnapshotOptions options);
 

Modified: trunk/Source/WebKit/WebProcess/WebPage/FindController.cpp (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/WebPage/FindController.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/WebPage/FindController.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -99,6 +99,27 @@
     m_webPage->send(Messages::WebPageProxy::DidCountStringMatches(string, matchCount));
 }
 
+uint32_t FindController::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly)
+{
+    if (matchIndices.isEmpty())
+        return m_webPage->corePage()->replaceSelectionWithText(replacementText);
+
+    // FIXME: This is an arbitrary cap on the maximum number of matches to try and replace, to prevent the web process from
+    // hanging while replacing an enormous amount of matches. In the future, we should handle replacement in batches, and
+    // periodically update an NSProgress in the UI process when a batch of find-in-page matches are replaced.
+    const uint32_t maximumNumberOfMatchesToReplace = 1000;
+
+    Vector<Ref<Range>> rangesToReplace;
+    rangesToReplace.reserveCapacity(std::min<uint32_t>(maximumNumberOfMatchesToReplace, matchIndices.size()));
+    for (auto index : matchIndices) {
+        if (index < m_findMatches.size())
+            rangesToReplace.uncheckedAppend(*m_findMatches[index]);
+        if (rangesToReplace.size() >= maximumNumberOfMatchesToReplace)
+            break;
+    }
+    return m_webPage->corePage()->replaceRangesWithText(WTFMove(rangesToReplace), replacementText, selectionOnly);
+}
+
 static Frame* frameWithSelection(Page* page)
 {
     for (Frame* frame = &page->mainFrame(); frame; frame = frame->tree().traverseNext()) {

Modified: trunk/Source/WebKit/WebProcess/WebPage/FindController.h (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/WebPage/FindController.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/WebPage/FindController.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -61,6 +61,7 @@
     void selectFindMatch(uint32_t matchIndex);
     void hideFindUI();
     void countStringMatches(const String&, FindOptions, unsigned maxMatchCount);
+    uint32_t replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly);
     
     void hideFindIndicator();
     void showFindIndicatorInSelection();

Modified: trunk/Source/WebKit/WebProcess/WebPage/WebPage.cpp (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/WebPage/WebPage.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/WebPage/WebPage.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -3791,6 +3791,16 @@
     return m_page->findString(target, core(options));
 }
 
+void WebPage::findStringMatchesFromInjectedBundle(const String& target, FindOptions options)
+{
+    findController().findStringMatches(target, options, 0);
+}
+
+void WebPage::replaceStringMatchesFromInjectedBundle(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly)
+{
+    findController().replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly);
+}
+
 void WebPage::findString(const String& string, uint32_t options, uint32_t maxMatchCount)
 {
     findController().findString(string, static_cast<FindOptions>(options), maxMatchCount);
@@ -3821,6 +3831,12 @@
     findController().countStringMatches(string, static_cast<FindOptions>(options), maxMatchCount);
 }
 
+void WebPage::replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, CallbackID callbackID)
+{
+    auto numberOfReplacements = findController().replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly);
+    send(Messages::WebPageProxy::UnsignedCallback(numberOfReplacements, callbackID));
+}
+
 void WebPage::didChangeSelectedIndexForActivePopupMenu(int32_t newIndex)
 {
     changeSelectedIndex(newIndex);

Modified: trunk/Source/WebKit/WebProcess/WebPage/WebPage.h (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/WebPage/WebPage.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/WebPage/WebPage.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -408,6 +408,8 @@
 #endif
 
     bool findStringFromInjectedBundle(const String&, FindOptions);
+    void findStringMatchesFromInjectedBundle(const String&, FindOptions);
+    void replaceStringMatchesFromInjectedBundle(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly);
 
     WebFrame* mainWebFrame() const { return m_mainFrame.get(); }
 
@@ -1304,6 +1306,7 @@
     void selectFindMatch(uint32_t matchIndex);
     void hideFindUI();
     void countStringMatches(const String&, uint32_t findOptions, uint32_t maxMatchCount);
+    void replaceMatches(Vector<uint32_t>&& matchIndices, const String& replacementText, bool selectionOnly, CallbackID);
 
 #if USE(COORDINATED_GRAPHICS)
     void sendViewportAttributesChanged(const WebCore::ViewportArguments&);

Modified: trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in (238437 => 238438)


--- trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in	2018-11-22 05:03:59 UTC (rev 238438)
@@ -266,6 +266,7 @@
     SelectFindMatch(uint32_t matchIndex)
     HideFindUI()
     CountStringMatches(String string, uint32_t findOptions, unsigned maxMatchCount)
+    ReplaceMatches(Vector<uint32_t> matchIndices, String replacementText, bool selectionOnly, WebKit::CallbackID callbackID)
     
     AddMIMETypeWithCustomContentProvider(String mimeType)
 

Modified: trunk/Tools/ChangeLog (238437 => 238438)


--- trunk/Tools/ChangeLog	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/ChangeLog	2018-11-22 05:03:59 UTC (rev 238438)
@@ -1,3 +1,34 @@
+2018-11-21  Wenson Hsieh  <wenson_hs...@apple.com>
+
+        [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.
+
+        * 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:
+
 2018-11-21  Zalan Bujtas  <za...@apple.com>
 
         [LFC][IFC] Horizontal margins should be considered as non-breakable space

Modified: trunk/Tools/MiniBrowser/mac/WK2BrowserWindowController.m (238437 => 238438)


--- trunk/Tools/MiniBrowser/mac/WK2BrowserWindowController.m	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/MiniBrowser/mac/WK2BrowserWindowController.m	2018-11-22 05:03:59 UTC (rev 238438)
@@ -763,11 +763,8 @@
 
 - (void)setFindBarView:(NSView *)findBarView
 {
-    if (_textFindBarView)
-        [_textFindBarView removeFromSuperview];
     _textFindBarView = findBarView;
     _findBarVisible = YES;
-    [containerView addSubview:_textFindBarView];
     [_textFindBarView setFrame:NSMakeRect(0, 0, containerView.bounds.size.width, _textFindBarView.frame.size.height)];
 }
 

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm (238437 => 238438)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/FindInPage.mm	2018-11-22 05:03:59 UTC (rev 238438)
@@ -27,6 +27,7 @@
 
 #import "PlatformUtilities.h"
 #import "TestNavigationDelegate.h"
+#import "TestWKWebView.h"
 #import <WebKit/WKWebViewPrivate.h>
 #import <wtf/RetainPtr.h>
 
@@ -52,6 +53,7 @@
 @interface WKWebView (NSTextFinderSupport)
 
 - (void)findMatchesForString:(NSString *)targetString relativeToMatch:(FindMatch)relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector;
+- (void)replaceMatches:(NSArray<FindMatch> *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector;
 
 @end
 
@@ -76,6 +78,20 @@
     return result;
 }
 
+static NSUInteger replaceMatches(WKWebView *webView, NSArray<FindMatch> *matchesToReplace, NSString *replacementText)
+{
+    __block NSUInteger result;
+    __block bool done = false;
+
+    [webView replaceMatches:matchesToReplace withString:replacementText inSelectionOnly:NO resultCollector:^(NSUInteger replacementCount) {
+        result = replacementCount;
+        done = true;
+    }];
+
+    TestWebKitAPI::Util::run(&done);
+    return result;
+}
+
 TEST(WebKit, FindInPage)
 {
     RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]);
@@ -205,4 +221,31 @@
     EXPECT_TRUE(result.didWrap);
 }
 
-#endif
+TEST(WebKit, FindAndReplace)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable><input id='first' value='hello'>hello world<input id='second' value='world'></body>"];
+
+    auto result = findMatches(webView.get(), @"hello");
+    EXPECT_EQ(2U, [result.matches count]);
+    EXPECT_EQ(2U, replaceMatches(webView.get(), result.matches.get(), @"hi"));
+    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
+    EXPECT_WK_STREQ("world", [webView stringByEvaluatingJavaScript:@"second.value"]);
+    EXPECT_WK_STREQ("hi world", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
+
+    result = findMatches(webView.get(), @"world");
+    EXPECT_EQ(2U, [result.matches count]);
+    EXPECT_EQ(1U, replaceMatches(webView.get(), @[ [result.matches firstObject] ], @"hi"));
+    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
+    EXPECT_WK_STREQ("world", [webView stringByEvaluatingJavaScript:@"second.value"]);
+    EXPECT_WK_STREQ("hi hi", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
+
+    result = findMatches(webView.get(), @"world");
+    EXPECT_EQ(1U, [result.matches count]);
+    EXPECT_EQ(1U, replaceMatches(webView.get(), @[ [result.matches firstObject] ], @"hi"));
+    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"first.value"]);
+    EXPECT_WK_STREQ("hi", [webView stringByEvaluatingJavaScript:@"second.value"]);
+    EXPECT_WK_STREQ("hi hi", [webView stringByEvaluatingJavaScript:@"document.body.textContent"]);
+}
+
+#endif // WK_API_ENABLED && !PLATFORM(IOS_FAMILY)

Modified: trunk/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl (238437 => 238438)


--- trunk/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl	2018-11-22 05:03:59 UTC (rev 238438)
@@ -146,6 +146,8 @@
 
     // Text search testing.
     boolean findString(DOMString target, object optionsArray);
+    void findStringMatchesInPage(DOMString target, object optionsArray);
+    void replaceFindMatchesAtIndices(object matchIndicesArray, DOMString replacementText, boolean selectionOnly);
 
     // Evaluating script in a special context.
     [PassContext] void evaluateScriptInIsolatedWorld(unsigned long worldID, DOMString script);

Modified: trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp (238437 => 238438)


--- trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp	2018-11-22 05:03:59 UTC (rev 238438)
@@ -45,11 +45,13 @@
 #include <WebKit/WKBundlePrivate.h>
 #include <WebKit/WKBundleScriptWorld.h>
 #include <WebKit/WKData.h>
+#include <WebKit/WKNumber.h>
 #include <WebKit/WKPagePrivate.h>
 #include <WebKit/WKRetainPtr.h>
 #include <WebKit/WKSerializedScriptValue.h>
 #include <WebKit/WebKit2_C.h>
 #include <wtf/HashMap.h>
+#include <wtf/Optional.h>
 #include <wtf/StdLibExtras.h>
 #include <wtf/text/CString.h>
 #include <wtf/text/StringBuilder.h>
@@ -299,10 +301,8 @@
     WKBundlePageExecuteEditingCommand(InjectedBundle::singleton().page()->page(), toWK(name).get(), toWK(value).get());
 }
 
-bool TestRunner::findString(JSStringRef target, JSValueRef optionsArrayAsValue)
+static std::optional<WKFindOptions> findOptionsFromArray(JSValueRef optionsArrayAsValue)
 {
-    WKFindOptions options = 0;
-
     auto& injectedBundle = InjectedBundle::singleton();
     WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(injectedBundle.page()->page());
     JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);
@@ -310,8 +310,9 @@
     JSObjectRef optionsArray = JSValueToObject(context, optionsArrayAsValue, 0);
     JSValueRef lengthValue = JSObjectGetProperty(context, optionsArray, lengthPropertyName.get(), 0);
     if (!JSValueIsNumber(context, lengthValue))
-        return false;
+        return std::nullopt;
 
+    WKFindOptions options = 0;
     size_t length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
     for (size_t i = 0; i < length; ++i) {
         JSValueRef value = JSObjectGetPropertyAtIndex(context, optionsArray, i, 0);
@@ -334,10 +335,47 @@
             // FIXME: No kWKFindOptionsStartInSelection.
         }
     }
+    return options;
+}
 
-    return WKBundlePageFindString(injectedBundle.page()->page(), toWK(target).get(), options);
+bool TestRunner::findString(JSStringRef target, JSValueRef optionsArrayAsValue)
+{
+    if (auto options = findOptionsFromArray(optionsArrayAsValue))
+        return WKBundlePageFindString(InjectedBundle::singleton().page()->page(), toWK(target).get(), *options);
+
+    return false;
 }
 
+void TestRunner::findStringMatchesInPage(JSStringRef target, JSValueRef optionsArrayAsValue)
+{
+    if (auto options = findOptionsFromArray(optionsArrayAsValue))
+        return WKBundlePageFindStringMatches(InjectedBundle::singleton().page()->page(), toWK(target).get(), *options);
+}
+
+void TestRunner::replaceFindMatchesAtIndices(JSValueRef matchIndicesAsValue, JSStringRef replacementText, bool selectionOnly)
+{
+    auto& bundle = InjectedBundle::singleton();
+    auto mainFrame = WKBundlePageGetMainFrame(bundle.page()->page());
+    auto context = WKBundleFrameGetJavaScriptContext(mainFrame);
+    auto lengthPropertyName = adopt(JSStringCreateWithUTF8CString("length"));
+    auto matchIndicesObject = JSValueToObject(context, matchIndicesAsValue, 0);
+    auto lengthValue = JSObjectGetProperty(context, matchIndicesObject, lengthPropertyName.get(), 0);
+    if (!JSValueIsNumber(context, lengthValue))
+        return;
+
+    auto indices = adoptWK(WKMutableArrayCreate());
+    auto length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
+    for (size_t i = 0; i < length; ++i) {
+        auto value = JSObjectGetPropertyAtIndex(context, matchIndicesObject, i, 0);
+        if (!JSValueIsNumber(context, value))
+            continue;
+
+        auto index = adoptWK(WKUInt64Create(std::round(JSValueToNumber(context, value, nullptr))));
+        WKArrayAppendItem(indices.get(), index.get());
+    }
+    WKBundlePageReplaceStringMatches(bundle.page()->page(), indices.get(), toWK(replacementText).get(), selectionOnly);
+}
+
 void TestRunner::clearAllDatabases()
 {
     WKBundleClearAllDatabases(InjectedBundle::singleton().bundle());

Modified: trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h (238437 => 238438)


--- trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h	2018-11-22 03:43:30 UTC (rev 238437)
+++ trunk/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h	2018-11-22 05:03:59 UTC (rev 238438)
@@ -154,6 +154,8 @@
 
     // Text search testing.
     bool findString(JSStringRef, JSValueRef optionsArray);
+    void findStringMatchesInPage(JSStringRef, JSValueRef optionsArray);
+    void replaceFindMatchesAtIndices(JSValueRef matchIndices, JSStringRef replacementText, bool selectionOnly);
 
     // Local storage
     void clearAllDatabases();
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to