Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ios): deletion of selected text #11179

Merged
merged 7 commits into from
Apr 18, 2024
Merged

fix(ios): deletion of selected text #11179

merged 7 commits into from
Apr 18, 2024

Conversation

jahorton
Copy link
Contributor

@jahorton jahorton commented Apr 5, 2024

Fixes #11095.

User Testing

TEST_SELECTION_REPLACEMENT: Attempt to repro #11095.

  1. Install the Keyman for iOS version associated with this PR for user testing.
  2. Type Something beautiful - it's fine to use predictive-text suggestions to help
  3. Select the second word: 'beautiful'.
  4. Press letter 'B'.

Expected result: Something B, with a space between Something and B.

TEST_SELECTION_BACKSPACE: Using the iOS Keyman app, verify that pressing backspace to delete a selection acts as intended - the selected text should be removed, with no further edits. Any preceding spaces should remain.

TEST_SELECTION_REPLACEMENT_MID: Replace selected text that starts in the middle of a word.

  1. Install the Keyman for iOS version associated with this PR for user testing.
  2. Type Something beautiful - it's fine to use predictive-text suggestions to help
  3. Select the part of the second word: 'utiful'.
  4. Press letter 'n'.

TEST_SELECTION_BACKSPACE_MID: Delete selected text that starts in the middle of a word.

  1. Install the Keyman for iOS version associated with this PR for user testing.
  2. Type Something beautiful - it's fine to use predictive-text suggestions to help
  3. Select the part of the second word: 'utiful'.
  4. Press backspace.

TEST_GENERAL_USE: Check that general usage of the iOS Keyman app acts as expected.

@jahorton jahorton requested a review from sgschantz as a code owner April 5, 2024 08:24
@keymanapp-test-bot keymanapp-test-bot bot added has-user-test user-test-required User tests have not been completed labels Apr 5, 2024
@keymanapp-test-bot
Copy link

keymanapp-test-bot bot commented Apr 5, 2024

User Test Results

Test specification and instructions

  • TEST_SELECTION_REPLACEMENT (PASSED): Tested with the attached PR build (Keyman 17.0.307-beta-test) on an iPhone mobile device and here is my observation: 1. Typed 'Something beautiful'. 2. Selected the second word 'beautiful'. Presse the letter B. 3. Verified that there is a space appears between Something and B.
  • TEST_SELECTION_BACKSPACE (PASSED): 1. Verified that a selected word can be deleted by pressing the backspace key, with no further edits.
  • TEST_SELECTION_REPLACEMENT_MID (PASSED): 1. Typed 'Something beautiful'. 2. Selected a part of the second word 'utiful'. 3. Pressed letter 'n. Verified that it showed 'Something bean' on the text input screen.
  • TEST_SELECTION_BACKSPACE_MID (PASSED): 1. Typed 'Something beautiful'. 2. Selected a part of the second word 'utiful'. 3. Pressed the backspace key. Verified that it deleted the selected word. It showed 'Something bea'.
  • 🟥 TEST_GENERAL_USE (FAILED): Retested with the attached PR build (Keyman 17.0.305-beta-test-11179) on an iPhone 13 Mobile device (iOS 17.4) and here is my observation: 1. Set Keyman as the system wide keyboard. 2. Opened Safari browser. 3. Switched to Keyman Keyboard. 4. Typed a text on the Search bar. 5. Locked the mobile device by pressing Lock key from the mobile. 6. Unlock the mobile. Here, I noticed that the Keyman Keyboard was no longer visible in the Keyboard panel. Seems to be an issue. (notes)
Retesting Template
@keymanapp-test-bot retest TEST_GENERAL_USE

@keymanapp-test-bot keymanapp-test-bot bot added this to the B17S5 milestone Apr 5, 2024
@jahorton jahorton linked an issue Apr 5, 2024 that may be closed by this pull request
8 tasks
@bharanidharanj
Copy link

Test Results

  • TEST_SELECTION_REPLACEMENT (PASSED): Tested with the attached PR build (17.0.303-beta-test-11179) on an iPhone 13 Mobile device and here is my observation: 1. Typed 'Something beautiful'. 2. Selected the second word 'beautiful'. 3. Pressed letter 'B'. Verified that there is space between Something and B on the text input screen. Seems to be working fine.

@bharanidharanj
Copy link

Test Results

  • TEST_SELECTION_BACKSPACE (FAILED): Tested with the attached PR build (Keyman 17.0.303-beta-test-11179) on an iPhone 13 mobile device (iOS 17.4) and here is my observation: 1. Opened the Keyman In-app. 2. Typed 'Something beautiful' on the screen. 3. Selected the second word 'beautiful'. 4. Pressed the backspace key to delete and noticed that the selected word still appeared on the screen. It seems that the backspace key is not working on the selected text at all. Seems to be an issue.

