[iOS] Implement support for UIWKDocumentRequestSpatialAndCurrentSelection
https://bugs.webkit.org/show_bug.cgi?id=213704
<rdar://problem/59738878>

Reviewed by Wenson Hsieh.

Source/WebKit:

Implement support for the new request type UIWKDocumentRequestSpatialAndCurrentSelection.
Requests of this type return the contents inside the range that covers both the specified
rect and the current selection, if there is one.

The flag UIWKDocumentRequestSpatialAndCurrentSelection was added in <rdar://problem/64867540>.

* Platform/spi/ios/UIKitSPI.h: Same thing I did in TestWebKitAPI/ios/UIKitSPI.h. See Tools ChangeLog for why.
* Shared/DocumentEditingContext.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(toWebDocumentRequestOptions):
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::requestDocumentEditingContext):

Tools:

Add some tests.

* TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm:
(TEST):
* TestWebKitAPI/ios/UIKitSPI.h: Define the enumerator using a macro just like what was done for
UIWKDocumentRequestMarkedTextRects. The reason for the macro instead of modifying the enumeration
definition in this file is to 1) avoid breaking the build for Apple engineers that haven't picked
up <rdar://problem/64867540> and to 2) avoid breaking the OpenSource build.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@263813 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index e35f0e3..c3873bf 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,24 @@
+2020-07-01  Daniel Bates  <dabates@apple.com>
+
+        [iOS] Implement support for UIWKDocumentRequestSpatialAndCurrentSelection
+        https://bugs.webkit.org/show_bug.cgi?id=213704
+        <rdar://problem/59738878>
+
+        Reviewed by Wenson Hsieh.
+
+        Implement support for the new request type UIWKDocumentRequestSpatialAndCurrentSelection.
+        Requests of this type return the contents inside the range that covers both the specified
+        rect and the current selection, if there is one.
+
+        The flag UIWKDocumentRequestSpatialAndCurrentSelection was added in <rdar://problem/64867540>.
+
+        * Platform/spi/ios/UIKitSPI.h: Same thing I did in TestWebKitAPI/ios/UIKitSPI.h. See Tools ChangeLog for why.
+        * Shared/DocumentEditingContext.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (toWebDocumentRequestOptions):
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::requestDocumentEditingContext):
+
 2020-07-01  Chris Dumez  <cdumez@apple.com>
 
         Crash under IPC::isValidMessageName()
diff --git a/Source/WebKit/Platform/spi/ios/UIKitSPI.h b/Source/WebKit/Platform/spi/ios/UIKitSPI.h
index 484bf89..a28b46d 100644
--- a/Source/WebKit/Platform/spi/ios/UIKitSPI.h
+++ b/Source/WebKit/Platform/spi/ios/UIKitSPI.h
@@ -1225,6 +1225,7 @@
 #endif // USE(APPLE_INTERNAL_SDK)
 
 #define UIWKDocumentRequestMarkedTextRects (1 << 5)
+#define UIWKDocumentRequestSpatialAndCurrentSelection (1 << 6)
 
 @interface UITextInteractionAssistant (IPI)
 @property (nonatomic, readonly) BOOL inGesture;
diff --git a/Source/WebKit/Shared/DocumentEditingContext.h b/Source/WebKit/Shared/DocumentEditingContext.h
index 5466e1e..9b9fd60 100644
--- a/Source/WebKit/Shared/DocumentEditingContext.h
+++ b/Source/WebKit/Shared/DocumentEditingContext.h
@@ -48,6 +48,7 @@
         Spatial = 1 << 3,
         Annotation = 1 << 4,
         MarkedTextRects = 1 << 5,
+        SpatialAndCurrentSelection = 1 << 6,
     };
 
     OptionSet<Options> options;
@@ -118,7 +119,8 @@
         WebKit::DocumentEditingContextRequest::Options::Rects,
         WebKit::DocumentEditingContextRequest::Options::Spatial,
         WebKit::DocumentEditingContextRequest::Options::Annotation,
