Skip to content

Commit

Permalink
[iOS] Tap to revert does not always display an accurate prompt for mu…
Browse files Browse the repository at this point in the history
…lti-word autocorrections

https://bugs.webkit.org/show_bug.cgi?id=259595
rdar://113036177

Reviewed by Wenson Hsieh.

UIKit requires additional information in order to detect that a tapped word is
part of a multi-word correction. Specifically, they require the entire
corrected string given a single tapped word, in order to look up the right
autocorrection record.

To support this requirement, add support for obtaining autocorrected ranges in
a document editing context. WebKit will provide `autocorrectedRanges` on
`UIWKDocumentContext`, when the `UIWKDocumentRequest` has
`UIWKDocumentRequestAutocorrectedRanges` specified in its flags. Then, with the
existing properties on `UIWKDocumentContext`, UIKit can determine the corrected
string nearest to the current selection.

* Source/WebCore/dom/DocumentMarkerController.cpp:
(WebCore::DocumentMarkerController::forEach):

Pass the node into the callback function so that it may be used to obtain a
`SimpleRange` from the `DocumentMarker`.

(WebCore::DocumentMarkerController::rangesForMarkersInRange):

Return a `SimpleRange` for each `DocumentMarker`.

(WebCore::DocumentMarkerController::hasMarkers):
(WebCore::DocumentMarkerController::clearDescriptionOnMarkersIntersectingRange):
* Source/WebCore/dom/DocumentMarkerController.h:
* Source/WebKit/Platform/spi/ios/UIKitSPI.h:
* Source/WebKit/Shared/DocumentEditingContext.h:
* Source/WebKit/Shared/DocumentEditingContext.mm:
(WebKit::DocumentEditingContext::toPlatformContext):
(IPC::ArgumentCoder<WebKit::DocumentEditingContext>::encode):
(IPC::ArgumentCoder<WebKit::DocumentEditingContext>::decode):
* Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm:
(toWebDocumentRequestOptions):
* Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm:

Resolve the `SimpleRange` from each marker as a character range, using a
`SimpleRange` representing the current context.

Populate `context.autocorrectedRanges` to be sent back to the UI process and UIKit.

(WebKit::WebPage::requestDocumentEditingContext):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm:
(TEST):
* Tools/TestWebKitAPI/ios/UIKitSPI.h:

Canonical link: https://commits.webkit.org/266398@main
  • Loading branch information
pxlcoder committed Jul 28, 2023
1 parent fdd0bc1 commit 6517574
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 7 deletions.
20 changes: 15 additions & 5 deletions Source/WebCore/dom/DocumentMarkerController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ Vector<RenderedDocumentMarker*> DocumentMarkerController::markersFor(Node& node,
return result;
}

