Commit 13947996 authored by Keren Zhu's avatar Keren Zhu Committed by Chromium LUCI CQ

Mac: Fix IME predictive completions in content

Issue: Predictive completions in some IMEs are not working properly
when typing in a webpage.

Cause: Appkit <-(sync)-> RWHVCocoa <-(async)-> Blink

In an IME composition commit, RWHV commits the text to Blink
through an async mojo call. Later when Blink finishes commit, it updates
the text in RWHV through a separate mojo call.

For predictive completions to work, IMEs calls RWHV's
|attributedSubstringForProposedRange:| to retrieve some text
as the context for prediction. Oftentimes the call happens before
Blink's async update to RWHV, resulting in a wrong predictive context.

This issue troubles both Chromium and Firefox but not Safari/Webkit.
Webkit is using an undocumented async IME API.

Also after macOS 10.12+, IMEs expect |selectedRange:| to return a empty
range positioned at the end of the committed text. If not respected the
predictive completions won't work.

Solution:
- Temporarily insert the committed text to RWHV's available text
- Follow the |selectedRange:| requirement

Bug: 710101
Change-Id: I3e4267362ea3e0a6b9f7cebce23fe59d1a6ece6a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2606140Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Commit-Queue: Keren Zhu <kerenzhu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#845832}
parent c75ca41f
...@@ -135,10 +135,11 @@ struct DidOverscrollParams; ...@@ -135,10 +135,11 @@ struct DidOverscrollParams;
// the whole content yet. // the whole content yet.
NSRange _markedRange; NSRange _markedRange;
// The text selection, cached from the RenderWidgetHostView. This is only ever // The text selection, cached from the RenderWidgetHostView.
// updated from the renderer. // |_availableText| contains the selected text and is a substring of the
base::string16 _textSelectionText; // full string in the renderer.
size_t _textSelectionOffset; base::string16 _availableText;
size_t _availableTextOffset;
gfx::Range _textSelectionRange; gfx::Range _textSelectionRange;
// The composition range, cached from the RenderWidgetHostView. This is only // The composition range, cached from the RenderWidgetHostView. This is only
......
...@@ -277,7 +277,7 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -277,7 +277,7 @@ void ExtractUnderlines(NSAttributedString* string,
if (!textCheckingTypes) if (!textCheckingTypes)
return; return;
NSString* availableText = base::SysUTF16ToNSString(_textSelectionText); NSString* availableText = base::SysUTF16ToNSString(_availableText);
if (!availableText) if (!availableText)
return; return;
...@@ -295,7 +295,7 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -295,7 +295,7 @@ void ExtractUnderlines(NSAttributedString* string,
base::scoped_nsobject<NSTextCheckingResult> scopedCandidateResult; base::scoped_nsobject<NSTextCheckingResult> scopedCandidateResult;
for (NSTextCheckingResult* result in textCheckingResults) { for (NSTextCheckingResult* result in textCheckingResults) {
NSTextCheckingResult* adjustedResult = NSTextCheckingResult* adjustedResult =
[result resultByAdjustingRangesWithOffset:_textSelectionOffset]; [result resultByAdjustingRangesWithOffset:_availableTextOffset];
if (!NSLocationInRange(cursorLocation, if (!NSLocationInRange(cursorLocation,
NSMakeRange(adjustedResult.range.location, NSMakeRange(adjustedResult.range.location,
adjustedResult.range.length + 1))) adjustedResult.range.length + 1)))
...@@ -344,13 +344,13 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -344,13 +344,13 @@ void ExtractUnderlines(NSAttributedString* string,
return; return;
NSRange availableTextRange = NSRange availableTextRange =
NSMakeRange(_textSelectionOffset, _textSelectionText.length()); NSMakeRange(_availableTextOffset, _availableText.length());
if (NSMaxRange(correction.range) > NSMaxRange(availableTextRange)) if (NSMaxRange(correction.range) > NSMaxRange(availableTextRange))
return; return;
NSAttributedString* attString = [[[NSAttributedString alloc] NSAttributedString* attString = [[[NSAttributedString alloc]
initWithString:base::SysUTF16ToNSString(_textSelectionText)] autorelease]; initWithString:base::SysUTF16ToNSString(_availableText)] autorelease];
NSRange trailingRange = NSMakeRange( NSRange trailingRange = NSMakeRange(
NSMaxRange(correction.range), NSMaxRange(correction.range),
NSMaxRange(availableTextRange) - NSMaxRange(correction.range)); NSMaxRange(availableTextRange) - NSMaxRange(correction.range));
...@@ -358,7 +358,7 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -358,7 +358,7 @@ void ExtractUnderlines(NSAttributedString* string,
if (trailingRange.length > 0 && if (trailingRange.length > 0 &&
trailingRange.location < NSMaxRange(availableTextRange)) { trailingRange.location < NSMaxRange(availableTextRange)) {
NSRange trailingRangeInAvailableText = NSMakeRange( NSRange trailingRangeInAvailableText = NSMakeRange(
trailingRange.location - _textSelectionOffset, trailingRange.length); trailingRange.location - _availableTextOffset, trailingRange.length);
if (@available(macOS 10.12, *)) { if (@available(macOS 10.12, *)) {
NSString* trailingString = NSString* trailingString =
[attString.string substringWithRange:trailingRangeInAvailableText]; [attString.string substringWithRange:trailingRangeInAvailableText];
...@@ -386,12 +386,12 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -386,12 +386,12 @@ void ExtractUnderlines(NSAttributedString* string,
if (!touchBarItem.candidateListVisible) if (!touchBarItem.candidateListVisible)
return; return;
if (!_textSelectionRange.IsValid() || if (!_textSelectionRange.IsValid() ||
_textSelectionOffset > _textSelectionRange.GetMin()) _availableTextOffset > _textSelectionRange.GetMin())
return; return;
NSRange selectionRange = _textSelectionRange.ToNSRange(); NSRange selectionRange = _textSelectionRange.ToNSRange();
NSString* selectionText = base::SysUTF16ToNSString(_textSelectionText); NSString* selectionText = base::SysUTF16ToNSString(_availableText);
selectionRange.location -= _textSelectionOffset; selectionRange.location -= _availableTextOffset;
if (NSMaxRange(selectionRange) > selectionText.length) if (NSMaxRange(selectionRange) > selectionText.length)
return; return;
...@@ -446,8 +446,8 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -446,8 +446,8 @@ void ExtractUnderlines(NSAttributedString* string,
- (void)setTextSelectionText:(base::string16)text - (void)setTextSelectionText:(base::string16)text
offset:(size_t)offset offset:(size_t)offset
range:(gfx::Range)range { range:(gfx::Range)range {
_textSelectionText = text; _availableText = text;
_textSelectionOffset = offset; _availableTextOffset = offset;
_textSelectionRange = range; _textSelectionRange = range;
_substitutionWasApplied = NO; _substitutionWasApplied = NO;
[NSSpellChecker.sharedSpellChecker dismissCorrectionIndicatorForView:self]; [NSSpellChecker.sharedSpellChecker dismissCorrectionIndicatorForView:self];
...@@ -466,7 +466,7 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -466,7 +466,7 @@ void ExtractUnderlines(NSAttributedString* string,
return; return;
NSTextCheckingResult* selectedResult = anItem.candidates[index]; NSTextCheckingResult* selectedResult = anItem.candidates[index];
NSRange replacementRange = selectedResult.range; NSRange replacementRange = selectedResult.range;
replacementRange.location += _textSelectionOffset; replacementRange.location += _availableTextOffset;
[self insertText:selectedResult.replacementString [self insertText:selectedResult.replacementString
replacementRange:replacementRange]; replacementRange:replacementRange];
...@@ -488,14 +488,13 @@ void ExtractUnderlines(NSAttributedString* string, ...@@ -488,14 +488,13 @@ void ExtractUnderlines(NSAttributedString* string,
} }
- (base::string16)selectedText { - (base::string16)selectedText {
gfx::Range textRange(_textSelectionOffset, gfx::Range textRange(_availableTextOffset,
_textSelectionOffset + _textSelectionText.size()); _availableTextOffset + _availableText.size());
gfx::Range intersectionRange = textRange.Intersect(_textSelectionRange); gfx::Range intersectionRange = textRange.Intersect(_textSelectionRange);
if (intersectionRange.is_empty()) if (intersectionRange.is_empty())
return base::string16(); return base::string16();
return _textSelectionText.substr( return _availableText.substr(intersectionRange.start() - _availableTextOffset,
intersectionRange.start() - _textSelectionOffset, intersectionRange.length());
intersectionRange.length());
} }
- (void)setCompositionRange:(gfx::Range)range { - (void)setCompositionRange:(gfx::Range)range {
...@@ -1824,19 +1823,9 @@ extern NSString* NSTextInputReplacementRangeAttributeName; ...@@ -1824,19 +1823,9 @@ extern NSString* NSTextInputReplacementRangeAttributeName;
gfx::Range expectedRange; gfx::Range expectedRange;
const base::string16* expectedText; const base::string16* expectedText;
if (!_compositionRange.is_empty()) { expectedText = &_availableText;
// This method might get called after TextInputState.type is reset to none, size_t offset = _availableTextOffset;
// in which case there will be no composition range information expectedRange = gfx::Range(offset, offset + expectedText->size());
// https://crbug.com/698672
expectedText = &_markedText;
expectedRange = _compositionRange.Intersect(
gfx::Range(_compositionRange.start(),
_compositionRange.start() + expectedText->length()));
} else {
expectedText = &_textSelectionText;
size_t offset = _textSelectionOffset;
expectedRange = gfx::Range(offset, offset + expectedText->size());
}
gfx::Range gfxActualRange = expectedRange.Intersect(requestedRange); gfx::Range gfxActualRange = expectedRange.Intersect(requestedRange);
if (!gfxActualRange.IsValid()) if (!gfxActualRange.IsValid())
...@@ -1980,16 +1969,44 @@ extern NSString* NSTextInputReplacementRangeAttributeName; ...@@ -1980,16 +1969,44 @@ extern NSString* NSTextInputReplacementRangeAttributeName;
// Text inserting might be initiated by other source instead of keyboard // Text inserting might be initiated by other source instead of keyboard
// events, such as the Characters dialog. In this case the text should be // events, such as the Characters dialog. In this case the text should be
// sent as an input method event as well. // sent as an input method event as well.
// TODO(suzhe): It's hard for us to support replacementRange without accessing
// the full web content.
BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
NSString* im_text = isAttributedString ? [string string] : string; NSString* imText = isAttributedString ? [string string] : string;
if (_handlingKeyDown && replacementRange.location == NSNotFound) { if (_handlingKeyDown && replacementRange.location == NSNotFound) {
_textToBeInserted.append(base::SysNSStringToUTF16(im_text)); // The user uses keyboard to type in a char without an IME or select a word
// on the IME. Don't commit the change to the render, because the event is
// being processed in |keyEvent:|. The commit will happen later after
// |interpretKeyEvents:| returns.
_textToBeInserted.append(base::SysNSStringToUTF16(imText));
_shouldRequestTextSubstitutions = YES; _shouldRequestTextSubstitutions = YES;
} else { } else {
// The user uses mouse or touch bar to select a word on the IME.
gfx::Range replacement_range(replacementRange); gfx::Range replacement_range(replacementRange);
_host->ImeCommitText(base::SysNSStringToUTF16(im_text), replacement_range); _host->ImeCommitText(base::SysNSStringToUTF16(imText), replacement_range);
}
if (replacementRange.location == NSNotFound) {
// Cancel selection after a IME commit by setting a zero-length selection
// at the end of insertion point.
// This is required for macOS 10.12+, otherwise the predictive completions
// of IMEs won't work. See crbug.com/710101.
int insertEndpoint = _markedRange.location + [imText length];
_textSelectionRange = gfx::Range(insertEndpoint, insertEndpoint);
// IMEs read |_availableText| preceding the insertion point as the context
// for predictive completion. Unfortunately by the moment IME reads the
// text, Blink likely hasn't finished the commit so the IME will read a
// wrong context. We hack it by temporarily inserting the committed text
// into
// |_availableText|. This variable will ultimately be asynchronously updated
// by Blink.
// TODO(crbug.com/1169288): Mac's IME API is synchronous and it plays badly
// with async APIs between the browser and the renderer. Probably replace
// the sync |interpretKeyEvents:| with the async
// |handleEventByInputMethod:|, which is an undocumented API used in
// Webkit2.
if (_markedRange.location >= _availableTextOffset &&
_markedRange.location <= _availableTextOffset + _availableText.length())
_availableText.insert(_markedRange.location - _availableTextOffset,
base::SysNSStringToUTF16(imText));
} }
// Inserting text will delete all marked text automatically. // Inserting text will delete all marked text automatically.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment