Skip to content

Commit 93bfd76

Browse files
committed
Marked Text, Remove Common Module, lineBreakStrategy
1 parent 05390c5 commit 93bfd76

32 files changed

+338
-123
lines changed

Package.resolved

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
"version" : "0.1.17"
1010
}
1111
},
12+
{
13+
"identity" : "mainoffender",
14+
"kind" : "remoteSourceControl",
15+
"location" : "https://github.com/mattmassicotte/MainOffender",
16+
"state" : {
17+
"revision" : "343cc3797618c29b48b037b4e2beea0664e75315",
18+
"version" : "0.1.0"
19+
}
20+
},
1221
{
1322
"identity" : "rearrange",
1423
"kind" : "remoteSourceControl",
@@ -50,17 +59,17 @@
5059
"kind" : "remoteSourceControl",
5160
"location" : "https://github.com/ChimeHQ/TextFormation",
5261
"state" : {
53-
"revision" : "158a603054ed5176f18d7c08ba355c0e05cb0586",
54-
"version" : "0.7.0"
62+
"revision" : "b4987856bc860643ac2c9cdbc7d5f3e9ade68377",
63+
"version" : "0.8.1"
5564
}
5665
},
5766
{
5867
"identity" : "textstory",
5968
"kind" : "remoteSourceControl",
6069
"location" : "https://github.com/ChimeHQ/TextStory",
6170
"state" : {
62-
"revision" : "b7b3fc551bd0177c32b3dc46d0478e9f0b6f8c6f",
63-
"version" : "0.7.2"
71+
"revision" : "8883fa739aa213e70e6cb109bfbf0a0b551e4cb5",
72+
"version" : "0.8.0"
6473
}
6574
}
6675
],

Package.swift

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ let package = Package(
2929
url: "https://github.com/lukepistrol/SwiftLintPlugin",
3030
from: "0.2.2"
3131
),
32-
// Filters for indentation, pair completion, whitespace
32+
// Text mutation, storage helpers
33+
.package(
34+
url: "https://github.com/ChimeHQ/TextStory",
35+
from: "0.8.0"
36+
),
37+
// Rules for indentation, pair completion, whitespace
3338
.package(
3439
url: "https://github.com/ChimeHQ/TextFormation",
35-
from: "0.7.0"
40+
from: "0.8.1"
3641
),
3742
// Useful data structures
3843
.package(
@@ -45,7 +50,6 @@ let package = Package(
4550
.target(
4651
name: "CodeEditTextView",
4752
dependencies: [
48-
"Common",
4953
"CodeEditInputView",
5054
"CodeEditLanguages",
5155
"TextFormation",
@@ -59,18 +63,8 @@ let package = Package(
5963
.target(
6064
name: "CodeEditInputView",
6165
dependencies: [
62-
"Common",
63-
"TextFormation"
64-
],
65-
plugins: [
66-
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
67-
]
68-
),
69-
70-
// Common classes and extensions used in both CodeEditTextView and CodeEditInputView
71-
.target(
72-
name: "Common",
73-
dependencies: [
66+
"TextStory",
67+
"TextFormation",
7468
.product(name: "Collections", package: "swift-collections")
7569
],
7670
plugins: [

Sources/CodeEditInputView/MarkedTextManager/MarkedTextManager.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class MarkedTextManager {
2929
func removeAll() {
3030
markedRanges.removeAll()
3131
}
32-
32+
3333
/// Updates the stored marked ranges.
3434
/// - Parameters:
3535
/// - insertLength: The length of the string being inserted.
@@ -50,7 +50,7 @@ class MarkedTextManager {
5050
markedRanges = [selectedRange]
5151
}
5252
}
53-
53+
5454
/// Finds any marked ranges for a line and returns them.
5555
/// - Parameter lineRange: The range of the line.
5656
/// - Returns: A `MarkedRange` struct with information about attributes and ranges. `nil` if there is no marked
@@ -68,4 +68,31 @@ class MarkedTextManager {
6868
return MarkedRanges(ranges: ranges, attributes: attributes)
6969
}
7070
}
71+
72+
/// Updates marked text ranges for a new set of selections.
73+
/// - Parameter textSelections: The new text selections.
74+
/// - Returns: `True` if the marked text needs layout.
75+
func updateForNewSelections(textSelections: [TextSelectionManager.TextSelection]) -> Bool {
76+
// Ensure every marked range has a matching selection.
77+
// If any marked ranges do not have a matching selection, unmark.
78+
// Matching, in this context, means having a selection in the range location...max
79+
var markedRanges = markedRanges
80+
for textSelection in textSelections {
81+
if let markedRangeIdx = markedRanges.firstIndex(where: {
82+
($0.location...$0.max).contains(textSelection.range.location)
83+
&& ($0.location...$0.max).contains(textSelection.range.max)
84+
}) {
85+
markedRanges.remove(at: markedRangeIdx)
86+
} else {
87+
return true
88+
}
89+
}
90+
91+
// If any remaining marked ranges, we need to unmark.
92+
if !markedRanges.isEmpty {
93+
return false
94+
} else {
95+
return true
96+
}
97+
}
7198
}

Sources/CodeEditInputView/TextLayoutManager/TextLayoutManager+Edits.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77

88
import AppKit
9-
import Common
109

1110
// MARK: - Edits
1211

Sources/CodeEditInputView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,17 @@ extension TextLayoutManager {
188188
/// - Parameter offset: The offset to ensure layout until.
189189
private func ensureLayoutFor(position: TextLineStorage<TextLine>.TextLinePosition) -> CGFloat {
190190
guard let textStorage else { return 0 }
191-
position.data.prepareForDisplay(
191+
let displayData = TextLine.DisplayData(
192192
maxWidth: maxLineLayoutWidth,
193193
lineHeightMultiplier: lineHeightMultiplier,
194-
estimatedLineHeight: estimateLineHeight(),
194+
estimatedLineHeight: estimateLineHeight()
195+
)
196+
position.data.prepareForDisplay(
197+
displayData: displayData,
195198
range: position.range,
196199
stringRef: textStorage,
197-
markedRanges: markedTextManager.markedRanges(in: position.range)
200+
markedRanges: markedTextManager.markedRanges(in: position.range),
201+
breakStrategy: lineBreakStrategy
198202
)
199203
var height: CGFloat = 0
200204
for fragmentPosition in position.data.lineFragments {

Sources/CodeEditInputView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import Foundation
99
import AppKit
10-
import Common
1110

1211
public protocol TextLayoutManagerDelegate: AnyObject {
1312
func layoutManagerHeightDidUpdate(newHeight: CGFloat)
@@ -48,10 +47,17 @@ public class TextLayoutManager: NSObject {
4847
lineStorage.count
4948
}
5049

50+
/// The strategy to use when breaking lines. Defaults to ``LineBreakStrategy/word``.
51+
public var lineBreakStrategy: LineBreakStrategy = .word {
52+
didSet {
53+
setNeedsLayout()
54+
}
55+
}
56+
5157
// MARK: - Internal
5258

53-
internal weak var textStorage: NSTextStorage?
54-
internal var lineStorage: TextLineStorage<TextLine> = TextLineStorage()
59+
weak var textStorage: NSTextStorage?
60+
var lineStorage: TextLineStorage<TextLine> = TextLineStorage()
5561
var markedTextManager: MarkedTextManager = MarkedTextManager()
5662
private let viewReuseQueue: ViewReuseQueue<LineFragmentView, UUID> = ViewReuseQueue()
5763
private var visibleLineIds: Set<TextLine.ID> = []
@@ -63,7 +69,7 @@ public class TextLayoutManager: NSObject {
6369
transactionCounter > 0
6470
}
6571

66-
weak internal var layoutView: NSView?
72+
weak var layoutView: NSView?
6773

6874
/// The calculated maximum width of all laid out lines.
6975
/// - Note: This does not indicate *the* maximum width of the text view if all lines have not been laid out.
@@ -79,6 +85,13 @@ public class TextLayoutManager: NSObject {
7985
: .greatestFiniteMagnitude
8086
}
8187

88+
/// Contains all data required to perform layout on a text line.
89+
private struct LineLayoutData {
90+
let minY: CGFloat
91+
let maxY: CGFloat
92+
let maxWidth: CGFloat
93+
}
94+
8295
// MARK: - Init
8396

8497
/// Initialize a text layout manager and prepare it for use.
@@ -124,7 +137,7 @@ public class TextLayoutManager: NSObject {
124137
print("Text Layout Manager built in: ", TimeInterval(nanos) / TimeInterval(NSEC_PER_MSEC), "ms")
125138
#endif
126139
}
127-
140+
128141
/// Resets the layout manager to an initial state.
129142
internal func reset() {
130143
lineStorage.removeAll()
@@ -235,9 +248,7 @@ public class TextLayoutManager: NSObject {
235248
let lineSize = layoutLine(
236249
linePosition,
237250
textStorage: textStorage,
238-
minY: linePosition.yPos,
239-
maxY: maxY,
240-
maxWidth: maxLineLayoutWidth,
251+
layoutData: LineLayoutData(minY: linePosition.yPos, maxY: maxY, maxWidth: maxLineLayoutWidth),
241252
laidOutFragmentIDs: &usedFragmentIDs
242253
)
243254
if lineSize.height != linePosition.height {
@@ -297,19 +308,22 @@ public class TextLayoutManager: NSObject {
297308
private func layoutLine(
298309
_ position: TextLineStorage<TextLine>.TextLinePosition,
299310
textStorage: NSTextStorage,
300-
minY: CGFloat,
301-
maxY: CGFloat,
302-
maxWidth: CGFloat,
311+
layoutData: LineLayoutData,
303312
laidOutFragmentIDs: inout Set<UUID>
304313
) -> CGSize {
314+
let lineDisplayData = TextLine.DisplayData(
315+
maxWidth: layoutData.maxWidth,
316+
lineHeightMultiplier: lineHeightMultiplier,
317+
estimatedLineHeight: estimateLineHeight()
318+
)
319+
305320
let line = position.data
306321
line.prepareForDisplay(
307-
maxWidth: maxWidth,
308-
lineHeightMultiplier: lineHeightMultiplier,
309-
estimatedLineHeight: estimateLineHeight(),
322+
displayData: lineDisplayData,
310323
range: position.range,
311324
stringRef: textStorage,
312-
markedRanges: markedTextManager.markedRanges(in: position.range)
325+
markedRanges: markedTextManager.markedRanges(in: position.range),
326+
breakStrategy: lineBreakStrategy
313327
)
314328

315329
if position.range.isEmpty {
@@ -323,7 +337,7 @@ public class TextLayoutManager: NSObject {
323337
for lineFragmentPosition in line.typesetter.lineFragments {
324338
let lineFragment = lineFragmentPosition.data
325339

326-
layoutFragmentView(for: lineFragmentPosition, at: minY + lineFragmentPosition.yPos)
340+
layoutFragmentView(for: lineFragmentPosition, at: layoutData.minY + lineFragmentPosition.yPos)
327341

328342
width = max(width, lineFragment.width)
329343
height += lineFragment.scaledHeight

Sources/CodeEditInputView/TextLine/LineBreakStrategy.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
// Created by Khan Winter on 9/19/23.
66
//
77

8+
/// Options for breaking lines when they cannot fit in the viewport.
89
public enum LineBreakStrategy {
10+
/// Break lines at word boundaries when possible.
911
case word
12+
/// Break lines at the nearest character, regardless of grouping.
1013
case character
1114
}

Sources/CodeEditInputView/TextLine/LineFragmentView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77

88
import AppKit
9-
import Common
109

1110
/// Displays a line fragment.
1211
final class LineFragmentView: NSView {

Sources/CodeEditInputView/TextLine/TextLine.swift

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,24 @@ public final class TextLine: Identifiable, Equatable {
3636

3737
/// Prepares the line for display, generating all potential line breaks and calculating the real height of the line.
3838
/// - Parameters:
39-
/// - maxWidth: The maximum width the line can be. Used to find line breaks.
40-
/// - lineHeightMultiplier: The multiplier to use for lines.
41-
/// - estimatedLineHeight: The estimated height for an empty line.
39+
/// - displayData: Information required to display a text line.
4240
/// - range: The range this text range represents in the entire document.
4341
/// - stringRef: A reference to the string storage for the document.
4442
/// - markedRanges: Any marked ranges in the line.
43+
/// - breakStrategy: Determines how line breaks are calculated.
4544
func prepareForDisplay(
46-
maxWidth: CGFloat,
47-
lineHeightMultiplier: CGFloat,
48-
estimatedLineHeight: CGFloat,
45+
displayData: DisplayData,
4946
range: NSRange,
5047
stringRef: NSTextStorage,
51-
markedRanges: MarkedTextManager.MarkedRanges?
48+
markedRanges: MarkedTextManager.MarkedRanges?,
49+
breakStrategy: LineBreakStrategy
5250
) {
5351
let string = stringRef.attributedSubstring(from: range)
54-
self.maxWidth = maxWidth
52+
self.maxWidth = displayData.maxWidth
5553
typesetter.typeset(
5654
string,
57-
maxWidth: maxWidth,
58-
lineHeightMultiplier: lineHeightMultiplier,
59-
estimatedLineHeight: estimatedLineHeight,
55+
displayData: displayData,
56+
breakStrategy: breakStrategy,
6057
markedRanges: markedRanges
6158
)
6259
needsLayout = false
@@ -65,4 +62,11 @@ public final class TextLine: Identifiable, Equatable {
6562
public static func == (lhs: TextLine, rhs: TextLine) -> Bool {
6663
lhs.id == rhs.id
6764
}
65+
66+
/// Contains all required data to perform a typeset and layout operation on a text line.
67+
struct DisplayData {
68+
let maxWidth: CGFloat
69+
let lineHeightMultiplier: CGFloat
70+
let estimatedLineHeight: CGFloat
71+
}
6872
}

Sources/CodeEditInputView/TextLine/Typesetter.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ final class Typesetter {
1919

2020
func typeset(
2121
_ string: NSAttributedString,
22-
maxWidth: CGFloat,
23-
lineHeightMultiplier: CGFloat,
24-
estimatedLineHeight: CGFloat,
22+
displayData: TextLine.DisplayData,
23+
breakStrategy: LineBreakStrategy,
2524
markedRanges: MarkedTextManager.MarkedRanges?
2625
) {
2726
lineFragments.removeAll()
@@ -36,9 +35,10 @@ final class Typesetter {
3635
}
3736
self.typesetter = CTTypesetterCreateWithAttributedString(self.string)
3837
generateLines(
39-
maxWidth: maxWidth,
40-
lineHeightMultiplier: lineHeightMultiplier,
41-
estimatedLineHeight: estimatedLineHeight
38+
maxWidth: displayData.maxWidth,
39+
lineHeightMultiplier: displayData.lineHeightMultiplier,
40+
estimatedLineHeight: displayData.estimatedLineHeight,
41+
breakStrategy: breakStrategy
4242
)
4343
}
4444

@@ -49,7 +49,12 @@ final class Typesetter {
4949
/// - maxWidth: The maximum width the line can be.
5050
/// - lineHeightMultiplier: The multiplier to apply to an empty line's height.
5151
/// - estimatedLineHeight: The estimated height of an empty line.
52-
private func generateLines(maxWidth: CGFloat, lineHeightMultiplier: CGFloat, estimatedLineHeight: CGFloat) {
52+
private func generateLines(
53+
maxWidth: CGFloat,
54+
lineHeightMultiplier: CGFloat,
55+
estimatedLineHeight: CGFloat,
56+
breakStrategy: LineBreakStrategy
57+
) {
5358
guard let typesetter else { return }
5459
var lines: [TextLineStorage<LineFragment>.BuildItem] = []
5560
var height: CGFloat = 0
@@ -69,7 +74,7 @@ final class Typesetter {
6974
while startIndex < string.length {
7075
let lineBreak = suggestLineBreak(
7176
using: typesetter,
72-
strategy: .word, // TODO: Make this configurable
77+
strategy: breakStrategy,
7378
startingOffset: startIndex,
7479
constrainingWidth: maxWidth
7580
)

Sources/CodeEditInputView/TextLineStorage/TextLineStorage+NSTextStorage.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77

88
import AppKit
9-
import Common
109

1110
extension TextLineStorage where Data == TextLine {
1211
/// Builds the line storage object from the given `NSTextStorage`.

0 commit comments

Comments
 (0)