Skip to content

Commit

Permalink
Moving to using TextTokenizer completely
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Aug 31, 2024
1 parent 73e264a commit 0e6e265
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 42 deletions.
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"originHash" : "a37ab791b028e91ba927aed2f01979a802c12b4164919a272b77af2d9e021dbb",
"originHash" : "00743848cbb91a47eb19084a5d4a354ab430ceab782cdee3f3c57556b13f019f",
"pins" : [
{
"identity" : "keycodes",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/KeyCodes",
"state" : {
"revision" : "93ba759d2bfd7b5233294188bc685fd09dce2729",
"version" : "1.0.3"
}
},
{
"identity" : "ligature",
"kind" : "remoteSourceControl",
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ let package = Package(
.library(name: "IBeam", targets: ["IBeam"]),
],
dependencies: [
.package(url: "https://github.com/ChimeHQ/Ligature", branch: "main"),
.package(url: "https://github.com/ChimeHQ/Ligature", revision: "b184b82dd4c68327ed37257d1b60fc9f41a50042"),
.package(url: "https://github.com/ChimeHQ/KeyCodes", from: "1.0.3"),
],
targets: [
.target(name: "IBeam", dependencies: ["KeyCodes", "Ligature"]),
.testTarget(name: "IBeamTests", dependencies: ["IBeam"]),
.testTarget(name: "IBeamTests", dependencies: ["IBeam", "Ligature"]),
]
)
2 changes: 1 addition & 1 deletion Sources/IBeam/Cursor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public struct Cursor<TextLocation: Comparable>: Identifiable {
public typealias TextRange = Range<TextLocation>

public let id: UUID
public let range: TextRange
public var range: TextRange

public init(range: TextRange) {
self.range = range
Expand Down
57 changes: 41 additions & 16 deletions Sources/IBeam/MultiCursorState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ import UIKit
#endif

import KeyCodes
import Ligature

public enum SelectionAffinity: Hashable, Sendable, Codable {
case upstream
case downstream
}

public struct MultiCursorState<CoordinateProvider: TextCoordinateProvider> {
public typealias TextLocation = CoordinateProvider.TextLocation
public typealias Cursor = IBeam.Cursor<TextLocation>
@MainActor
public final class MultiCursorState<Tokenizer: TextTokenizer> where Tokenizer.Position: Comparable {
public typealias Position = Tokenizer.Position
public typealias Cursor = IBeam.Cursor<Position>

public var affinity: SelectionAffinity
public var cursors: [Cursor]
public let coordinateProvider: CoordinateProvider
public let tokenizer: Tokenizer

public init(cursors: [Cursor] = [], affinity: SelectionAffinity = .upstream, coordinateProvider: CoordinateProvider) {
public init(cursors: [Cursor] = [], affinity: SelectionAffinity = .upstream, tokenizer: Tokenizer) {
self.cursors = cursors
self.affinity = affinity
self.coordinateProvider = coordinateProvider
self.tokenizer = tokenizer
}

public var hasMultipleCursors: Bool {
Expand All @@ -35,15 +37,15 @@ extension MultiCursorState {

}

public mutating func addCursorBelow() {
public func addCursorBelow() {
guard let range = cursors.last?.range else {
assertionFailure("at least one cursor should be present")
return
}

guard
let start = coordinateProvider.closestMatchingVerticalLocation(to: range.lowerBound, above: false),
let end = coordinateProvider.closestMatchingVerticalLocation(to: range.upperBound, above: false)
let start = tokenizer.position(from: range.lowerBound, toBoundary: .character, inDirection: .layout(.down)),
let end = tokenizer.position(from: range.upperBound, toBoundary: .character, inDirection: .layout(.down))
else {
return
}
Expand All @@ -54,19 +56,20 @@ extension MultiCursorState {
self.cursors.sort()
}

public mutating func addCursor(at location: TextLocation) {
let newCursor = Cursor(range: location..<location)
public func addCursor(at position: Position) {
let newCursor = Cursor(range: position..<position)

self.cursors.append(newCursor)
self.cursors.sort()
}
}

extension MultiCursorState {
#if os(macOS)
/// Process a keyDown event
///
/// - Returns: true if the event was processed and should now be ignored.
public mutating func handleKeyDown(with event: NSEvent) -> Bool {
public func handleKeyDown(with event: NSEvent) -> Bool {
let flags = event.keyModifierFlags?.subtracting(.numericPad) ?? []
let key = event.keyboardHIDUsage

Expand All @@ -77,14 +80,36 @@ extension MultiCursorState {
case ([.control, .shift], .keyboardDownArrow):
addCursorBelow()
return true
case ([], .keyboardLeftArrow):
moveLeft(self)
return true
default:
break
}

guard hasMultipleCursors else {
return false
}

return false
}
#endif
}

extension MultiCursorState {
public func moveLeft(_ sender: Any?) {
self.cursors = cursors.compactMap { cursor in
let start = cursor.range.lowerBound
guard let newStart = tokenizer.position(from: start, toBoundary: .character, inDirection: .layout(.left)) else {
return nil
}

var cursor = cursor

switch affinity {
case .upstream:
cursor.range = newStart..<newStart
case .downstream:
cursor.range = newStart..<cursor.range.upperBound
}

return cursor
}
}
}
11 changes: 0 additions & 11 deletions Sources/IBeam/TextCoordinateProvider.swift

This file was deleted.

31 changes: 20 additions & 11 deletions Tests/IBeamTests/MultiCursorStateTests.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import Testing
import IBeam

class MockCoordinateProvider: TextCoordinateProvider {
func closestMatchingVerticalLocation(to location: Int, above: Bool) -> Int? {
let offset = above ? -5 : 5

return location + offset
}
}
import Ligature

final class MultiCursorStateTests {
@Test
func addCursorBelow() async throws {
@Test @MainActor
func addCursorBelow() throws {
let cursor = Cursor(range: 0..<10)
let provider = MockCoordinateProvider()
var state = MultiCursorState(cursors: [cursor], coordinateProvider: provider)
let tokenizer = MockTextTokenizer()
let state = MultiCursorState(cursors: [cursor], tokenizer: tokenizer)

tokenizer.responses = [.position(5), .position(15)]
state.addCursorBelow()

#expect(tokenizer.requests == [.position(0, .character, .layout(.down)), .position(10, .character, .layout(.down))])
#expect(state.cursors.map(\.range) == [0..<10, 5..<15])
}

@Test @MainActor
func handleLeftArrowWithSingleInsertion() throws {
let cursor = Cursor(range: 1..<1)
let tokenizer = MockTextTokenizer()
let state = MultiCursorState(cursors: [cursor], affinity: .upstream, tokenizer: tokenizer)

tokenizer.responses = [.position(0)]
state.moveLeft(nil)

#expect(tokenizer.requests == [.position(1, .character, .layout(.left))])
#expect(state.cursors.map(\.range) == [0..<0])
}
}

0 comments on commit 0e6e265

Please sign in to comment.