Skip to content

Commit 353ada9

Browse files
committed
Folds in Minimap, Double Click/Enter DIsmisses Fold
1 parent 06ffead commit 353ada9

17 files changed

+136
-55
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
// A fast, efficient, text view for code.
1818
.package(
1919
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20-
from: "0.11.3"
20+
from: "0.11.4"
2121
),
2222
// tree-sitter languages
2323
.package(

Sources/CodeEditSourceEditor/Controller/TextViewController+StyleViews.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extension TextViewController {
1313
// swiftlint:disable:next force_cast
1414
let paragraph = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
1515
paragraph.tabStops.removeAll()
16-
paragraph.defaultTabInterval = CGFloat(tabWidth) * fontCharWidth
16+
paragraph.defaultTabInterval = CGFloat(tabWidth) * font.charWidth
1717
return paragraph
1818
}
1919

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public class TextViewController: NSViewController {
173173
/// This will be `nil` if another highlighter provider is passed to the source editor.
174174
internal(set) public var treeSitterClient: TreeSitterClient?
175175

176-
var fontCharWidth: CGFloat { (" " as NSString).size(withAttributes: [.font: font]).width }
176+
var foldProvider: LineFoldProvider
177177

178178
/// Filters used when applying edits..
179179
var textFilters: [TextFormation.Filter] = []
@@ -202,13 +202,15 @@ public class TextViewController: NSViewController {
202202
configuration: SourceEditorConfiguration,
203203
cursorPositions: [CursorPosition],
204204
highlightProviders: [HighlightProviding] = [TreeSitterClient()],
205+
foldProvider: LineFoldProvider? = nil,
205206
undoManager: CEUndoManager? = nil,
206207
coordinators: [TextViewCoordinator] = []
207208
) {
208209
self.language = language
209210
self.configuration = configuration
210211
self.cursorPositions = cursorPositions
211212
self.highlightProviders = highlightProviders
213+
self.foldProvider = foldProvider ?? LineIndentationFoldProvider()
212214
self._undoManager = undoManager
213215
self.invisibleCharactersCoordinator = InvisibleCharactersCoordinator(configuration: configuration)
214216

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// NSFont+CharWidth.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 6/25/25.
6+
//
7+
8+
import AppKit
9+
10+
extension NSFont {
11+
var charWidth: CGFloat {
12+
(" " as NSString).size(withAttributes: [.font: self]).width
13+
}
14+
}

Sources/CodeEditSourceEditor/Gutter/GutterView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public class GutterView: NSView {
160160
self.textView = controller.textView
161161
self.delegate = delegate
162162

163-
foldingRibbon = FoldingRibbonView(controller: controller, foldProvider: nil)
163+
foldingRibbon = FoldingRibbonView(controller: controller)
164164

165165
super.init(frame: .zero)
166166
clipsToBounds = true

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ class StyledRangeContainer {
9696
var runs: [RangeStoreRun<StyleElement>] = []
9797

9898
var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length })
99-
10099
while let value = minValue {
101100
// Get minimum length off the end of each array
102101
let minRunIdx = value.offset
@@ -118,7 +117,9 @@ class StyledRangeContainer {
118117
}
119118
}
120119

121-
allRuns[minRunIdx].removeLast()
120+
if !allRuns[minRunIdx].isEmpty {
121+
allRuns[minRunIdx].removeLast()
122+
}
122123

123124
runs.append(minRun)
124125
minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length })

Sources/CodeEditSourceEditor/LineFolding/FoldProviders/LineFoldProvider.swift renamed to Sources/CodeEditSourceEditor/LineFolding/LineFoldProviders/LineFoldProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import AppKit
99
import CodeEditTextView
1010

11-
enum LineFoldProviderLineInfo {
11+
public enum LineFoldProviderLineInfo {
1212
case startFold(rangeStart: Int, newDepth: Int)
1313
case endFold(rangeEnd: Int, newDepth: Int)
1414

@@ -32,7 +32,7 @@ enum LineFoldProviderLineInfo {
3232
}
3333

3434
@MainActor
35-
protocol LineFoldProvider: AnyObject {
35+
public protocol LineFoldProvider: AnyObject {
3636
func foldLevelAtLine(
3737
lineNumber: Int,
3838
lineRange: NSRange,

Sources/CodeEditSourceEditor/LineFolding/FoldProviders/IndentationLineFoldProvider.swift renamed to Sources/CodeEditSourceEditor/LineFolding/LineFoldProviders/LineIndentationFoldProvider.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// IndentationLineFoldProvider.swift
2+
// LineIndentationFoldProvider.swift
33
// CodeEditSourceEditor
44
//
55
// Created by Khan Winter on 5/8/25.
@@ -8,7 +8,8 @@
88
import AppKit
99
import CodeEditTextView
1010

11-
final class IndentationLineFoldProvider: LineFoldProvider {
11+
/// A basic fold provider that uses line indentation to determine fold regions.
12+
final class LineIndentationFoldProvider: LineFoldProvider {
1213
func indentLevelAtLine(substring: NSString) -> Int? {
1314
for idx in 0..<substring.length {
1415
let character = UnicodeScalar(substring.character(at: idx))

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldCalculator.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ actor LineFoldCalculator {
2626
/// - controller: The text controller to use for text and attachment fetching.
2727
/// - textChangedStream: A stream of text changes, received as the document is edited.
2828
init(
29-
foldProvider: LineFoldProvider?,
3029
controller: TextViewController,
31-
textChangedStream: AsyncStream<(NSRange, Int)>
30+
textChangedStream: AsyncStream<Void>
3231
) {
33-
self.foldProvider = foldProvider
32+
self.foldProvider = controller.foldProvider
3433
self.controller = controller
3534
(valueStream, valueStreamContinuation) = AsyncStream<LineFoldStorage>.makeStream()
3635
Task { await listenToTextChanges(textChangedStream: textChangedStream) }
@@ -42,10 +41,10 @@ actor LineFoldCalculator {
4241

4342
/// Sets up an attached task to listen to values on a stream of text changes.
4443
/// - Parameter textChangedStream: A stream of text changes.
45-
private func listenToTextChanges(textChangedStream: AsyncStream<(NSRange, Int)>) {
44+
private func listenToTextChanges(textChangedStream: AsyncStream<Void>) {
4645
textChangedTask = Task {
4746
for await edit in textChangedStream {
48-
await buildFoldsForDocument(afterEditIn: edit.0, delta: edit.1)
47+
await buildFoldsForDocument()
4948
}
5049
}
5150
}
@@ -54,7 +53,7 @@ actor LineFoldCalculator {
5453
///
5554
/// For each line in the document, find the indentation level using the ``levelProvider``. At each line, if the
5655
/// indent increases from the previous line, we start a new fold. If it decreases we end the fold we were in.
57-
private func buildFoldsForDocument(afterEditIn: NSRange, delta: Int) async {
56+
private func buildFoldsForDocument() async {
5857
guard let controller = self.controller, let foldProvider = self.foldProvider else { return }
5958
let documentRange = await controller.textView.documentRange
6059
var foldCache: [LineFoldStorage.RawFold] = []

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldingModel.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
2626
@Published var foldCache: LineFoldStorage = LineFoldStorage(documentLength: 0)
2727
private var calculator: LineFoldCalculator
2828

29-
private var textChangedStream: AsyncStream<(NSRange, Int)>
30-
private var textChangedStreamContinuation: AsyncStream<(NSRange, Int)>.Continuation
29+
private var textChangedStream: AsyncStream<Void>
30+
private var textChangedStreamContinuation: AsyncStream<Void>.Continuation
3131
private var cacheListenTask: Task<Void, Never>?
3232

3333
weak var controller: TextViewController?
34+
weak var foldView: NSView?
3435

35-
init(controller: TextViewController, foldView: NSView, foldProvider: LineFoldProvider?) {
36+
init(controller: TextViewController, foldView: NSView) {
3637
self.controller = controller
37-
(textChangedStream, textChangedStreamContinuation) = AsyncStream<(NSRange, Int)>.makeStream()
38+
self.foldView = foldView
39+
(textChangedStream, textChangedStreamContinuation) = AsyncStream<Void>.makeStream()
3840
self.calculator = LineFoldCalculator(
39-
foldProvider: foldProvider,
4041
controller: controller,
4142
textChangedStream: textChangedStream
4243
)
@@ -49,7 +50,7 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
4950
foldView?.needsDisplay = true
5051
}
5152
}
52-
textChangedStreamContinuation.yield((.zero, 0))
53+
textChangedStreamContinuation.yield(Void())
5354
}
5455

5556
func getFolds(in range: Range<Int>) -> [FoldRange] {
@@ -66,7 +67,7 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
6667
return
6768
}
6869
foldCache.storageUpdated(editedRange: editedRange, changeInLength: delta)
69-
textChangedStreamContinuation.yield((editedRange, delta))
70+
textChangedStreamContinuation.yield()
7071
}
7172

7273
/// Finds the deepest cached depth of the fold for a line number.
@@ -126,3 +127,13 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
126127
controller?.textView.emphasisManager?.removeEmphases(for: Self.emphasisId)
127128
}
128129
}
130+
131+
// MARK: - LineFoldPlaceholderDelegate
132+
133+
extension LineFoldingModel: LineFoldPlaceholderDelegate {
134+
func placeholderDiscarded(fold: FoldRange) {
135+
foldCache.toggleCollapse(forFold: fold)
136+
foldView?.needsDisplay = true
137+
textChangedStreamContinuation.yield()
138+
}
139+
}

0 commit comments

Comments
 (0)