Skip to content

Commit 9a134d1

Browse files
Folding Ribbon: Collapsed Indicators, Masking, Bracket Emphasis (#332)
1 parent ea47016 commit 9a134d1

File tree

12 files changed

+361
-169
lines changed

12 files changed

+361
-169
lines changed

Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,6 @@ import TextFormation
1111
import TextStory
1212

1313
extension TextViewController {
14-
15-
internal enum BracketPairs {
16-
static let allValues: [(String, String)] = [
17-
("{", "}"),
18-
("[", "]"),
19-
("(", ")"),
20-
("\"", "\""),
21-
("'", "'")
22-
]
23-
24-
static let emphasisValues: [(String, String)] = [
25-
("{", "}"),
26-
("[", "]"),
27-
("(", ")")
28-
]
29-
}
30-
3114
// MARK: - Filter Configuration
3215

3316
/// Initializes any filters for text editing.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// BracketPairs.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 6/5/25.
6+
//
7+
8+
enum BracketPairs {
9+
static let allValues: [(String, String)] = [
10+
("{", "}"),
11+
("[", "]"),
12+
("(", ")"),
13+
("\"", "\""),
14+
("'", "'")
15+
]
16+
17+
static let emphasisValues: [(String, String)] = [
18+
("{", "}"),
19+
("[", "]"),
20+
("(", ")")
21+
]
22+
23+
/// Checks if the given string is a matchable emphasis string.
24+
/// - Parameter potentialMatch: The string to check for matches.
25+
/// - Returns: True if a match was found with either start or end bracket pairs.
26+
static func matches(_ potentialMatch: String) -> Bool {
27+
allValues.contains(where: { $0.0 == potentialMatch || $0.1 == potentialMatch })
28+
}
29+
}

Sources/CodeEditSourceEditor/Extensions/NSBezierPath+RoundedCorners.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extension NSBezierPath {
2222
public static let bottomLeft = Corners(rawValue: 1 << 1)
2323
public static let topRight = Corners(rawValue: 1 << 2)
2424
public static let bottomRight = Corners(rawValue: 1 << 3)
25+
public static let all: Corners = Corners(rawValue: 0b1111)
2526
}
2627

2728
// swiftlint:disable:next function_body_length
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// NSColor+LightDark.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 6/4/25.
6+
//
7+
8+
import AppKit
9+
10+
extension NSColor {
11+
convenience init(light: NSColor, dark: NSColor) {
12+
self.init(name: nil) { appearance in
13+
return switch appearance.name {
14+
case .aqua:
15+
light
16+
case .darkAqua:
17+
dark
18+
default:
19+
NSColor()
20+
}
21+
}
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// NSRect+Transform.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 6/4/25.
6+
//
7+
8+
import AppKit
9+
10+
extension NSRect {
11+
func transform(x xVal: CGFloat = 0, y yVal: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0) -> NSRect {
12+
NSRect(
13+
x: self.origin.x + xVal,
14+
y: self.origin.y + yVal,
15+
width: self.width + width,
16+
height: self.height + height
17+
)
18+
}
19+
}

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldCalculator.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ actor LineFoldCalculator {
1919

2020
private var valueStreamContinuation: AsyncStream<LineFoldStorage>.Continuation
2121
private var textChangedTask: Task<Void, Never>?
22-
22+
2323
/// Create a new calculator object that listens to a given stream for text changes.
2424
/// - Parameters:
2525
/// - foldProvider: The object to use to calculate fold regions.
@@ -39,7 +39,7 @@ actor LineFoldCalculator {
3939
deinit {
4040
textChangedTask?.cancel()
4141
}
42-
42+
4343
/// Sets up an attached task to listen to values on a stream of text changes.
4444
/// - Parameter textChangedStream: A stream of text changes.
4545
private func listenToTextChanges(textChangedStream: AsyncStream<(NSRange, Int)>) {
@@ -105,7 +105,7 @@ actor LineFoldCalculator {
105105

106106
await yieldNewStorage(newFolds: foldCache, controller: controller, documentRange: documentRange)
107107
}
108-
108+
109109
/// Yield a new storage value on the value stream using a new set of folds.
110110
/// - Parameters:
111111
/// - newFolds: The new folds to yield with the storage value.

Sources/CodeEditSourceEditor/LineFolding/Model/LineFoldingModel.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import Combine
1919
/// - Loop through the list, creating nested folds as indents go up and down.
2020
///
2121
class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
22+
static let emphasisId = "lineFolding"
23+
24+
2225
/// An ordered tree of fold ranges in a document. Can be traversed using ``FoldRange/parent``
2326
/// and ``FoldRange/subFolds``.
2427
@Published var foldCache: LineFoldStorage = LineFoldStorage(documentLength: 0)
@@ -92,4 +95,35 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
9295
}
9396
return deepestFold
9497
}
98+
99+
func emphasizeBracketsForFold(_ fold: FoldRange) {
100+
clearEmphasis()
101+
102+
// Find the text object, make sure there's available characters around the fold.
103+
guard let text = controller?.textView.textStorage.string as? NSString,
104+
fold.range.lowerBound > 0 && fold.range.upperBound < text.length - 1 else {
105+
return
106+
}
107+
108+
let firstRange = NSRange(location: fold.range.lowerBound - 1, length: 1)
109+
let secondRange = NSRange(location: fold.range.upperBound, length: 1)
110+
111+
// Check if these are emphasizable bracket pairs.
112+
guard BracketPairs.matches(text.substring(from: firstRange) ?? "")
113+
&& BracketPairs.matches(text.substring(from: secondRange) ?? "") else {
114+
return
115+
}
116+
117+
controller?.textView.emphasisManager?.addEmphases(
118+
[
119+
Emphasis(range: firstRange, style: .standard, flash: false, inactive: false, selectInDocument: false),
120+
Emphasis(range: secondRange, style: .standard, flash: false, inactive: false, selectInDocument: false),
121+
],
122+
for: Self.emphasisId
123+
)
124+
}
125+
126+
func clearEmphasis() {
127+
controller?.textView.emphasisManager?.removeEmphases(for: Self.emphasisId)
128+
}
95129
}

Sources/CodeEditSourceEditor/LineFolding/Placeholder/LineFoldPlaceholder.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,40 @@ import CodeEditTextView
1010

1111
class LineFoldPlaceholder: TextAttachment {
1212
let fold: FoldRange
13+
let charWidth: CGFloat
14+
var isSelected: Bool = false
1315

14-
init(fold: FoldRange) {
16+
init(fold: FoldRange, charWidth: CGFloat) {
1517
self.fold = fold
18+
self.charWidth = charWidth
1619
}
1720

18-
var width: CGFloat { 17 }
21+
var width: CGFloat {
22+
charWidth * 5
23+
}
1924

2025
func draw(in context: CGContext, rect: NSRect) {
2126
context.saveGState()
2227

2328
let centerY = rect.midY - 1.5
2429

30+
if isSelected {
31+
context.setFillColor(NSColor.controlAccentColor.cgColor)
32+
context.addPath(
33+
NSBezierPath(
34+
rect: rect.transform(x: 2.0, y: 3.0, width: -4.0, height: -6.0 ),
35+
roundedCorners: .all,
36+
cornerRadius: 2
37+
).cgPathFallback
38+
)
39+
context.fillPath()
40+
}
41+
2542
context.setFillColor(NSColor.secondaryLabelColor.cgColor)
26-
context.addEllipse(in: CGRect(x: rect.minX + 2, y: centerY, width: 3, height: 3))
27-
context.addEllipse(in: CGRect(x: rect.minX + 7, y: centerY, width: 3, height: 3))
28-
context.addEllipse(in: CGRect(x: rect.minX + 12, y: centerY, width: 3, height: 3))
43+
let size = charWidth / 2
44+
context.addEllipse(in: CGRect(x: rect.minX + charWidth * 1.25, y: centerY, width: size, height: size))
45+
context.addEllipse(in: CGRect(x: rect.minX + (charWidth * 2.25), y: centerY, width: size, height: size))
46+
context.addEllipse(in: CGRect(x: rect.minX + (charWidth * 3.25), y: centerY, width: size, height: size))
2947
context.fillPath()
3048

3149
context.restoreGState()

0 commit comments

Comments
 (0)