-        WebKit::DocumentEditingContextRequest::Options::MarkedTextRects
+        WebKit::DocumentEditingContextRequest::Options::MarkedTextRects,
+        WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection
     >;
 };
 
diff --git a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
index 667401c..7b8b5ff 100644
--- a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
+++ b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
@@ -7800,6 +7800,8 @@
         options.add(WebKit::DocumentEditingContextRequest::Options::Annotation);
     if (flags & UIWKDocumentRequestMarkedTextRects)
         options.add(WebKit::DocumentEditingContextRequest::Options::MarkedTextRects);
+    if (flags & UIWKDocumentRequestSpatialAndCurrentSelection)
+        options.add(WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection);
 
     return options;
 }
diff --git a/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm b/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
index 7b8102c..935c349 100644
--- a/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
+++ b/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
@@ -4137,7 +4137,7 @@
     VisiblePosition selectionStart = selection.visibleStart();
     VisiblePosition selectionEnd = selection.visibleEnd();
 
-    bool isSpatialRequest = request.options.contains(DocumentEditingContextRequest::Options::Spatial);
+    bool isSpatialRequest = request.options.containsAny({ DocumentEditingContextRequest::Options::Spatial, DocumentEditingContextRequest::Options::SpatialAndCurrentSelection });
     bool wantsRects = request.options.contains(DocumentEditingContextRequest::Options::Rects);
     bool wantsMarkedTextRects = request.options.contains(DocumentEditingContextRequest::Options::MarkedTextRects);
 
@@ -4161,6 +4161,12 @@
         rangeOfInterestEnd = visiblePositionForPointInRootViewCoordinates(frame.get(), request.rect.maxXMaxYCorner());
         if (rangeOfInterestEnd < rangeOfInterestStart)
             std::exchange(rangeOfInterestStart, rangeOfInterestEnd);
+        if (request.options.contains(DocumentEditingContextRequest::Options::SpatialAndCurrentSelection)) {
+            if (selectionStart < rangeOfInterestStart)
+                rangeOfInterestStart = selectionStart;
+            if (selectionEnd > rangeOfInterestEnd)
+                rangeOfInterestEnd = selectionEnd;
+        }
     } else if (!selection.isNone()) {
         rangeOfInterestStart = selectionStart;
         rangeOfInterestEnd = selectionEnd;
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 111a114..0bd744e 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,20 @@
+2020-07-01  Daniel Bates  <dabates@apple.com>
+
+        [iOS] Implement support for UIWKDocumentRequestSpatialAndCurrentSelection
+        https://bugs.webkit.org/show_bug.cgi?id=213704
+        <rdar://problem/59738878>
+
+        Reviewed by Wenson Hsieh.
+
+        Add some tests.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm:
+        (TEST):
+        * TestWebKitAPI/ios/UIKitSPI.h: Define the enumerator using a macro just like what was done for
+        UIWKDocumentRequestMarkedTextRects. The reason for the macro instead of modifying the enumeration
+        definition in this file is to 1) avoid breaking the build for Apple engineers that haven't picked
+        up <rdar://problem/64867540> and to 2) avoid breaking the OpenSource build.
+
 2020-07-01  Kate Cheney  <katherine_cheney@apple.com>
 
         WKUserScripts injecting into all frames seem to violate app-bound domains
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm
index f6f8cde..36c1372 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm
@@ -458,6 +458,128 @@
     }
 }
 
+static CGRect CGRectFromJSONEncodedDOMRectJSValue(id jsValue)
+{
+    if (![jsValue isKindOfClass:NSDictionary.class])
+        return CGRectNull;
+    NSDictionary *domRect = jsValue;
+    return CGRectMake([domRect[@"left"] floatValue], [domRect[@"top"] floatValue], [domRect[@"width"] floatValue], [domRect[@"height"] floatValue]);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectBeforeRangeSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"<span id='spatialBox'>The</span> quick brown fox <span id='jumps'>jumps</span> over the dog.")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 1)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(0, 0, 3 * glyphWidth, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("The quick brown fox ", context.contextBefore);
+    EXPECT_NSSTRING_EQ("jumps", context.selectedText);
+    EXPECT_NSSTRING_EQ(" over the", context.contextAfter);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectAfterRangeSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"The quick brown fox <span id='jumps'>jumps</span> over the dog.<span id='spatialBox'></span>")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 1)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(39 * glyphWidth, 0, 0, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("brown fox ", context.contextBefore);
+    EXPECT_NSSTRING_EQ("jumps", context.selectedText);
+    EXPECT_NSSTRING_EQ(" over the dog.", context.contextAfter);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectAroundRangeSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"The quick brown <span id='spatialBox'>fox <span id='jumps'>jumps</span> </span>over the dog.")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 1)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(16 * glyphWidth, 0, 10 * glyphWidth, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("quick brown fox ", context.contextBefore);
+    EXPECT_NSSTRING_EQ("jumps", context.selectedText);
+    EXPECT_NSSTRING_EQ(" over the", context.contextAfter);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectBeforeCaretSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"<body contenteditable='true'><span id='spatialBox'>The</span> quick brown fox <span id='jumps'>jumps</span> over the dog.</body>")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 0)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(0, 0, 3 * glyphWidth, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("The quick brown fox ", context.contextBefore);
+    EXPECT_NULL(context.selectedText);
+    EXPECT_NSSTRING_EQ("jumps over", context.contextAfter);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectAfterCaretSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"<body contenteditable='true'>The quick brown fox <span id='jumps'>jumps</span> over the dog.<span id='spatialBox'></span></body>")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 0)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(39 * glyphWidth, 0, 0, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("brown fox ", context.contextBefore);
+    EXPECT_NULL(context.selectedText);
+    EXPECT_NSSTRING_EQ("jumps over the dog.", context.contextAfter);
+}
+
+TEST(DocumentEditingContext, SpatialAndCurrentSelectionRequest_RectAroundCaretSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 980, 600)]);
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:applyAhemStyle(@"<body contenteditable='true'>The quick brown <span id='spatialBox'>fox <span id='jumps'>jumps</span> </span>over the dog.</body>")];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(jumps, 0, jumps, 0)"];
+
+    // Hit testing below the last line is treated as if the line was hit. So, use height of 1
+    // to ensure we aren't even close to the line height.
+    auto spatialBoxRect = CGRectFromJSONEncodedDOMRectJSValue([webView objectByEvaluatingJavaScript:@"spatialBox.getBoundingClientRect().toJSON()"]);
+    spatialBoxRect.size.height = 1;
+    EXPECT_EQ(CGRectMake(16 * glyphWidth, 0, 10 * glyphWidth, 1), spatialBoxRect);
+
+    UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestSpatialAndCurrentSelection, UITextGranularityWord, 2, spatialBoxRect)];
+    EXPECT_NOT_NULL(context);
+    EXPECT_NSSTRING_EQ("quick brown fox ", context.contextBefore);
+    EXPECT_NULL(context.selectedText);
+    EXPECT_NSSTRING_EQ("jumps over the", context.contextAfter);
+}
+
 TEST(DocumentEditingContext, RequestRectsInTextAreaAcrossWordWrappedLine)
 {
     auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
diff --git a/Tools/TestWebKitAPI/ios/UIKitSPI.h b/Tools/TestWebKitAPI/ios/UIKitSPI.h
index ad70dd5..1eb8314 100644
--- a/Tools/TestWebKitAPI/ios/UIKitSPI.h
+++ b/Tools/TestWebKitAPI/ios/UIKitSPI.h
@@ -223,6 +223,7 @@
 #endif // USE(APPLE_INTERNAL_SDK)
 
 #define UIWKDocumentRequestMarkedTextRects (1 << 5)
+#define UIWKDocumentRequestSpatialAndCurrentSelection (1 << 6)
 
 @interface UITextAutofillSuggestion ()
 + (instancetype)autofillSuggestionWithUsername:(NSString *)username password:(NSString *)password;