diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 69c0636d..8dd0e91f 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -46,7 +46,6 @@ D44FB74A2791B829003C80A6 /* VXHanConvert in Frameworks */ = {isa = PBXBuildFile; productRef = D44FB7492791B829003C80A6 /* VXHanConvert */; }; D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */; }; D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */; }; - D45EB5C127A9894C00E28B17 /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45EB5BF27A9890C00E28B17 /* StringUtils.swift */; }; D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; }; D47D73A427A5D43900255A50 /* KeyHandlerBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */; }; @@ -60,6 +59,7 @@ D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; }; D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; }; D4A13D5A27A59F0B003BE359 /* InputMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* InputMethodController.swift */; }; + D4C9CAB127AAC9690058DFEA /* NSStringUtils in Frameworks */ = {isa = PBXBuildFile; productRef = D4C9CAB027AAC9690058DFEA /* NSStringUtils */; }; D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8827A838CF006DB1CF /* Localizable.strings */; }; D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8D27A838F0006DB1CF /* InfoPlist.strings */; }; D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */; }; @@ -169,7 +169,6 @@ D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PhraseReplacementMap.cpp; sourceTree = ""; }; D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = ""; }; D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerInput.swift; sourceTree = ""; }; - D45EB5BF27A9890C00E28B17 /* StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = ""; }; D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerBopomofoTests.swift; sourceTree = ""; }; @@ -186,6 +185,7 @@ D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = ""; }; D495583A27A5C6C4006ADE1C /* LanguageModelManager+Privates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LanguageModelManager+Privates.h"; sourceTree = ""; }; D4A13D5927A59D5C003BE359 /* InputMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMethodController.swift; sourceTree = ""; }; + D4C9CAAF27AAC8EC0058DFEA /* NSStringUtils */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NSStringUtils; path = Packages/NSStringUtils; sourceTree = ""; }; D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; D4E33D8B27A838D5006DB1CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; D4E33D8C27A838D8006DB1CF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; @@ -214,6 +214,7 @@ D427F7A927905E90004A2160 /* TooltipUI in Frameworks */, D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */, D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */, + D4C9CAB127AAC9690058DFEA /* NSStringUtils in Frameworks */, D427F7AE27907B8A004A2160 /* NotifierUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -272,7 +273,6 @@ D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */, D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */, D461B791279DAC010070E734 /* InputState.swift */, - D45EB5BF27A9890C00E28B17 /* StringUtils.swift */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, D44FB74427915555003C80A6 /* Preferences.swift */, D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, @@ -414,6 +414,7 @@ D427F7BF27908EAC004A2160 /* OpenCCBridge */, D44FB7482791B346003C80A6 /* VXHanConvert */, D47D73C027A71FFA00255A50 /* FSEventStreamHelper */, + D4C9CAAF27AAC8EC0058DFEA /* NSStringUtils */, ); name = Packages; sourceTree = ""; @@ -477,6 +478,7 @@ D427F7C027908EFC004A2160 /* OpenCCBridge */, D44FB7492791B829003C80A6 /* VXHanConvert */, D47D73C227A7200500255A50 /* FSEventStreamHelper */, + D4C9CAB027AAC9690058DFEA /* NSStringUtils */, ); productName = McBopomofo; productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */; @@ -661,7 +663,6 @@ D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 6ACC3D452793701600F1B140 /* ParselessLM.cpp in Sources */, - D45EB5C127A9894C00E28B17 /* StringUtils.swift in Sources */, D41355DE278EA3ED005E5CBD /* UserPhrasesLM.cpp in Sources */, 6ACC3D3F27914F2400F1B140 /* KeyValueBlobReader.cpp in Sources */, D41355D8278D74B5005E5CBD /* LanguageModelManager.mm in Sources */, @@ -1350,6 +1351,10 @@ isa = XCSwiftPackageProductDependency; productName = FSEventStreamHelper; }; + D4C9CAB027AAC9690058DFEA /* NSStringUtils */ = { + isa = XCSwiftPackageProductDependency; + productName = NSStringUtils; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 6A0D4E9415FC0CFA00ABF4B3 /* Project object */; diff --git a/Packages/NSStringUtils/.gitignore b/Packages/NSStringUtils/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/NSStringUtils/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/NSStringUtils/Package.swift b/Packages/NSStringUtils/Package.swift new file mode 100644 index 00000000..8dce049e --- /dev/null +++ b/Packages/NSStringUtils/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "NSStringUtils", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "NSStringUtils", + targets: ["NSStringUtils"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "NSStringUtils", + dependencies: []), + .testTarget( + name: "NSStringUtilsTests", + dependencies: ["NSStringUtils"]), + ] +) diff --git a/Packages/NSStringUtils/README.md b/Packages/NSStringUtils/README.md new file mode 100644 index 00000000..9ab0fe3f --- /dev/null +++ b/Packages/NSStringUtils/README.md @@ -0,0 +1,3 @@ +# NSStringUtils + +A description of this package. diff --git a/Packages/NSStringUtils/Sources/NSStringUtils/NSStringUtils.swift b/Packages/NSStringUtils/Sources/NSStringUtils/NSStringUtils.swift new file mode 100644 index 00000000..63a9d6b9 --- /dev/null +++ b/Packages/NSStringUtils/Sources/NSStringUtils/NSStringUtils.swift @@ -0,0 +1,50 @@ +import Cocoa + +public extension NSString { + + /// Converts the index in an NSString to the index in a Swift string. + /// + /// An Emoji might be compose by more than one UTF-16 code points, however + /// the length of an NSString is only the sum of the UTF-16 code points. It + /// causes that the NSString and Swift string representation of the same + /// string have different lengths once the string contains such Emoji. The + /// method helps to find the index in a Swift string by passing the index + /// in an NSString. + func characterIndex(from utf16Index:Int) -> (Int, String) { + let string = (self as String) + var length = 0 + for (i, character) in string.enumerated() { + length += character.utf16.count + if length > utf16Index { + return (i, string) + } + } + return (string.count, string) + } + + @objc func nextUtf16Position(for index: Int) -> Int { + var (fixedIndex, string) = characterIndex(from: index) + if fixedIndex < string.count { + fixedIndex += 1 + } + return string[.. Int { + var (fixedIndex, string) = characterIndex(from: index) + if fixedIndex > 0 { + fixedIndex -= 1 + } + return string[.. @import CandidateUI; +@import NSStringUtils; // C++ namespace usages using namespace std; @@ -576,7 +577,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if ([input isShiftHold]) { // Shift + left if (currentState.cursorIndex > 0) { - NSInteger previousPosition = [StringUtils previousUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + NSInteger previousPosition = [currentState.composingBuffer nextUtf16PositionFor:currentState.cursorIndex]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:previousPosition readings:[self _currentReadings]]; marking.tooltipForInputting = currentState.tooltip; stateCallback(marking); @@ -614,7 +615,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if ([input isShiftHold]) { // Shift + Right if (currentState.cursorIndex < currentState.composingBuffer.length) { - NSInteger nextPosition = [StringUtils nextUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + NSInteger nextPosition = [currentState.composingBuffer nextUtf16PositionFor:currentState.cursorIndex]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:nextPosition readings:[self _currentReadings]]; marking.tooltipForInputting = currentState.tooltip; stateCallback(marking); @@ -842,7 +843,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index > 0) { - index = [StringUtils previousUtf16PositionForIndex:index in:state.composingBuffer]; + index = [state.composingBuffer previousUtf16PositionFor:index]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; marking.tooltipForInputting = state.tooltipForInputting; stateCallback(marking); @@ -858,7 +859,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index < state.composingBuffer.length) { - index = [StringUtils nextUtf16PositionForIndex:index in:state.composingBuffer]; + index = [state.composingBuffer nextUtf16PositionFor:index]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; marking.tooltipForInputting = state.tooltipForInputting; stateCallback(marking); diff --git a/Source/StringUtils.swift b/Source/StringUtils.swift deleted file mode 100644 index e8ddf054..00000000 --- a/Source/StringUtils.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2022 and onwards The McBopomofo Authors. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -import Foundation - -/// Utilities to convert the length of an NSString and a Swift string. -class StringUtils: NSObject { - - /// Converts the index in an NSString to the index in a Swift string. - /// - /// An Emoji might be compose by more than one UTF-16 code points, however - /// the length of an NSString is only the sum of the UTF-16 code points. It - /// causes that the NSString and Swift string representation of the same - /// string have different lengths once the string contains such Emoji. The - /// method helps to find the index in a Swift string by passing the index - /// in an NSString. - static func convertToCharIndex(from utf16Index: Int, in string: String) -> Int { - var length = 0 - for (i, character) in string.enumerated() { - if length >= utf16Index { - return i - } - length += character.utf16.count - } - return string.count - } - - @objc (nextUtf16PositionForIndex:in:) - static func nextUtf16Position(for index: Int, in string: String) -> Int { - var index = convertToCharIndex(from: index, in: string) - if index < string.count { - index += 1 - } - let count = string[.. Int { - var index = convertToCharIndex(from: index, in: string) - if index > 0 { - index -= 1 - } - let count = string[..