@bharanidharanj
Copy link

Test Results

  • TEST_GENERAL_USE (FAILED): Tested with the attached PR build (Keyman 17.0.303-beta-test-11179) on an iPhone 13 mobile device and here is my observation: 1. Opened the Keyman In-app. 2. Noticed that after installing the Khmer Angkor keyboard, the corresponding dictionary was missing in the suggestion bar. 3. Installed four Keyman Keyboards using Settings menu. 5. Set Keyman as the system wide keyboard. Opened Safari browser. Switch to Keyman keyboard. Noticed that there is only one keyboard (EuroLatin (SIL)) is shown in the keyboard menu list. This appears to be an issue.

..missing khmer_angkor dictionary in the suggestion bar

..missing other keyman keyboard in keyboard list

@keymanapp-test-bot keymanapp-test-bot bot removed the user-test-required User tests have not been completed label Apr 8, 2024
@jahorton
Copy link
Contributor Author

jahorton commented Apr 9, 2024

Test Results

  • TEST_GENERAL_USE (FAILED): Tested with the attached PR build (Keyman 17.0.303-beta-test-11179) on an iPhone 13 mobile device and here is my observation: 1. Opened the Keyman In-app. 2. Noticed that after installing the Khmer Angkor keyboard, the corresponding dictionary was missing in the suggestion bar. 3. Installed four Keyman Keyboards using Settings menu. 5. Set Keyman as the system wide keyboard. Opened Safari browser. Switch to Keyman keyboard. Noticed that there is only one keyboard (EuroLatin (SIL)) is shown in the keyboard menu list. This appears to be an issue.

..missing khmer_angkor dictionary in the suggestion bar

..missing other keyman keyboard in keyboard list

Huh, that's... useful test data to have, though the observed issues are unrelated to this PR. I'll make a separate tracking issue.

@jahorton
Copy link
Contributor Author

Whose bright idea was it at Apple to have textDocumentProxy.insertText("") - an empty string insert - do nothing when there's selected text active? 😞 Any other actual string will nuke the selection... but not that. So, yay, workaround time.

@keymanapp-test-bot retest all

I've fixed the error plaguing the second user-test now.

As for the third user test, please verify that "Allow Full Access" has been toggled - that would probably affect the system keyboard issue you saw. As for the Khmer model not downloading... perhaps there was a connection hiccup? Perhaps fully uninstall and reinstall the Keyman app, to be safe.

@keymanapp-test-bot keymanapp-test-bot bot added user-test-required User tests have not been completed and removed user-test-failed labels Apr 10, 2024
@bharanidharanj
Copy link

Test Results

  • TEST_SELECTION_REPLACEMENT (PASSED): Tested with the attached PR build (17.0.305-beta-test-11179) on an iPhone 13 Mobile (iOS 17.4) device and here is my observation: 1. Typed 'Something beautiful'. 2. Selected the second word 'beautiful'. 3. Pressed letter 'B'. Verified that there is space between Something and B on the text input screen. Seems to be working fine.

  • TEST_SELECTION_BACKSPACE (PASSED): 1. Opened the Keyman In-app. 2. Typed 'Something beautiful' on the screen. 3. Selected the second word 'beautiful'. 4. Pressed the backspace key to delete the word 'beautiful' and verified that it was deleted and the preceding space remained unchanged. Seems to be working as expected.

@bharanidharanj
Copy link

bharanidharanj commented Apr 10, 2024

Test Results

  • TEST_GENERAL_USE (FAILED): Retested with the attached PR build (Keyman 17.0.305-beta-test-11179) on an iPhone 13 Mobile device (iOS 17.4) and here is my observation: 1. Set Keyman as the system wide keyboard. 2. Opened Safari browser. 3. Switched to Keyman Keyboard. 4. Typed a text on the Search bar. 5. Locked the mobile device by pressing Lock key from the mobile. 6. Unlock the mobile. Here, I noticed that the Keyman Keyboard was no longer visible in the Keyboard panel. Seems to be an issue.

@keymanapp-test-bot keymanapp-test-bot bot added user-test-failed and removed user-test-required User tests have not been completed labels Apr 10, 2024
@jahorton
Copy link
Contributor Author

