Bug 25330

Summary: Implement text checking/autocorrection with new SnowLeopard text checking API
Product: WebKit Reporter: Justin Garcia <justin.garcia>
Component: HTML EditingAssignee: Douglas Davidson <ddavidso>
Status: RESOLVED FIXED    
Severity: Normal CC: ddavidso, sidchat
Priority: P2 Keywords: InRadar
Version: 528+ (Nightly build)   
Hardware: Mac   
OS: OS X 10.5   
Attachments:
Description Flags
patch
none
new patch taking into account suggestions from Justin
eric: review-
new patch to improve conformance to style guidelines
justin.garcia: review+
new patch taking into account suggestions from Justin justin.garcia: review+

Description Justin Garcia 2009-04-22 13:21:18 PDT
Implement text checking/autocorrection with new SnowLeopard text checking API

<rdar://problem/6724106>
Comment 1 Justin Garcia 2009-04-22 13:26:02 PDT
Created attachment 29688 [details]
patch
Comment 2 Douglas Davidson 2009-04-24 11:11:04 PDT
Created attachment 29750 [details]
new patch taking into account suggestions from Justin

Following Justin's suggestions, I've simplified "resultType" to "type" and used an enum for the values.
Comment 3 Darin Adler 2009-04-24 12:15:01 PDT
Comment on attachment 29750 [details]
new patch taking into account suggestions from Justin

Setting review? for Doug.
Comment 4 Eric Seidel (no email) 2009-04-24 12:36:11 PDT
Comment on attachment 29750 [details]
new patch taking into account suggestions from Justin

Mostly style complaints:

I would think the Editor stuff should be in its own EditorMac.cpp file, since none of the rest of the ports care about it.

WebKit style normally uses:
if (!ec) instead:
+            if (ec == 0) {
Personally I find (ec == 0) more readable in some cases, but we should follow the style guidelines here.

     if (client()->substitutionsPanelIsShowing()) {
 1154         client()->showSubstitutionsPanel(false);
 1155     } else {

no { } according to the style guidelines.

Why is this one different from all the others:
2     if (!client())
 1163         return false;
 1164     return client()->substitutionsPanelIsShowing();

return client() && ... would fit the pattern.

Even though much of the editing code hasn't been updated to the modern style guidelines, we still should try to follow them here:
 2227 void Editor::markAllMisspellingsAndBadGrammarInRanges(Range *spellingRange, bool markGrammar, Range *grammarRange, bool performTextChecking)

for C++ * goes next to the type in WebKit.  In Obj-C the * goes next to the variable name (to match the rest of AppKit/Cocoa)

Looks like you have some tabs in the file:
 2273 		restoreSelectionAfterChange = true;
 2274 		adjustSelectionForParagraphBoundaries = (selectionOffset >= paragraphLength) ? true : false;
 2275 	    }
the commit will fail if there are tabs in the file.

I would think this whole block would make more sense in a small function:
 uint64_t checkingTypes = markGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling;
 2281     if (performTextChecking) {
 2282         if (isAutomaticLinkDetectionEnabled())
 2283

something like uint64_t checkingTypes =  typesToCheck(markGrammar) or some better name...

It looks like you changed the encoding of the strings file?

Seems TextCheckingResult result; could really use a constructor function :)

This should all be a static inline funtion:
    if (action == @selector(toggleAutomaticQuoteSubstitution:)) {
 2649         NSMenuItem *menuItem = (NSMenuItem *)item;
 2650         if ([menuItem isKindOfClass:[NSMenuItem class]])
 2651             [menuItem setState:[self isAutomaticQuoteSubstitutionEnabled] ? NSOnState : NSOffState];
 2652         return YES;
 2653     }
cause you repeat the same copy/paste code a bunch there...

AGain here:
} else if (action == @selector(toggleAutomaticLinkDetection:)) {
 3767         BOOL checkMark = [self isAutomaticLinkDetectionEnabled];
 3768         if ([(NSObject *)item isKindOfClass:[NSMenuItem class]]) {
 3769             NSMenuItem *menuItem = (NSMenuItem *)item;
 3770             [menuItem setState:checkMark ? NSOnState : NSOffState];
 3771         }
 3772         return YES;
 3773     } 

And here:
4655     if (automaticDashSubstitutionEnabled == flag)
 4656         return;
 4657     automaticDashSubstitutionEnabled = flag;
 4658     [[NSUserDefaults standardUserDefaults] setBool:automaticDashSubstitutionEnabled forKey:WebAutomaticDashSubstitutionEnabled];    
 4659     [[NSSpellChecker sharedSpellChecker] updatePanels];

I think you're going to need to go more rounds with Darin/Justin (folks who actually still work at Apple).   But hopefully you'll find the style comments useful.
Comment 5 Douglas Davidson 2009-04-24 12:58:15 PDT
Much of this is Mac-specific now, but I would hope we'll be able to change that in the future.

The substitutionsPanel code you mention is the way it is because it is copied from the existing spellingPanel code elsewhere in the file.  I'll fix the extra braces by making that code match too.

I'll move the asterisks and remove the tabs.  Damn Xcode started putting in tabs when I told it not to.

The encoding of the .strings file is still UTF-16; I'm not sure why these diffs are showing up.

The repetitive copy/paste code is there to match corresponding existing code; I didn't really feel comfortable changing existing patterns, but if Darin or Justin want we can do it.
Comment 6 Douglas Davidson 2009-04-24 13:23:21 PDT
I see what happened with the .strings file, and I've fixed it.  I still haven't been able to get the update-webkit-localizable-strings script to work.
Comment 7 Douglas Davidson 2009-04-24 13:50:16 PDT
Created attachment 29761 [details]
new patch to improve conformance to style guidelines
Comment 8 Justin Garcia 2009-04-24 14:51:07 PDT
 about to give this a look thanks for your patience
Comment 9 Justin Garcia 2009-04-24 16:00:38 PDT
doug why the bit about storing the caret offset in order to restore the caret after the operation? are we not always one character after the autocorrection?  can't we then just rely on the selection set by the replacement operation + 1?  I guess autocorrection might kick in when we submit a form and in that case we're not + 1...
Comment 10 Douglas Davidson 2009-04-24 16:08:51 PDT
Yeah, I tried letting the selection be set by the replacement but there were cases where it didn't work.  You really want it to end up where it was before the replacement operations took place, taking into account any change in length the replacements may have caused.  I'd be delighted to find a simpler way of restoring the selection but I haven't come up with one.
Comment 11 Justin Garcia 2009-04-24 16:19:44 PDT
(In reply to comment #10)
> Yeah, I tried letting the selection be set by the replacement but there were
> cases where it didn't work.  You really want it to end up where it was before
> the replacement operations took place, taking into account any change in length
> the replacements may have caused.  I'd be delighted to find a simpler way of
> restoring the selection but I haven't come up with one.

what you did is fine although I'd like to know the cases where the replacement failed to restore a selection as they are bugs.
Comment 12 Justin Garcia 2009-04-24 17:37:21 PDT
Comment on attachment 29761 [details]
new patch to improve conformance to style guidelines

+    if (performTextChecking) {
+        if (m_frame->selection()->selectionType() == VisibleSelection::CaretSelection) {
+            RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), paragraphRange->startPosition());
+            offsetAsRange->setEnd(m_frame->selection()->end().containerNode(), m_frame->selection()->end().computeOffsetInContainerNode(), ec);
+            if (!ec) {
+                selectionOffset = TextIterator::rangeLength(offsetAsRange.get());
+                restoreSelectionAfterChange = true;         
+                adjustSelectionForParagraphBoundaries = (selectionOffset >= paragraphLength) ? true : false;
+            }
+        }
+    }

You only need to do this if there will be a replacement, but perhaps it would be cumbersome to find that out here/do this later once we've found that we'll be doing a replacement.


+ offsetAsRange->setEnd(m_frame->selection()->end().containerNode(), m_frame->selection()->end().computeOffsetInContainerNode(), ec)

Would look nice split out:

+ Position caretPosition = m_frame->selection()->end();
+ offsetAsRange->setEnd(caretPosition.containerNode(), caretPosition.computeOffsetInContainerNode(), ec);


"performTextChecking"