void DocumentMarkerController::forEach(const SimpleRange& range, OptionSet<DocumentMarker::MarkerType> types, Function<bool(RenderedDocumentMarker&)> function)
void DocumentMarkerController::forEach(const SimpleRange& range, OptionSet<DocumentMarker::MarkerType> types, Function<bool(Node&, RenderedDocumentMarker&)> function)
{
if (!possiblyHasMarkers(types))
return;
Expand All @@ -480,7 +480,7 @@ void DocumentMarkerController::forEach(const SimpleRange& range, OptionSet<Docum
if (marker.startOffset() >= offsetRange.end)
break;
if (marker.endOffset() > offsetRange.start && types.contains(marker.type())) {
if (function(marker))
if (function(node, marker))
return;
}
}
Expand Down Expand Up @@ -508,13 +508,23 @@ Vector<RenderedDocumentMarker*> DocumentMarkerController::markersInRange(const S
{
// FIXME: Consider making forEach public and changing callers to use that function instead of this one.
Vector<RenderedDocumentMarker*> markers;
forEach(range, types, [&] (RenderedDocumentMarker& marker) {
forEach(range, types, [&] (Node&, RenderedDocumentMarker& marker) {
markers.append(&marker);
return false;
});
return markers;
}

Vector<SimpleRange> DocumentMarkerController::rangesForMarkersInRange(const SimpleRange& range, OptionSet<DocumentMarker::MarkerType> types)
{
Vector<SimpleRange> ranges;
forEach(range, types, [&] (Node& node, RenderedDocumentMarker& marker) {
ranges.append(makeSimpleRange(node, marker));
return false;
});
return ranges;
}

void DocumentMarkerController::removeMarkers(Node& node, OptionSet<DocumentMarker::MarkerType> types)
{
if (!possiblyHasMarkers(types))
Expand Down Expand Up @@ -726,7 +736,7 @@ void DocumentMarkerController::fadeAnimationTimerFired()
bool DocumentMarkerController::hasMarkers(const SimpleRange& range, OptionSet<DocumentMarker::MarkerType> types)
{
bool foundMarker = false;
forEach(range, types, [&] (RenderedDocumentMarker&) {
forEach(range, types, [&] (Node&, RenderedDocumentMarker&) {
foundMarker = true;
return true;
});
Expand All @@ -735,7 +745,7 @@ bool DocumentMarkerController::hasMarkers(const SimpleRange& range, OptionSet<Do

void DocumentMarkerController::clearDescriptionOnMarkersIntersectingRange(const SimpleRange& range, OptionSet<DocumentMarker::MarkerType> types)
{
forEach(range, types, [&] (RenderedDocumentMarker& marker) {
forEach(range, types, [&] (Node&, RenderedDocumentMarker& marker) {
marker.clearData();
return false;
});
Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/dom/DocumentMarkerController.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class DocumentMarkerController {

WEBCORE_EXPORT Vector<RenderedDocumentMarker*> markersFor(Node&, OptionSet<DocumentMarker::MarkerType> = DocumentMarker::allMarkers());
WEBCORE_EXPORT Vector<RenderedDocumentMarker*> markersInRange(const SimpleRange&, OptionSet<DocumentMarker::MarkerType>);
WEBCORE_EXPORT Vector<SimpleRange> rangesForMarkersInRange(const SimpleRange&, OptionSet<DocumentMarker::MarkerType>);
void clearDescriptionOnMarkersIntersectingRange(const SimpleRange&, OptionSet<DocumentMarker::MarkerType>);

WEBCORE_EXPORT void updateRectsForInvalidatedMarkersOfType(DocumentMarker::MarkerType);
Expand All @@ -101,7 +102,7 @@ class DocumentMarkerController {
};
Vector<TextRange> collectTextRanges(const SimpleRange&);

void forEach(const SimpleRange&, OptionSet<DocumentMarker::MarkerType>, const Function<bool(RenderedDocumentMarker&)>);
void forEach(const SimpleRange&, OptionSet<DocumentMarker::MarkerType>, const Function<bool(Node&, RenderedDocumentMarker&)>);
void forEachOfTypes(OptionSet<DocumentMarker::MarkerType>, const Function<bool(Node&, RenderedDocumentMarker&)>);

using MarkerMap = HashMap<RefPtr<Node>, std::unique_ptr<Vector<RenderedDocumentMarker>>>;
Expand Down
6 changes: 6 additions & 0 deletions Source/WebKit/Platform/spi/ios/UIKitSPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,12 @@ typedef NS_ENUM(NSUInteger, _UIScrollDeviceCategory) {

#endif

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
@interface UIWKDocumentContext (Staging_112795757)
@property (nonatomic, copy) NSArray<NSValue *> *autocorrectedRanges;
@end
#endif

WTF_EXTERN_C_BEGIN

BOOL UIKeyboardEnabledInputModesAllowOneToManyShortcuts(void);
Expand Down
5 changes: 4 additions & 1 deletion Source/WebKit/Shared/DocumentEditingContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct DocumentEditingContextRequest {
Annotation = 1 << 4,
MarkedTextRects = 1 << 5,
SpatialAndCurrentSelection = 1 << 6,
AutocorrectedRanges = 1 << 7,
};

OptionSet<Options> options;
Expand Down Expand Up @@ -82,6 +83,7 @@ struct DocumentEditingContext {
};

Vector<TextRectAndRange> textRects;
Vector<Range> autocorrectedRanges;
};

}
Expand Down Expand Up @@ -119,7 +121,8 @@ template<> struct EnumTraits<WebKit::DocumentEditingContextRequest::Options> {
WebKit::DocumentEditingContextRequest::Options::Spatial,
WebKit::DocumentEditingContextRequest::Options::Annotation,
WebKit::DocumentEditingContextRequest::Options::MarkedTextRects,
WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection
WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection,
WebKit::DocumentEditingContextRequest::Options::AutocorrectedRanges
>;
};

Expand Down
16 changes: 16 additions & 0 deletions Source/WebKit/Shared/DocumentEditingContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#import "UIKitSPI.h"
#import "WebCoreArgumentCoders.h"
#import <WebCore/ElementContext.h>
#import <wtf/cocoa/VectorCocoa.h>

namespace WebKit {

Expand Down Expand Up @@ -63,6 +64,17 @@ static inline NSRange toNSRange(DocumentEditingContext::Range range)

[platformContext setAnnotatedText:annotatedText.nsAttributedString().get()];

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
if (options.contains(DocumentEditingContextRequest::Options::AutocorrectedRanges)) {
auto ranges = createNSArray(autocorrectedRanges, [&] (DocumentEditingContext::Range range) {
return [NSValue valueWithRange:toNSRange(range)];
});

if ([platformContext respondsToSelector:@selector(setAutocorrectedRanges:)])
[platformContext setAutocorrectedRanges:ranges.get()];
}
#endif

return platformContext.autorelease();
#else
UNUSED_PARAM(options);
Expand Down Expand Up @@ -125,6 +137,7 @@ static inline NSRange toNSRange(DocumentEditingContext::Range range)
encoder << context.selectedRangeInMarkedText;

encoder << context.textRects;
encoder << context.autocorrectedRanges;
}

std::optional<WebKit::DocumentEditingContext> ArgumentCoder<WebKit::DocumentEditingContext>::decode(Decoder& decoder)
Expand Down Expand Up @@ -170,6 +183,9 @@ static inline NSRange toNSRange(DocumentEditingContext::Range range)
if (!decoder.decode(context.textRects))
return std::nullopt;

if (!decoder.decode(context.autocorrectedRanges))
return std::nullopt;

return context;
}

Expand Down
8 changes: 8 additions & 0 deletions Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@
#import <pal/ios/QuickLookSoftLink.h>
#import <pal/spi/ios/DataDetectorsUISoftLink.h>

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
#define UIWKDocumentRequestAutocorrectedRanges (1 << 7)
#endif

#if HAVE(LINK_PREVIEW) && USE(UICONTEXTMENU)
static NSString * const webkitShowLinkPreviewsPreferenceKey = @"WebKitShowLinkPreviews";
#endif
Expand Down Expand Up @@ -9613,6 +9617,10 @@ - (void)presentContextMenu:(UIContextMenuInteraction *)contextMenuInteraction at
options.add(WebKit::DocumentEditingContextRequest::Options::MarkedTextRects);
if (flags & UIWKDocumentRequestSpatialAndCurrentSelection)
options.add(WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection);
#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
if (flags & UIWKDocumentRequestAutocorrectedRanges)
options.add(WebKit::DocumentEditingContextRequest::Options::AutocorrectedRanges);
#endif

return options;
}
Expand Down
10 changes: 10 additions & 0 deletions Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5017,6 +5017,16 @@ static VisiblePositionRange constrainRangeToSelection(const VisiblePositionRange
context.annotatedText = m_textCheckingControllerProxy->annotatedSubstringBetweenPositions(contextBeforeStart, contextAfterEnd);
#endif

if (request.options.contains(DocumentEditingContextRequest::Options::AutocorrectedRanges)) {
if (auto contextRange = makeSimpleRange(contextBeforeStart, contextAfterEnd)) {
auto ranges = frame->document()->markers().rangesForMarkersInRange(*contextRange, DocumentMarker::MarkerType::CorrectionIndicator);
context.autocorrectedRanges = ranges.map([&] (auto& range) {
auto characterRangeInContext = characterRange(*contextRange, range);
return DocumentEditingContext::Range { characterRangeInContext.location, characterRangeInContext.length };
});
}
}

completionHandler(context);
}

Expand Down
73 changes: 73 additions & 0 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

#import "PlatformUtilities.h"
#import "TestCocoa.h"
#import "TestInputDelegate.h"
#import "TestWKWebView.h"
#import "UIKitSPI.h"
#import <WebKit/WKWebViewPrivateForTesting.h>
Expand Down Expand Up @@ -1541,6 +1542,78 @@ static void checkThatAllCharacterRectsAreConsistentWithSelectionRects(TestWKWebV
}
}

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)

#define UIWKDocumentRequestAutocorrectedRanges (1 << 7)

TEST(DocumentEditingContext, RequestAutocorrectedRanges)
{
if (![UIWKDocumentContext instancesRespondToSelector:@selector(autocorrectedRanges)])
return;

auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);

auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
[inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id<_WKFocusedElementInfo>) -> _WKFocusStartsInputSessionPolicy {
return _WKFocusStartsInputSessionPolicyAllow;
}];

[webView _setInputDelegate:inputDelegate.get()];
[webView synchronouslyLoadTestPageNamed:@"autofocused-text-input"];

auto *contentView = [webView textInputContentView];
[contentView insertText:@"Should we go to "];
[contentView insertText:@"sanfrancisco"];

[webView waitForNextPresentationUpdate];

__block bool appliedAutocorrection = false;
[webView applyAutocorrection:@"San Francisco" toString:@"sanfrancisco" isCandidate:YES withCompletionHandler:^{
appliedAutocorrection = true;
}];

TestWebKitAPI::Util::run(&appliedAutocorrection);

UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestAutocorrectedRanges, UITextGranularityParagraph, 1)];
NSArray<NSValue *> *autocorrectedRanges = context.autocorrectedRanges;

EXPECT_NOT_NULL(context);
EXPECT_EQ([autocorrectedRanges count], 1u);
EXPECT_TRUE(NSEqualRanges([autocorrectedRanges[0] rangeValue], NSMakeRange(16, 13)));

[contentView insertText:@" atfer"];

appliedAutocorrection = false;
[webView applyAutocorrection:@"after" toString:@"atfer" isCandidate:YES withCompletionHandler:^{
appliedAutocorrection = true;
}];

TestWebKitAPI::Util::run(&appliedAutocorrection);

context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestAutocorrectedRanges, UITextGranularityParagraph, 1)];
autocorrectedRanges = context.autocorrectedRanges;

EXPECT_NOT_NULL(context);
EXPECT_EQ([autocorrectedRanges count], 2u);
EXPECT_TRUE(NSEqualRanges([autocorrectedRanges[0] rangeValue], NSMakeRange(16, 13)));
EXPECT_TRUE(NSEqualRanges([autocorrectedRanges[1] rangeValue], NSMakeRange(30, 5)));

[contentView insertText:@" "];
[contentView insertText:@"work"];
[contentView insertText:@" "];
[contentView insertText:@"tomorrow?"];

TestWebKitAPI::Util::runFor(1_s);

context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestAutocorrectedRanges, UITextGranularityParagraph, 1)];
autocorrectedRanges = context.autocorrectedRanges;

EXPECT_NOT_NULL(context);
EXPECT_EQ([autocorrectedRanges count], 0u);
}

#endif // HAVE(AUTOCORRECTION_ENHANCEMENTS)

#if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)

TEST(DocumentEditingContext, RequestAnnotationsForTextChecking)
Expand Down
6 changes: 6 additions & 0 deletions Tools/TestWebKitAPI/ios/UIKitSPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,10 @@ typedef NS_ENUM(NSUInteger, _UIClickInteractionEvent) {

#endif

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
@interface UIWKDocumentContext (Staging_112795757)
@property (nonatomic, copy) NSArray<NSValue *> *autocorrectedRanges;
@end
#endif

#endif // PLATFORM(IOS_FAMILY)

0 comments on commit 6517574

Please sign in to comment.