Test Results

  • TEST_GENERAL_USE (FAILED): Retested with the attached PR build (Keyman 17.0.305-beta-test-11179) on an iPhone 13 Mobile device (iOS 17.4) and here is my observation: 1. Set Keyman as the system wide keyboard. 2. Opened Safari browser. 3. Switched to Keyman Keyboard. 4. Typed a text on the Search bar. 5. Locked the mobile device by pressing Lock key from the mobile. 6. Unlock the mobile. Here, I noticed that the Keyman Keyboard was no longer visible in the Keyboard panel. Seems to be an issue.

Interesting that we haven't seen this one until now. There always have been odd interactions with that specific use case - see #2300 - and I can see the host-page getting reloaded / restored after locking and unlocking. It's temporarily blank for me, rather than permanently blank... but I'm also not sitting and waiting between locking & unlocking. If something were to prevent the host-page reload, that could certainly cause the issue. Of course, getting evidence of that is an entirely different matter.

@jahorton
Copy link
Contributor Author

I've created a separate issue corresponding to the blank-keyboard test result; this PR's changes affect nothing near the reported behavior.

@sgschantz
Copy link
Contributor

@jahorton The behavior described in TEST_SELECTION_BACKSPACE appears to differ from the behavior for the Apple keyboard. Should a backspace be treated differently than typing a letter key if text is selected?

The one silver lining: Apple makes it impossible for users to select text
in a way that splits character clusters.

So, we can just insert something that won't combine, like a ZWNJ, and then delete it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying that you are working around the fact that insertText of the empty string does not cause the selection to be replaced by doing an insert of something invisible that we can then delete? Does this only happen for backspace?

It sounds like you are detecting this backspace case (based on line 360) by saying we have an empty string to insert and the text selection is greater than zero. Is that only true for backspace?

This comment is hard to follow because the problem is identified, then the silver lining is mentioned and then the workaround. Also, it would be nice to keep the emotion or sarcasm out of the code as it can distract from the technical content -- even if the technical content is correct. The same is true for comments in issues or PRs. This is not productive for our relationships with our vendors.

Copy link
Contributor Author

@jahorton jahorton Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying that you are working around the fact that insertText of the empty string does not cause the selection to be replaced by doing an insert of something invisible that we can then delete?

Yes. That's exactly what it's saying.

Does this only happen for backspace?

It sounds like you are detecting this backspace case (based on line 360) by saying we have an empty string to insert and the text selection is greater than zero. Is that only true for backspace?

It's theoretically possible for other backspace-like keys to be defined, I suppose. Changes to keyboard options don't trigger insertText unless they also actually manipulate text - we don't need to worry about keystrokes that aren't intended to change the context in some manner, as Web doesn't send those through.

Suppose the following:

self.insertText(keymanWeb , /*numCharsToDelete:*/ 2, /*newText*/ "a")

