Skip to content

Commit 8976911

Browse files
committed
Fix Drawing Ordering, Use Attachment Ranges
1 parent 8abf180 commit 8976911

File tree

5 files changed

+54
-51
lines changed

5 files changed

+54
-51
lines changed

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldCalculator.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ actor LineFoldCalculator {
5757
// Depth: Open range
5858
var openFolds: [Int: LineFoldStorage.RawFold] = [:]
5959
var currentDepth: Int = 0
60-
var iterator = await controller.textView.layoutManager.linesInRange(documentRange)
60+
var iterator = await controller.textView.layoutManager.lineStorage.makeIterator()
6161

6262
var lines = await self.getMoreLines(
6363
controller: controller,
@@ -109,25 +109,27 @@ actor LineFoldCalculator {
109109

110110
let attachments = await controller.textView.layoutManager.attachments
111111
.getAttachmentsOverlapping(documentRange)
112-
.compactMap { $0.attachment as? LineFoldPlaceholder }
113-
.map {
114-
LineFoldStorage.DepthStartPair(depth: $0.fold.depth, start: $0.fold.range.lowerBound)
112+
.compactMap { attachmentBox -> LineFoldStorage.DepthStartPair? in
113+
guard let attachment = attachmentBox.attachment as? LineFoldPlaceholder else {
114+
return nil
115+
}
116+
return LineFoldStorage.DepthStartPair(depth: attachment.fold.depth, start: attachmentBox.range.location)
115117
}
116118

117119
let storage = LineFoldStorage(
118120
documentLength: foldCache.max(
119121
by: { $0.range.upperBound < $1.range.upperBound }
120122
)?.range.upperBound ?? documentRange.length,
121123
folds: foldCache.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }),
122-
collapsedProvider: { Set(attachments) }
124+
collapsedRanges: Set(attachments)
123125
)
124126
valueStreamContinuation.yield(storage)
125127
}
126128

127129
@MainActor
128130
private func getMoreLines(
129131
controller: TextViewController,
130-
iterator: inout TextLayoutManager.RangeIterator,
132+
iterator: inout TextLineStorage<TextLine>.TextLineStorageIterator,
131133
previousDepth: Int,
132134
foldProvider: LineFoldProvider
133135
) -> [LineFoldProviderLineInfo]? {

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldStorage.swift

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ struct FoldRange: Sendable, Equatable {
1717
var isCollapsed: Bool
1818
}
1919

20-
/// Represents a single fold run with stable identifier and collapse state
21-
struct FoldRun: Sendable {
22-
let id: FoldRange.FoldIdentifier
23-
let depth: Int
24-
let range: Range<Int>
25-
let isCollapsed: Bool
26-
}
27-
2820
/// Sendable data model for code folding using RangeStore
2921
struct LineFoldStorage: Sendable {
3022
/// A temporary fold representation without stable ID
@@ -51,9 +43,9 @@ struct LineFoldStorage: Sendable {
5143
private var foldRanges: [FoldRange.FoldIdentifier: FoldRange] = [:]
5244

5345
/// Initialize with the full document length
54-
init(documentLength: Int, folds: [RawFold] = [], collapsedProvider: () -> Set<DepthStartPair> = { [] }) {
46+
init(documentLength: Int, folds: [RawFold] = [], collapsedRanges: Set<DepthStartPair> = []) {
5547
self.store = RangeStore<FoldStoreElement>(documentLength: documentLength)
56-
self.updateFolds(from: folds, collapsedProvider: collapsedProvider)
48+
self.updateFolds(from: folds, collapsedRanges: collapsedRanges)
5749
}
5850

5951
private mutating func nextFoldId() -> FoldRange.FoldIdentifier {
@@ -63,17 +55,14 @@ struct LineFoldStorage: Sendable {
6355

6456
/// Replace all fold data from raw folds, preserving collapse state via callback
6557
/// - Parameter rawFolds: newly computed folds (depth + range)
66-
/// - Parameter collapsedProvider: callback returning current collapsed ranges/depths
67-
mutating func updateFolds(from rawFolds: [RawFold], collapsedProvider: () -> Set<DepthStartPair>) {
58+
/// - Parameter collapsedRanges: Current collapsed ranges/depths
59+
mutating func updateFolds(from rawFolds: [RawFold], collapsedRanges: Set<DepthStartPair>) {
6860
// Build reuse map by start+depth, carry over collapse state
6961
var reuseMap: [DepthStartPair: FoldRange] = [:]
7062
for region in foldRanges.values {
7163
reuseMap[DepthStartPair(depth: region.depth, start: region.range.lowerBound)] = region
7264
}
7365

74-
// Determine which ranges are currently collapsed
75-
let collapsedSet = collapsedProvider()
76-
7766
// Build new regions
7867
foldRanges.removeAll(keepingCapacity: true)
7968
store = RangeStore<FoldStoreElement>(documentLength: store.length)
@@ -85,7 +74,7 @@ struct LineFoldStorage: Sendable {
8574
let id = prior?.id ?? nextFoldId()
8675
let wasCollapsed = prior?.isCollapsed ?? false
8776
// override collapse if provider says so
88-
let isCollapsed = collapsedSet.contains(key) || wasCollapsed
77+
let isCollapsed = collapsedRanges.contains(key) || wasCollapsed
8978
let fold = FoldRange(id: id, depth: raw.depth, range: raw.range, isCollapsed: isCollapsed)
9079

9180
foldRanges[id] = fold
@@ -99,6 +88,12 @@ struct LineFoldStorage: Sendable {
9988
store.storageUpdated(editedRange: editedRange, changeInLength: delta)
10089
}
10190

91+
mutating func toggleCollapse(forFold fold: FoldRange) {
92+
guard var existingRange = foldRanges[fold.id] else { return }
93+
existingRange.isCollapsed.toggle()
94+
foldRanges[fold.id] = existingRange
95+
}
96+
10297
/// Query a document subrange and return all folds as an ordered list by start position
10398
func folds(in queryRange: Range<Int>) -> [FoldRange] {
10499
let runs = store.runs(in: queryRange.clamped(to: 0..<store.length))
@@ -120,20 +115,6 @@ struct LineFoldStorage: Sendable {
120115
}
121116

122117
return result.sorted { $0.range.lowerBound < $1.range.lowerBound }
123-
// let runs = store.runs(in: queryRange.clamped(to: 0..<store.length))
124-
// var currentLocation = queryRange.lowerBound
125-
// return runs.compactMap { run in
126-
// defer {
127-
// currentLocation += run.length
128-
// }
129-
// guard let value = run.value else { return nil }
130-
// return FoldRun(
131-
// id: value.id,
132-
// depth: value.depth,
133-
// range: currentLocation..<(currentLocation + run.length),
134-
// isCollapsed: foldRanges[value.id]?.isCollapsed ?? false
135-
// )
136-
// }
137118
}
138119

139120
/// Given a depth and a location, return the full original fold region

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldingModel.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,19 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate {
7777
/// Finds the deepest cached fold and depth of the fold for a line number.
7878
/// - Parameter lineNumber: The line number to query, zero-indexed.
7979
/// - Returns: The deepest cached fold and depth of the fold if it was found.
80-
func getCachedFoldAt(lineNumber: Int) -> (range: FoldRange, depth: Int)? {
80+
func getCachedFoldAt(lineNumber: Int) -> FoldRange? {
8181
guard let lineRange = controller?.textView.layoutManager.textLineForIndex(lineNumber)?.range else { return nil }
82-
guard let deepestFold = foldCache.folds(in: lineRange.intRange).max(by: { $0.depth < $1.depth }) else {
82+
guard let deepestFold = foldCache.folds(in: lineRange.intRange).max(by: {
83+
if $0.isCollapsed != $1.isCollapsed {
84+
$1.isCollapsed // Collapsed folds take precedence.
85+
} else if $0.isCollapsed {
86+
$0.depth > $1.depth
87+
} else {
88+
$0.depth < $1.depth
89+
}
90+
}) else {
8391
return nil
8492
}
85-
return (deepestFold, deepestFold.depth)
93+
return deepestFold
8694
}
8795
}

Sources/CodeEditSourceEditor/LineFolding/View/FoldingRibbonView+Draw.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ extension FoldingRibbonView {
4040
let textRange = rangeStart.range.location..<rangeEnd.range.upperBound
4141

4242
let folds = getDrawingFolds(forTextRange: textRange)
43-
for fold in folds {
43+
for fold in folds.filter({ !$0.isCollapsed }) {
44+
drawFoldMarker(
45+
fold,
46+
in: context,
47+
using: layoutManager
48+
)
49+
}
50+
51+
for fold in folds.filter({ $0.isCollapsed }) {
4452
drawFoldMarker(
4553
fold,
4654
in: context,
@@ -141,6 +149,7 @@ extension FoldingRibbonView {
141149
context.setLineJoin(.round)
142150
context.setLineWidth(1.3)
143151

152+
context.setFillColor(NSColor.tertiaryLabelColor.cgColor)
144153
context.fill(fillRect)
145154
context.addPath(chevron)
146155
context.strokePath()

Sources/CodeEditSourceEditor/LineFolding/View/FoldingRibbonView.swift

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,28 +130,31 @@ class FoldingRibbonView: NSView {
130130
guard let layoutManager = model?.controller?.textView.layoutManager,
131131
event.type == .leftMouseDown,
132132
let lineNumber = layoutManager.textLineForPosition(clickPoint.y)?.index,
133-
let fold = model?.getCachedFoldAt(lineNumber: lineNumber) else {
133+
let fold = model?.getCachedFoldAt(lineNumber: lineNumber),
134+
let firstLineInFold = layoutManager.textLineForOffset(fold.range.lowerBound) else {
134135
super.mouseDown(with: event)
135136
return
136137
}
137138
if let attachment = model?.controller?.textView?.layoutManager.attachments
138-
.getAttachmentsStartingIn(NSRange(fold.range.range))
139-
.filter({ $0.attachment is LineFoldPlaceholder })
140-
.first {
139+
.getAttachmentsStartingIn(NSRange(fold.range))
140+
.filter({ $0.attachment is LineFoldPlaceholder && firstLineInFold.range.contains($0.range.location) }).first {
141141
layoutManager.attachments.remove(atOffset: attachment.range.location)
142-
// fold.range.isCollapsed = false
143142
attachments.removeAll(where: { $0 === attachment.attachment })
144143
} else {
145-
let placeholder = LineFoldPlaceholder(fold: fold.range)
146-
layoutManager.attachments.add(placeholder, for: NSRange(fold.range.range))
144+
let placeholder = LineFoldPlaceholder(fold: fold)
145+
layoutManager.attachments.add(placeholder, for: NSRange(fold.range))
147146
attachments.append(placeholder)
148-
// fold.range.collapsed = true
149147
}
150148

149+
model?.foldCache.toggleCollapse(forFold: fold)
151150
model?.controller?.textView.needsLayout = true
152151
}
153152

154153
override func mouseMoved(with event: NSEvent) {
154+
defer {
155+
super.mouseMoved(with: event)
156+
}
157+
155158
let pointInView = convert(event.locationInWindow, from: nil)
156159
guard let lineNumber = model?.controller?.textView.layoutManager.textLineForPosition(pointInView.y)?.index,
157160
let fold = model?.getCachedFoldAt(lineNumber: lineNumber) else {
@@ -160,15 +163,15 @@ class FoldingRibbonView: NSView {
160163
return
161164
}
162165

163-
guard fold.range.range != hoveringFold?.range else {
166+
guard fold.range != hoveringFold?.range else {
164167
return
165168
}
166169
hoverAnimationTimer?.invalidate()
167170
// We only animate the first hovered fold. If the user moves the mouse vertically into other folds we just
168171
// show it immediately.
169172
if hoveringFold == nil {
170173
hoverAnimationProgress = 0.0
171-
hoveringFold = fold.range
174+
hoveringFold = fold
172175

173176
let duration: TimeInterval = 0.2
174177
let startTime = CACurrentMediaTime()
@@ -186,7 +189,7 @@ class FoldingRibbonView: NSView {
186189

187190
// Don't animate these
188191
hoverAnimationProgress = 1.0
189-
hoveringFold = fold.range
192+
hoveringFold = fold
190193
}
191194

192195
override func mouseExited(with event: NSEvent) {

0 commit comments

Comments
 (0)