Moves the Swift StringUtils class to a Swift package.

This commit is contained in:
zonble 2022-02-02 22:47:15 +08:00
parent 5849478fe5
commit f39b4fa0bc
9 changed files with 165 additions and 89 deletions

View File

@ -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 = "<group>"; };
D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = "<group>"; };
D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerInput.swift; sourceTree = "<group>"; };
D45EB5BF27A9890C00E28B17 /* StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = "<group>"; };
D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = "<group>"; };
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerBopomofoTests.swift; sourceTree = "<group>"; };
@ -186,6 +185,7 @@
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = "<group>"; };
D495583A27A5C6C4006ADE1C /* LanguageModelManager+Privates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LanguageModelManager+Privates.h"; sourceTree = "<group>"; };
D4A13D5927A59D5C003BE359 /* InputMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMethodController.swift; sourceTree = "<group>"; };
D4C9CAAF27AAC8EC0058DFEA /* NSStringUtils */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NSStringUtils; path = Packages/NSStringUtils; sourceTree = "<group>"; };
D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
D4E33D8B27A838D5006DB1CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
D4E33D8C27A838D8006DB1CF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */;

7
Packages/NSStringUtils/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -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"]),
]
)

View File

@ -0,0 +1,3 @@
# NSStringUtils
A description of this package.

View File

@ -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[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc func previousUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex > 0 {
fixedIndex -= 1
}
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc var count: Int {
(self as String).count
}
@objc var split: [NSString] {
Array(self as String).map {
NSString(string: String($0))
}
}
}

View File

@ -0,0 +1,60 @@
import XCTest
@testable import NSStringUtils
final class NSStringUtilsTests: XCTestCase {
func testNextWith🌳_0() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 0), 2)
}
func testNextWith🌳_1() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 1), 2)
}
func testNextWith🌳_2() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 2), 4)
}
func testNextWith🌳_3() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 3), 4)
}
func testNextWith🌳_4() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 4), 4)
}
func testNextWith🌳_5() {
let s = NSString("🌳🌳🌳")
XCTAssertEqual(s.nextUtf16Position(for: 4), 6)
}
func testPrevWith🌳_0() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.previousUtf16Position(for: 0), 0)
}
func testPrevWith🌳_1() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.previousUtf16Position(for: 1), 0)
}
func testINextWith🌳_2() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.previousUtf16Position(for: 2), 0)
}
func testINextWith🌳_3() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.previousUtf16Position(for: 3), 0)
}
func testINextWith🌳_4() {
let s = NSString("🌳🌳")
XCTAssertEqual(s.previousUtf16Position(for: 4), 2)
}
}

View File

@ -22,6 +22,7 @@
// OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
import NSStringUtils
/// Represents the states for the input method controller.
///
@ -244,8 +245,8 @@ class InputState: NSObject {
@objc var userPhrase: String {
let text = (composingBuffer as NSString).substring(with: markedRange)
let exactBegin = StringUtils.convertToCharIndex(from: markedRange.location, in: composingBuffer)
let exactEnd = StringUtils.convertToCharIndex(from: markedRange.location + markedRange.length, in: composingBuffer)
let exactBegin = (composingBuffer as NSString).nextUtf16Position(for: markedRange.location)
let exactEnd = (composingBuffer as NSString).previousUtf16Position(for: markedRange.location)
let readings = readings[exactBegin..<exactEnd]
let joined = readings.joined(separator: "-")
return "\(text) \(joined)"

View File

@ -31,6 +31,7 @@
#import <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);

View File

@ -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[..<string.index(string.startIndex, offsetBy: index)].utf16.count
return count
}
@objc (previousUtf16PositionForIndex:in:)
static func previousUtf16Position(for index: Int, in string: String) -> Int {
var index = convertToCharIndex(from: index, in: string)
if index > 0 {
index -= 1
}
let count = string[..<string.index(string.startIndex, offsetBy: index)].utf16.count
return count
}
}
extension NSString {
@objc var count: Int {
(self as String).count
}
@objc var split: [NSString] {
Array(self as String).map {
NSString(string: String($0))
}
}
}