If text is selected that immediately follows a space, we need to preserve that space in order to properly match how many characters the internal Web engine has said to delete. (In fact... we need to preserve it regardless in order to stay synch'd with the internal engine.) We have to do the deletions before our text-insertions, too.

So... I think this needs to happen this way regardless of whether or not we're inserting text down the line - otherwise, we run the risk of desynchronizing. If temptext is the context and text is selected, as this code currently is, at the time of this comment, the first char to delete will be silenced due to erasing the selected text while not deleting the preceding p.

So... thanks for raising the point that helped me catch this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it would be nice to keep the emotion or sarcasm out of the code as it can distract from the technical content -- even if the technical content is correct.

I have at times been guilty of this. It's not a good practice, I agree. Frustration can be expressed in Slack ;-) and we'll keep the code comments, issues, and PRs focused on technical.

So... I think this needs to happen this way regardless of whether or not we're inserting text down the line - otherwise, we run the risk of desynchronizing. If temptext is the context and text is selected, as this code currently is, at the time of this comment, the first char to delete will be silenced due to erasing the selected text while not deleting the preceding p.

.insertText() will delete selection if the string is not empty, right? And in that scenario I don't think it deletes the space prior to the selection. AFAICT, we only need to jump through the hoop when we are deleting selection with no insertion (generally, backspace)

}
sendContextUpdate()
return
} else if numCharsToDelete <= 0 || deleteSelection {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was already long, and the new additions would be easier to follow and maintain if they were encapsulated in their own methods. If someone is reading this code and not concerned with the edge case, then it would be helpful for the edge case to be contained separately. Some of the comments could also be reduced if the new methods were named in a way that indicated what their purpose was.

@jahorton
Copy link
Contributor Author

@jahorton The behavior described in TEST_SELECTION_BACKSPACE appears to differ from the behavior for the Apple keyboard. Should a backspace be treated differently than typing a letter key if text is selected?

That's because Apple does fancy handling with spaces. Type temptext and select just the text part, then hit backspace. The p of temp will remain. Interestingly... wait, the p remains with our current implementation's use of .deleteBackward(), too.

The original issue is about how we don't want that space deleted if we're replacing text - use of .deleteBackward() does nuke that space, after all. So, preserve the space if replacing text, not if removing text - that seems to be what you're saying there?

@jahorton
Copy link
Contributor Author

Before I forget, that makes things pretty darn tricky: the internal Web engine doesn't know or have Apple's default handling for that scenario. We're only safe to delete that space if we can ensure the edit is properly synchronized with the internal Web engine. At present... I'm pretty sure that would trigger a context reset, and we don't want that.

Due to limitations, and how late we are in the release cycle, I think we need to break from how Apple does it - otherwise, we risk triggering context-desyncs, which are quite possibly the mobile app's nastiest class of errors.

@sgschantz
Copy link
Contributor

Before I forget, that makes things pretty darn tricky: the internal Web engine doesn't know or have Apple's default handling for that scenario. We're only safe to delete that space if we can ensure the edit is properly synchronized with the internal Web engine. At present... I'm pretty sure that would trigger a context reset, and we don't want that.

Due to limitations, and how late we are in the release cycle, I think we need to break from how Apple does it - otherwise, we risk triggering context-desyncs, which are quite possibly the mobile app's nastiest class of errors.

I checked for consistency with Apple's keyboard because I didn't have a strong intuitive feel for whether the space should be deleted when backspacing with a word selected. Just for comparison, I looked at Pages and found the same behavior. Then I looked at TextEdit and saw that the backspace does not remove the space preceeding the selected word. Should this be the same for desktop and mobile? I don't know.

It seems more consistent to say, in both cases, that the key typed should be applied after deleting the selected text, meaning the backspace will remove the preceeding space. However, when typing in a text editor on a desktop platform, if I select a word and hit backspace, I don't think I want that. In that moment, I expect the backspace key to act like a delete key (which is what the key is actually called on the Mac, anyway).

So I agree -- we should go with whatever is safest for now. We can map out all the scenarios in the future when we deal in depth with selected text behavior.

@darcywong00 darcywong00 modified the milestones: B17S5, B17S6 Apr 12, 2024
@keymanapp-test-bot keymanapp-test-bot bot added the user-test-required User tests have not been completed label Apr 12, 2024
@jahorton
Copy link
Contributor Author

jahorton commented Apr 12, 2024

@keymanapp-test-bot retest TEST_SELECTION_REPLACEMENT TEST_SELECTION_BACKSPACE

The selection-text removal code has been changed, so it's best to double-check that the new pattern holds for all cases. A couple of new unit tests have been added, now that new potential "wrinkles" have been identified.

Ideally, I should also add an extra test case that includes use of a key that deletes more than one character yet isn't a backspace - that would be a real stress-test.

Worth noting: we treat the context as nil if there's an active text selection, so that key must not depend on existing context... but wait. If the context acts as nil, the key probably can't request to delete text it doesn't believe exists, so... huh. That test might not be possible at the moment.

@bharanidharanj
Copy link

Test Results

  • TEST_SELECTION_REPLACEMENT (PASSED): Tested with the attached PR build (Keyman 17.0.307-beta-test) on an iPhone mobile device and here is my observation: 1. Typed 'Something beautiful'. 2. Selected the second word 'beautiful'. Presse the letter B. 3. Verified that there is a space appears between Something and B.
  • TEST_SELECTION_BACKSPACE (PASSED): 1. Verified that a selected word can be deleted by pressing the backspace key, with no further edits.
  • TEST_SELECTION_REPLACEMENT_MID (PASSED): 1. Typed 'Something beautiful'. 2. Selected a part of the second word 'utiful'. 3. Pressed letter 'n. Verified that it showed 'Something bean' on the text input screen.
  • TEST_SELECTION_BACKSPACE_MID (PASSED): 1. Typed 'Something beautiful'. 2. Selected a part of the second word 'utiful'. 3. Pressed the backspace key. Verified that it deleted the selected word. It showed 'Something bea'.

@keymanapp-test-bot keymanapp-test-bot bot removed the user-test-required User tests have not been completed label Apr 12, 2024
@bharanidharanj
Copy link

@jahorton It appears that the last test, TEST_GENERAL_USE, seems to be resolved in Keyman 17.0.307-beta-test-11179 and is functioning correctly. Could you please reassign this test? Thanks.

@mcdurdin
Copy link
Member

Whose bright idea was it at Apple to have textDocumentProxy.insertText("") - an empty string insert - do nothing when there's selected text active? 😞 Any other actual string will nuke the selection... but not that. So, yay, workaround time.

Have we reported this to Apple as a bug? May be worth doing so.

Comment on lines 356 to 359
Compare to Web: Web states an exact number of characters to delete
before the start of the currently-selected range... and it does this
completely unaware of the nuances listed above for .deleteBackward().
Keyman keyboard rules are likewise unaware of Apple's nuances.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a side-track.

Suggested change
Compare to Web: Web states an exact number of characters to delete
before the start of the currently-selected range... and it does this
completely unaware of the nuances listed above for .deleteBackward().
Keyman keyboard rules are likewise unaware of Apple's nuances.
Our policy (#9073) on handling the backspace key when there is a
text selection is to just delete the selection. We have to override
the special case of space being deleted by .deleteBackward() ourselves.

Comment on lines 361 to 365
In order to maintain proper synchronization between app context and
internal Web-engine context, we need to force selected-text deletion
to NEVER delete preceding spaces. Any attempts to adjust and include
the aforementioned nuance will need considerable design work to "get
right" due to the risk for adverse affects with Keyman keyboard rules.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In order to maintain proper synchronization between app context and
internal Web-engine context, we need to force selected-text deletion
to NEVER delete preceding spaces. Any attempts to adjust and include
the aforementioned nuance will need considerable design work to "get
right" due to the risk for adverse affects with Keyman keyboard rules.

I don't think this comment is needed. Just the technical background.

Copy link
Contributor Author

@jahorton jahorton Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... this is technical background, just from a different starting point. As the primary maintainer of the Web/mobile-app interface, this was the reason I implemented the solution as-is, after all - I wasn't even aware of the decision from #9073 when I made this. It just so happens that both reasons align.

Comment on lines +404 to +405
// `true` if there was selected text to be deleted
let deletedSelection = self.deleteSelection()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need to do this call to deleteSelection() if we (a) there is a selection, and (b) we have no new text to insert, right? We really don't want to jump through this hoop if we don't have to.

Copy link
Contributor Author

@jahorton jahorton Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why the guard exists at the start of deleteSelection. I placed it within deleteSelection rather than outside it.

Refer to #11179 (comment).

Comment on lines +407 to 408
if numCharsToDelete <= 0 || deletedSelection {
textDocumentProxy.insertText(newText)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems that if (a) we have no selection, and (b) no deletion, and (c) text to insert, then it won't insert the new text?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it just comes later, after deleting preceding text. In the case that no such deletions are needed, this section allows us to return early before all of the delete-left handling kicks in. No edits were made near the later code block, so it doesn't show up in the default code-review code-windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But... I think there is something off here; we probably shouldn't condition on the deletedSelection bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right. If we do have a selection, we currently act as if the context is nil and thus can't back delete, so it's probably a moot point.

Copy link
Contributor Author

@jahorton jahorton Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "enforcer" for "selected text" = "empty context", at least within the internal Web engine:

private KC_(n: number, ln: number, outputTarget: OutputTarget): string {
var tempContext = '';
// If we have a selection, we have an empty context
tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : "";

For a history trace, git blame points to #6272.

@jahorton
Copy link
Contributor Author

Whose bright idea was it at Apple to have textDocumentProxy.insertText("") - an empty string insert - do nothing when there's selected text active? 😞 Any other actual string will nuke the selection... but not that. So, yay, workaround time.

Have we reported this to Apple as a bug? May be worth doing so.

I have now submitted FB13734388 in regard to this.

Comment on lines +404 to +405
// `true` if there was selected text to be deleted
let deletedSelection = self.deleteSelection()
Copy link
Contributor Author

@jahorton jahorton Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why the guard exists at the start of deleteSelection. I placed it within deleteSelection rather than outside it.

Refer to #11179 (comment).

@@ -333,6 +333,60 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
}
}

func deleteSelection() -> Bool {
if let selected = textDocumentProxy.selectedText, selected.count > 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard ensures we only try to delete selected text only if there actually is selected text - a selection exists with length > 0. If it doesn't exist, we exit and indicate that no deletion happened.

@jahorton jahorton requested a review from mcdurdin April 18, 2024 03:49
Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jahorton jahorton merged commit 7aa6b66 into beta Apr 18, 2024
4 of 5 checks passed
@jahorton jahorton deleted the fix/ios/selection-deletion branch April 18, 2024 04:01
@keyman-server
Copy link
Collaborator

Changes in this pull request will be available for download in Keyman version 17.0.310-beta

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug(ios): No space between words when replacing selected text with a letter
6 participants