Is there any way we can make this bool more clearly describe that we're doing full text checking, not just spelling/grammar checking?  spelling/grammar checking sound like they'd also obey "performTextChecking".


+ } else if (performTextChecking && resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingRangeStartOffset &&

Should the check instead be resultLocation >= spellingRangeStartOffset?

I guess I'm slightly confused about what spellingRangeStartOffset actually is.  It looks like (in the !markGrammar case at least) to be the offset into the containing paragraph of the start of the spellingRange passed to this function.  Is that right?


+ RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRange.get(), resultLocation, resultLength);
+ VisibleSelection replacementSelection(replacementRange.get(), DOWNSTREAM);

I think better names might be rangeToReplace and selectionToReplace.


+ } else {
+    m_frame->selection()->moveTo(m_frame->selection()->end());
+    m_frame->selection()->modify(SelectionController::MOVE, SelectionController::FORWARD, CharacterGranularity);
+ }

This else case deserves a comment I think.


+void Editor::markMisspellingsAfterTypingToPosition(const VisiblePosition &p)

Did you need to move this.


 void TypingCommand::typingAddedToOpenCommand()
 {
-    markMisspellingsAfterTyping();
     document()->frame()->editor()->appliedEditing(this);
+    markMisspellingsAfterTyping();
 }

I think remember you explaining this somewhere but I couldn't find it.


enum TextCheckingType { TextCheckingTypeSpelling = 1 << 1, TextCheckingTypeGrammar = 1 << 2, TextCheckingTypeLink = 1 << 5, TextCheckingTypeQuote = 1 << 6, TextCheckingTypeDash = 1 << 7, TextCheckingTypeReplacement = 1 << 8, TextCheckingTypeCorrection = 1 << 9 };

Please split this out onto separate lines like the other enum definitions in WebCore.


#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)

Could we get a BUILDING_ON_SNOWLEOPARD?


Otherwise, r=me.
Comment 13 mitz 2009-04-24 17:40:14 PDT
(In reply to comment #12)

> Could we get a BUILDING_ON_SNOWLEOPARD?

Not until Snow Leopard is in "the past".
Comment 14 Justin Garcia 2009-04-24 17:45:51 PDT
What's up with this:

-    // FIXME 4811447: workaround for lack of API
+#ifndef BUILDING_ON_LEOPARD
+    [[NSSpellChecker sharedSpellChecker] updatePanels];
+#else
     NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
     if ([spellChecker respondsToSelector:@selector(_updateGrammar)])
         [spellChecker performSelector:@selector(_updateGrammar)];
+#endif

Comment 15 Douglas Davidson 2009-04-24 17:53:42 PDT
I will split out the code you point out, and find a more descriptive name than "performTextChecking".  

Your understanding about the spellingRangeStartOffset is correct; the condition for doing the various replacements is that the end of the region being replaced should touch the spelling range.  This is because the region being replaced may be inter-word punctuation such as dashes and quotes.

rangeToReplace and selectionToReplace are reasonable names.  I'll add comments and split the enum.  The reason for changing the order of markMisspellingsAfterTyping() is to make sure that changes are ordered properly for undoing.

The updatePanels is new API that we have that makes it unnecessary to use the old SPI.  We can split that out into a separate fix if you want.
Comment 16 Justin Garcia 2009-04-24 18:13:48 PDT
> Your understanding about the spellingRangeStartOffset is correct; the condition
> for doing the various replacements is that the end of the region being replaced
> should touch the spelling range.  This is because the region being replaced may
> be inter-word punctuation such as dashes and quotes.

Ah I understand now.  A comment there would be great.

> The reason for changing the order of markMisspellingsAfterTyping() is to make 
> sure that changes are ordered properly.

Ah of course.  Please add a comment.

> The updatePanels is new API that we have that makes it unnecessary to use the
> old SPI.  We can split that out into a separate fix if you want.

Up to you, either way please mention it in the ChangeLog.

Thanks!
Comment 17 Douglas Davidson 2009-04-24 18:47:27 PDT
Created attachment 29779 [details]
new patch taking into account suggestions from Justin
Comment 18 Adele Peterson 2009-04-27 16:15:28 PDT
Committed revision 42911.