mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-12 08:57:30 +00:00
Merge pull request #16634 from keymanapp/fix/mac/erratic-backspace
fix(mac): apply backspace to correct location 🍎
This commit is contained in:
commit
21e1e2e8b6
9 changed files with 305 additions and 84 deletions
|
|
@ -1 +1 @@
|
|||
github "erikdoe/ocmock" "v3.9.1"
|
||||
github "erikdoe/ocmock" "v3.9.4"
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n(cd .. ; ./is_same_version.sh Keyman4MacIM || exit 1 ; cd -)\n";
|
||||
shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n# TODO: use xcode-wrap.sh (follow-up in #16712)\nexport PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\necho \"BASH version inside Xcode is: $BASH_VERSION\"\n(cd .. ; ./is_same_version.sh Keyman4MacIM || exit 1 ; cd -)\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D2FF67BB6C749BA6861F5E25 /* [CP] Check Pods Manifest.lock */ = {
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
|
|||
NSEvent* sysEvent = [NSEvent eventWithCGEvent:event];
|
||||
// Too many of these to be useful for most debugging sessions, but we'll keep this around to be
|
||||
// un-commented when needed.
|
||||
os_log_debug([KMLogs keyTraceLog], "System Event: %{public}@", sysEvent);
|
||||
// os_log_debug([KMLogs keyTraceLog], "System Event: %{public}@", sysEvent);
|
||||
|
||||
switch (type) {
|
||||
case kCGEventFlagsChanged:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "KMInputMethodAppDelegate.h"
|
||||
|
||||
// used to perform delete by inserting with replacement
|
||||
typedef struct {
|
||||
BOOL canDeleteWithReplacement;
|
||||
NSUInteger replacementLocation;
|
||||
NSUInteger replacementLength;
|
||||
NSString *replacementString;
|
||||
} ReplacementInfo;
|
||||
|
||||
@interface KMInputMethodEventHandler : NSObject
|
||||
|
||||
- (instancetype)initWithClient:(NSString *)clientAppId client:(id)sender;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ CGKeyCode _keyCodeOfOriginalEvent;
|
|||
CGEventSourceRef _sourceFromOriginalEvent = nil;
|
||||
CGEventSourceRef _sourceForGeneratedEvent = nil;
|
||||
|
||||
// This is the public initializer.
|
||||
/**
|
||||
* The public initializer
|
||||
*/
|
||||
- (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
|
|
@ -109,7 +111,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
}
|
||||
|
||||
output = [self processEventWithKeymanEngine:event in:sender];
|
||||
os_log_debug([KMLogs keyTraceLog], "keyman engine output: %{public}@", output);
|
||||
os_log_debug([KMLogs keyTraceLog], "🅰️ keyman engine output: %{public}@", output);
|
||||
|
||||
if (output == nil) {
|
||||
return NO;
|
||||
|
|
@ -331,52 +333,69 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
// if we can read the text, then get the context and send it to Core
|
||||
// we do this whether the context has changed or not
|
||||
if (self.apiCompliance.canReadText) {
|
||||
contextString = [self readContext:event forClient:client];
|
||||
os_log_debug([KMLogs keyTraceLog], "reportContext, setting new context for compliant app (if needed)");
|
||||
contextString = [self readContext:client outSelection:NULL];
|
||||
[self.kme setCoreContextIfNeeded:contextString];
|
||||
} else if (self.contextChanged) {
|
||||
|
||||
// the context is now current, reset the flag
|
||||
self.contextChanged = NO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.contextChanged) {
|
||||
// we cannot read the text but know the context has changed, so we must clear it
|
||||
os_log_debug([KMLogs keyTraceLog], "reportContext, clearing context for non-compliant app");
|
||||
[self.kme clearCoreContext];
|
||||
|
||||
// the context is now current, reset the flag
|
||||
self.contextChanged = NO;
|
||||
}
|
||||
|
||||
// the context is now current, reset the flag
|
||||
self.contextChanged = NO;
|
||||
}
|
||||
|
||||
-(NSString*)readContext:(NSEvent *)event forClient:(id) client {
|
||||
/**
|
||||
* Returns the current context string up to kMaxContext characters
|
||||
* Also returns the results of `[client selectedRange]` in the`selection` pointer
|
||||
*/
|
||||
|
||||
-(NSString*)readContext:(id) client outSelection:(NSRangePointer) outSelection {
|
||||
NSString *contextString = @"";
|
||||
NSAttributedString *attributedString = nil;
|
||||
NSRange selectionRange = {0};
|
||||
|
||||
if (!self.apiCompliance.canReadText) {
|
||||
os_log_debug([KMLogs keyTraceLog], "InputMethodEventHandler readContext, unable to read context: non-compliant app");
|
||||
return contextString;
|
||||
}
|
||||
|
||||
// if we can read the text, then get the context for up to kMaxContext characters
|
||||
if (self.apiCompliance.canReadText) {
|
||||
NSRange selectionRange = [client selectedRange];
|
||||
os_log_debug([KMLogs eventsLog], "InputMethodEventHandler readContext, canReadText: true selectionRange %{public}@: ", NSStringFromRange(selectionRange));
|
||||
|
||||
NSUInteger contextLength = MIN(kMaxContext, selectionRange.location);
|
||||
NSUInteger contextStart = selectionRange.location - contextLength;
|
||||
|
||||
if (contextLength > 0) {
|
||||
NSRange contextRange = NSMakeRange(contextStart, contextLength);
|
||||
os_log_debug([KMLogs eventsLog], " contextRange %{public}@: client: %p", NSStringFromRange(contextRange), client);
|
||||
attributedString = [client attributedSubstringFromRange:contextRange];
|
||||
selectionRange = [client selectedRange];
|
||||
os_log_debug([KMLogs keyTraceLog], "InputMethodEventHandler readContext, selectionRange %{public}@: ", NSStringFromRange(selectionRange));
|
||||
|
||||
// adjust string in case that we receive half of a surrogate pair at context start
|
||||
// the API appears to always return a full code point, but this could vary by app
|
||||
if (attributedString.length > 0) {
|
||||
if (CFStringIsSurrogateLowCharacter([attributedString.string characterAtIndex:0])) {
|
||||
os_log_debug([KMLogs eventsLog], " first char is low surrogate, reducing context by one character");
|
||||
contextString = [attributedString.string substringFromIndex:1];
|
||||
} else {
|
||||
contextString = attributedString.string;
|
||||
|
||||
//only uncomment for testing as we do not want to write context in logs
|
||||
//os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString);
|
||||
}
|
||||
NSUInteger contextLength = MIN(kMaxContext, selectionRange.location);
|
||||
NSUInteger contextStart = selectionRange.location - contextLength;
|
||||
|
||||
if (contextLength > 0) {
|
||||
NSRange contextRange = NSMakeRange(contextStart, contextLength);
|
||||
attributedString = [client attributedSubstringFromRange:contextRange];
|
||||
|
||||
// adjust string in case that we receive half of a surrogate pair at context start
|
||||
// the API appears to always return a full code point, but this could vary by app
|
||||
if (attributedString.length > 0) {
|
||||
if (CFStringIsSurrogateLowCharacter([attributedString.string characterAtIndex:0])) {
|
||||
os_log_debug([KMLogs keyTraceLog], " first char is low surrogate, reducing context by one character");
|
||||
contextString = [attributedString.string substringFromIndex:1];
|
||||
} else {
|
||||
contextString = attributedString.string;
|
||||
|
||||
//only uncomment for testing as we do not want to write context in logs
|
||||
//os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the range of the current selection
|
||||
if (outSelection != NULL) {
|
||||
*outSelection = selectionRange;
|
||||
}
|
||||
return contextString;
|
||||
}
|
||||
|
||||
|
|
@ -508,6 +527,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
* 3. generate event: generate keydown backspace events as necessary
|
||||
*/
|
||||
-(BOOL)handleDeleteOnlyScenario:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
|
||||
os_log_debug([KMLogs keyTraceLog], "🔙 handleDeleteOnlyScenario");
|
||||
|
||||
// attempt to delete by replacing -- for compliant apps only
|
||||
if ([self handleDeleteWithReplacement:output keyDownEvent:event client:client]) {
|
||||
|
|
@ -566,35 +586,51 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
* Ref: https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:)
|
||||
*/
|
||||
-(BOOL)handleDeleteWithReplacement:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
|
||||
BOOL handledEvent = NO;
|
||||
NSString *context = [self readContext:event forClient:client];
|
||||
|
||||
// guard: only for compliant apps with sufficient context
|
||||
if (!(self.apiCompliance.canReplaceText) || ([context length] <= output.textToDelete.length)) {
|
||||
os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant or insufficient context");
|
||||
// guard: only for compliant apps
|
||||
if (!self.apiCompliance.canReadText) {
|
||||
os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant");
|
||||
return NO; // return without deleting/replacing
|
||||
}
|
||||
|
||||
|
||||
NSRange currentSelection = {0};
|
||||
NSString *context = [self readContext:client outSelection:¤tSelection];
|
||||
os_log_debug([KMLogs keyTraceLog], "handleDeleteWithReplacement, currentSelection: %{public}@: ", NSStringFromRange(currentSelection));
|
||||
|
||||
// guard: if text is currently selected, then accept default delete behavior of removing selected text
|
||||
if (currentSelection.length > 0) {
|
||||
os_log_debug([KMLogs keyTraceLog], "text is currently selected, accept default system handling to delete it");
|
||||
return NO; // return without deleting/replacing
|
||||
}
|
||||
|
||||
// guard: must have sufficient context
|
||||
if ([context length] <= output.textToDelete.length) {
|
||||
os_log_debug([KMLogs keyTraceLog], "cannot replace text, insufficient context");
|
||||
return NO; // return without deleting/replacing
|
||||
}
|
||||
|
||||
// guard: the logic of this method depends on locating textToDelete in the context
|
||||
if (![self stringToDeleteMatchesContextSuffix:output.textToDelete context:context]) {
|
||||
os_log_debug([KMLogs keyTraceLog], "cannot replace text, textToDelete not found at end of context");
|
||||
return NO; // return without deleting/replacing
|
||||
}
|
||||
|
||||
// the length of the text we are to delete
|
||||
NSUInteger deletionTargetLength = output.textToDelete.length;
|
||||
NSUInteger deletionTargetLocation = context.length-deletionTargetLength;
|
||||
NSUInteger precedingCharacterLocation = deletionTargetLocation - 1;
|
||||
// the location of the text we are to delete
|
||||
NSUInteger locationOfDeletionTarget = context.length-deletionTargetLength;
|
||||
|
||||
// if the preceding character is the trailing half of a surrogate pair
|
||||
// then delete by replacing with the entire surrogate pair
|
||||
if ([self precededBySurrogatePair:precedingCharacterLocation context:context]) {
|
||||
handledEvent = [self deleteByReplacingWithPrecedingSurrogate:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
|
||||
} else {
|
||||
// otherwise replace with only the preceding character
|
||||
handledEvent = [self deleteByReplacingWithPrecedingCharacter:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
|
||||
ReplacementInfo replacementInfo = [self evaluateForReplaceability: context textStoreLocation:currentSelection.location deleteLength:deletionTargetLength locationOfDeletionTarget:locationOfDeletionTarget];
|
||||
|
||||
if (replacementInfo.canDeleteWithReplacement) {
|
||||
id<NSTextInputClient> textInputClient = (id)client;
|
||||
NSRange replacementRange = NSMakeRange(replacementInfo.replacementLocation, replacementInfo.replacementLength);
|
||||
|
||||
// perform the delete with replacement
|
||||
os_log_debug([KMLogs keyTraceLog], "insertText with replacementString: %{public}@, replacementRange.location: %lu, replacementRange.length: %lu", replacementInfo.replacementString, replacementRange.location, replacementRange.length);
|
||||
[textInputClient insertText:replacementInfo.replacementString replacementRange:replacementRange];
|
||||
}
|
||||
|
||||
return handledEvent;
|
||||
|
||||
return replacementInfo.canDeleteWithReplacement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -616,6 +652,35 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
return doesMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate whether the target can be deleted with a replacement, and, if it can, include the information
|
||||
* that describes how to perform the replacement.
|
||||
*/
|
||||
- (ReplacementInfo)evaluateForReplaceability:(NSString*) context textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength locationOfDeletionTarget:(NSUInteger)deletionLocation {
|
||||
|
||||
ReplacementInfo replacementInfo;
|
||||
|
||||
NSUInteger precedingLocation = deletionLocation - 1;
|
||||
os_log_debug([KMLogs keyTraceLog], "evaluateForReplaceability of deleteLength: %lu at precedingLocation: %lu", (unsigned long)deleteLength, (unsigned long)precedingLocation);
|
||||
|
||||
// if the preceding character is the trailing half of a surrogate pair
|
||||
// then delete by replacing with the entire surrogate pair
|
||||
if ([self precededBySurrogatePair:precedingLocation context:context]) {
|
||||
replacementInfo = [self determineReplacementInfoWithPrecedingSurrogate:precedingLocation textStoreLocation:textStoreLocation deleteLength:deleteLength context:context];
|
||||
} else {
|
||||
// otherwise replace with only the preceding character
|
||||
replacementInfo = [self determineReplacementInfoWithPrecedingCharacter:precedingLocation textStoreLocation:textStoreLocation deleteLength:deleteLength context:context];
|
||||
}
|
||||
|
||||
if (replacementInfo.canDeleteWithReplacement) {
|
||||
os_log_debug([KMLogs keyTraceLog], "⭐️ evaluateForReplaceability, canDeleteWithReplacement = YES, replacementString = %{public}@, replacementLength: %lu, replacementLocation: %lu", replacementInfo.replacementString, replacementInfo.replacementLength, replacementInfo.replacementLocation);
|
||||
} else {
|
||||
os_log_debug([KMLogs keyTraceLog], "evaluateForReplaceability, canDeleteWithReplacement = NO");
|
||||
}
|
||||
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the preceding character, which is to be used for the replacement,
|
||||
* is part of a surrogate pair that is distinct from the character being deleted.
|
||||
|
|
@ -640,49 +705,53 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
}
|
||||
|
||||
/**
|
||||
* Replace both the text to delete and the character preceding it solely with the character that precedes it.
|
||||
* Returns YES if executing the replace/delete and NO otherwise.
|
||||
* Determine the replacement info using the character preceding the string to be deleted.
|
||||
*/
|
||||
-(BOOL) deleteByReplacingWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
|
||||
|
||||
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter");
|
||||
// get the preceding character
|
||||
-(ReplacementInfo) determineReplacementInfoWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context {
|
||||
ReplacementInfo replacementInfo = {NO, 0, 0, NULL};
|
||||
|
||||
// get the preceding character from the context
|
||||
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation, 1);
|
||||
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
|
||||
|
||||
|
||||
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter, precedingCharacterLocation = %lu, textStoreLocation = %lu, replacementString = %{public}@", precedingCharacterLocation, textStoreLocation, replacementString);
|
||||
|
||||
// guard: if preceding character is a control character, return NO
|
||||
if ([self containsControlCharacter:replacementString]) {
|
||||
NSString *message = @"replacementString contains control characters, cannot delete with replace";
|
||||
os_log_debug([KMLogs keyTraceLog], "%@", message);
|
||||
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
|
||||
return NO;
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
// perform the replacement
|
||||
// the length of the text to replace = the length of the replacement string + length of the string to delete
|
||||
NSUInteger replacementLength = [replacementString length] + deleteLength;
|
||||
NSRange replacementRange = NSMakeRange(precedingCharacterLocation, replacementLength);
|
||||
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
|
||||
[client insertText:replacementString replacementRange:replacementRange];
|
||||
// the location of the replacement is the current location in the text store
|
||||
// minus the length of the text we are deleting
|
||||
// minus the length of the preceding character (the one that makes this a replacement)
|
||||
NSUInteger replacementLocation = textStoreLocation - replacementLength;
|
||||
|
||||
return YES;
|
||||
replacementInfo.canDeleteWithReplacement = YES;
|
||||
replacementInfo.replacementString = replacementString;
|
||||
replacementInfo.replacementLength = replacementLength;
|
||||
replacementInfo.replacementLocation = replacementLocation;
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace both the text to delete and the surrogate pair preceding it with the surrogate pair preceding it.
|
||||
* Returns YES if executing the replace/delete and NO otherwise.
|
||||
* Determine the replacement info using the surrogate pair preceding the string to be deleted.
|
||||
*/
|
||||
-(BOOL) deleteByReplacingWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
|
||||
|
||||
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingSurrogate");
|
||||
-(ReplacementInfo) determineReplacementInfoWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context {
|
||||
ReplacementInfo replacementInfo = {NO, 0, 0, NULL};
|
||||
|
||||
// guard: return NO if there is no character before the precedingCharacterLocation
|
||||
if (precedingCharacterLocation <= 0) {
|
||||
NSString *message = @"no characters exist before precedingCharacterLocation, so it cannot be a surrogate pair";
|
||||
os_log_debug([KMLogs keyTraceLog], "%@", message);
|
||||
return NO;
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
// get the preceding character
|
||||
// get the preceding character from the context
|
||||
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation - 1, 2);
|
||||
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
|
||||
|
||||
|
|
@ -694,18 +763,24 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
|
|||
// found preceding surrogate as expected
|
||||
} else {
|
||||
NSString *message = [NSString stringWithFormat:@"Preceding characters of string do not comprise a surrogate pair: 0x%02x, 0x%02x", (unsigned int)highCharacter, (unsigned int)lowCharacter];
|
||||
os_log_debug([KMLogs keyTraceLog], "%@", message);
|
||||
os_log_error([KMLogs keyTraceLog], "%@", message);
|
||||
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
|
||||
return NO;
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
// perform the replacement
|
||||
// the length of the text to replace = the length of the replacement string + length of the string to delete
|
||||
NSUInteger replacementLength = [replacementString length] + deleteLength;
|
||||
NSRange replacementRange = NSMakeRange(precedingCharacterRange.location, replacementLength);
|
||||
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
|
||||
[client insertText:replacementString replacementRange:replacementRange];
|
||||
// the location of the replacement is the current location in the text store
|
||||
// minus the length of the text we are deleting
|
||||
// minus the length of the preceding character (the one that makes this a replacement)
|
||||
NSUInteger replacementLocation = textStoreLocation - replacementLength;
|
||||
|
||||
return YES;
|
||||
replacementInfo.canDeleteWithReplacement = YES;
|
||||
replacementInfo.replacementString = replacementString;
|
||||
replacementInfo.replacementLength = replacementLength;
|
||||
replacementInfo.replacementLocation = replacementLocation;
|
||||
|
||||
return replacementInfo;
|
||||
}
|
||||
|
||||
-(BOOL) containsControlCharacter:(NSString*)text {
|
||||
|
|
|
|||
|
|
@ -377,8 +377,12 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
|
|||
[clientAppId isEqual: @"com.google.Chrome"] ||
|
||||
// 2023-12-13[sgs]: must hard-code for Chrome because Google Docs returns relative but incorrect location so no way to auto-detect
|
||||
[clientAppId hasPrefix: @"net.java"] ||
|
||||
[clientAppId isEqual: @"com.Keyman.test.legacyInput"]
|
||||
[clientAppId isEqual: @"com.Keyman.test.legacyInput"] ||
|
||||
/*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */
|
||||
[clientAppId isEqual: @"com.tinyspeck.slackmacgap"] ||
|
||||
// #16731 2026-09-24[sgs]: Slack does double backspace when inserting with replacement
|
||||
[clientAppId isEqual: @"com.microsoft.VSCode"]
|
||||
// #16731 2026-09-24[sgs]: VSCode does double backspace when inserting with replacement
|
||||
);
|
||||
|
||||
os_log_debug([KMLogs complianceLog], "containedInHardCodedNoncompliantAppList: for app %{public}@: %{public}@", clientAppId, isAppNonCompliant?@"yes":@"no");
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ id testClient = nil;
|
|||
@interface InputMethodTests : XCTestCase
|
||||
@end
|
||||
|
||||
// redeclare private properties and functions to expose for testing
|
||||
@interface KMInputMethodEventHandler (Testing)
|
||||
@property (nonatomic, retain) TextApiCompliance* apiCompliance;
|
||||
@property (nonatomic, retain) NSString* clientApplicationId;
|
||||
|
|
@ -30,7 +31,10 @@ id testClient = nil;
|
|||
- (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender;
|
||||
- (NSRange) calculateInsertRangeForDeletedText:(NSString*)textToDelete selectionRange:(NSRange) selection;
|
||||
- (void)checkTextApiCompliance:(id)client;
|
||||
|
||||
- (ReplacementInfo)evaluateForReplaceability:(NSString*) context
|
||||
textStoreLocation:(NSUInteger)textStoreLocation
|
||||
deleteLength:(NSUInteger)deleteLength
|
||||
locationOfDeletionTarget:(NSUInteger)deletionLocation;
|
||||
@end
|
||||
|
||||
@implementation InputMethodTests
|
||||
|
|
@ -92,6 +96,134 @@ id testClient = nil;
|
|||
XCTAssertTrue(correctResult, @"insert or replacement range expected to be {1,2}");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_precededByControlCharacter_canReplaceFalse {
|
||||
NSString *context = @"test\ta";
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = 1;
|
||||
NSUInteger locationOfDeletion = 5;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
XCTAssertFalse(replacementInfo.canDeleteWithReplacement, @"target preceded by control character, canDelete should be false");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_precededByLatinCharacter_canReplaceTrue {
|
||||
NSString *context = @"testxa";
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = 1;
|
||||
NSUInteger locationOfDeletion = 5;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"x"], @"replacementString should be == 'x'");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 2, @"replacementLength should be 2");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_lowStoreLocation_correctReplacementLocation {
|
||||
NSString *context = @"testxa";
|
||||
NSUInteger storeLocation = 10;
|
||||
NSUInteger deletionLength = 1;
|
||||
NSUInteger locationOfDeletion = 5;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue(replacementInfo.replacementLocation==8, @"replacementLocation should be 8");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_highStoreLocation_correctReplacementLocation {
|
||||
NSString *context = @"testxa";
|
||||
NSUInteger storeLocation = 1000;
|
||||
NSUInteger deletionLength = 1;
|
||||
NSUInteger locationOfDeletion = 5;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue(replacementInfo.replacementLocation==998, @"replacementLocation should be 998");
|
||||
}
|
||||
|
||||
// Uses Malar Tirhuta script
|
||||
- (void)testEvaluateReplacement_latinCharacterPrecededBySurrogatePair_replacementLengthThree {
|
||||
NSString *context = @"test.𑒏a"; // 𑒏 : U+1148F : TIRHUTA LETTER
|
||||
NSString *stringToDelete = @"a";
|
||||
|
||||
NSRange range = [context rangeOfString:stringToDelete];
|
||||
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = stringToDelete.length;
|
||||
NSUInteger locationOfDeletion = range.location;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"𑒏"], @"replacementString is incorrect");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_surrogatePairPrecededByLatinCharacter_replacementLengthThree {
|
||||
NSString *context = @"test.a𑒏"; // 𑒏 : U+1148F : TIRHUTA LETTER
|
||||
NSString *stringToDelete = @"𑒏";
|
||||
|
||||
NSRange range = [context rangeOfString:stringToDelete];
|
||||
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = stringToDelete.length;
|
||||
NSUInteger locationOfDeletion = range.location;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString is incorrect");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_latinCharacterPrecededByEmoji_replacementLengthThree {
|
||||
NSString *context = @"test🏁a";
|
||||
NSString *stringToDelete = @"a";
|
||||
|
||||
NSRange range = [context rangeOfString:stringToDelete];
|
||||
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = stringToDelete.length;
|
||||
NSUInteger locationOfDeletion = range.location;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"🏁"], @"replacementString should be == '🏁'");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3");
|
||||
}
|
||||
|
||||
- (void)testEvaluateReplacement_emojiPrecededByLatinCharacter_replacementLengthThree {
|
||||
NSString *context = @"testa🏁";
|
||||
NSString *stringToDelete = @"🏁";
|
||||
|
||||
NSRange range = [context rangeOfString:stringToDelete];
|
||||
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = stringToDelete.length;
|
||||
NSUInteger locationOfDeletion = range.location;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString should be == 'a'");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3");
|
||||
}
|
||||
|
||||
// Note: though this test checks the calculation, deleting the jolly roger emoji will currently not behave as expected.
|
||||
// Keyman core instructs only part of the emoji to be deleted. The same happens with emoji with skin tones.
|
||||
// See #4956 for context
|
||||
- (void)testEvaluateReplacement_jollyRogerPrecededByLatinCharacter_replacementLengthSix {
|
||||
NSString *context = @"testa🏴☠️";
|
||||
NSString *stringToDelete = @"🏴☠️";
|
||||
|
||||
NSRange range = [context rangeOfString:stringToDelete];
|
||||
|
||||
NSUInteger storeLocation = 100;
|
||||
NSUInteger deletionLength = stringToDelete.length;
|
||||
NSUInteger locationOfDeletion = range.location;
|
||||
|
||||
ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion];
|
||||
|
||||
XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString should be == 'a'");
|
||||
XCTAssertTrue(replacementInfo.replacementLength == 6, @"replacementLength should == 6");
|
||||
}
|
||||
|
||||
/**
|
||||
* test compliance check of a KMInputMethodEventHandler with a nil client application ID
|
||||
* not sure if this can ever happen, but the lifecycle of the input method is not 100% clear,
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n(cd .. ; ./is_same_version.sh KeymanEngine4Mac || exit 1 ; cd -)\n";
|
||||
shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n# TODO: use xcode-wrap.sh (follow-up in #16712)\nexport PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\necho \"BASH version inside Xcode is: $BASH_VERSION\"\n(cd .. ; ./is_same_version.sh KeymanEngine4Mac || exit 1 ; cd -)\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E2588F2F1F2647A50061C9C4 /* ShellScript */ = {
|
||||
|
|
|
|||
|
|
@ -275,7 +275,9 @@ const int CORE_ENVIRONMENT_ARRAY_LENGTH = 6;
|
|||
-(void)setContextIfNeeded:(NSString*)context {
|
||||
unichar const * unicharContext = [self.coreHelper createUnicharStringFromNSString:context];
|
||||
km_core_status result = km_core_state_context_set_if_needed(self.coreState, unicharContext);
|
||||
os_log_debug([KMELogs coreLog], "setContextIfNeeded, context=%{public}@, km_core_state_context_set_if_needed result=%d", context, result);
|
||||
|
||||
// only uncomment for debugging to avoid logging context
|
||||
//os_log_debug([KMELogs coreLog], "setContextIfNeeded, context=%{public}@, km_core_state_context_set_if_needed result=%d", context, result);
|
||||
}
|
||||
|
||||
-(NSString*)contextDebug {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue