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();