Skip to content

Commit cd0a8b9

Browse files
committed
Renamed EmphasisAPI to EmphasisManager. Separated concerns by moving match cycle logic from EmphasisManager to FindViewController. Using EmphasisManager in bracket pair matching instead of custom implementation reducing duplicated code. Implemented flash find matches when clicking the next and previous buttons when the editor is in focus. bracketPairHighlight becomes bracketPairEmphasis. Fixed various find issues and cleaned up implementation.
1 parent 968a4e9 commit cd0a8b9

16 files changed

+520
-477
lines changed

Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
4040
/// value is true, and `isEditable` is false, the editor is selectable but not editable.
4141
/// - letterSpacing: The amount of space to use between letters, as a percent. Eg: `1.0` = no space, `1.5` = 1/2 a
4242
/// character's width between characters, etc. Defaults to `1.0`
43-
/// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
43+
/// - bracketPairEmphasis: The type of highlight to use to highlight bracket pairs.
4444
/// See `BracketPairHighlight` for more information. Defaults to `nil`
4545
/// - useSystemCursor: If true, uses the system cursor on `>=macOS 14`.
4646
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
@@ -62,7 +62,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
6262
isEditable: Bool = true,
6363
isSelectable: Bool = true,
6464
letterSpacing: Double = 1.0,
65-
bracketPairHighlight: BracketPairHighlight? = nil,
65+
bracketPairEmphasis: BracketPairEmphasis? = .flash,
6666
useSystemCursor: Bool = true,
6767
undoManager: CEUndoManager? = nil,
6868
coordinators: [any TextViewCoordinator] = []
@@ -83,7 +83,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
8383
self.isEditable = isEditable
8484
self.isSelectable = isSelectable
8585
self.letterSpacing = letterSpacing
86-
self.bracketPairHighlight = bracketPairHighlight
86+
self.bracketPairEmphasis = bracketPairEmphasis
8787
if #available(macOS 14, *) {
8888
self.useSystemCursor = useSystemCursor
8989
} else {
@@ -116,8 +116,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
116116
/// value is true, and `isEditable` is false, the editor is selectable but not editable.
117117
/// - letterSpacing: The amount of space to use between letters, as a percent. Eg: `1.0` = no space, `1.5` = 1/2 a
118118
/// character's width between characters, etc. Defaults to `1.0`
119-
/// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
120-
/// See `BracketPairHighlight` for more information. Defaults to `nil`
119+
/// - bracketPairEmphasis: The type of highlight to use to highlight bracket pairs.
120+
/// See `BracketPairEmphasis` for more information. Defaults to `nil`
121121
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
122122
/// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
123123
public init(
@@ -137,7 +137,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
137137
isEditable: Bool = true,
138138
isSelectable: Bool = true,
139139
letterSpacing: Double = 1.0,
140-
bracketPairHighlight: BracketPairHighlight? = nil,
140+
bracketPairEmphasis: BracketPairEmphasis? = .flash,
141141
useSystemCursor: Bool = true,
142142
undoManager: CEUndoManager? = nil,
143143
coordinators: [any TextViewCoordinator] = []
@@ -158,7 +158,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
158158
self.isEditable = isEditable
159159
self.isSelectable = isSelectable
160160
self.letterSpacing = letterSpacing
161-
self.bracketPairHighlight = bracketPairHighlight
161+
self.bracketPairEmphasis = bracketPairEmphasis
162162
if #available(macOS 14, *) {
163163
self.useSystemCursor = useSystemCursor
164164
} else {
@@ -184,7 +184,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
184184
private var isEditable: Bool
185185
private var isSelectable: Bool
186186
private var letterSpacing: Double
187-
private var bracketPairHighlight: BracketPairHighlight?
187+
private var bracketPairEmphasis: BracketPairEmphasis?
188188
private var useSystemCursor: Bool
189189
private var undoManager: CEUndoManager?
190190
package var coordinators: [any TextViewCoordinator]
@@ -210,7 +210,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
210210
isSelectable: isSelectable,
211211
letterSpacing: letterSpacing,
212212
useSystemCursor: useSystemCursor,
213-
bracketPairHighlight: bracketPairHighlight,
213+
bracketPairEmphasis: bracketPairEmphasis,
214214
undoManager: undoManager,
215215
coordinators: coordinators
216216
)
@@ -309,7 +309,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
309309
controller.setHighlightProviders(highlightProviders)
310310
}
311311

312-
controller.bracketPairHighlight = bracketPairHighlight
312+
if controller.bracketPairEmphasis != bracketPairEmphasis {
313+
controller.bracketPairEmphasis = bracketPairEmphasis
314+
}
313315
}
314316

315317
/// Checks if the controller needs updating.
@@ -329,7 +331,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
329331
controller.indentOption == indentOption &&
330332
controller.tabWidth == tabWidth &&
331333
controller.letterSpacing == letterSpacing &&
332-
controller.bracketPairHighlight == bracketPairHighlight &&
334+
controller.bracketPairEmphasis == bracketPairEmphasis &&
333335
controller.useSystemCursor == useSystemCursor &&
334336
areHighlightProvidersEqual(controller: controller)
335337
}
@@ -359,7 +361,7 @@ public struct CodeEditTextView: View {
359361
isEditable: Bool = true,
360362
isSelectable: Bool = true,
361363
letterSpacing: Double = 1.0,
362-
bracketPairHighlight: BracketPairHighlight? = nil,
364+
bracketPairEmphasis: BracketPairEmphasis? = nil,
363365
undoManager: CEUndoManager? = nil,
364366
coordinators: [any TextViewCoordinator] = []
365367
) {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//
2+
// TextViewController+EmphasizeBracket.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/26/23.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
11+
extension TextViewController {
12+
/// Emphasizes bracket pairs using the current selection.
13+
internal func emphasizeSelectionPairs() {
14+
guard bracketPairEmphasis != nil else { return }
15+
textView.emphasisManager?.removeEmphases(for: "bracketPairs")
16+
for range in textView.selectionManager.textSelections.map({ $0.range }) {
17+
if range.isEmpty,
18+
range.location > 0, // Range is not the beginning of the document
19+
let precedingCharacter = textView.textStorage.substring(
20+
from: NSRange(location: range.location - 1, length: 1) // The preceding character exists
21+
) {
22+
for pair in BracketPairs.emphasisValues {
23+
if precedingCharacter == pair.0 {
24+
// Walk forwards
25+
if let characterIndex = findClosingPair(
26+
pair.0,
27+
pair.1,
28+
from: range.location,
29+
limit: min(NSMaxRange(textView.visibleTextRange ?? .zero) + 4096,
30+
NSMaxRange(textView.documentRange)),
31+
reverse: false
32+
) {
33+
emphasizeCharacter(characterIndex)
34+
if bracketPairEmphasis?.emphasizesSourceBracket ?? false {
35+
emphasizeCharacter(range.location - 1)
36+
}
37+
}
38+
} else if precedingCharacter == pair.1 && range.location - 1 > 0 {
39+
// Walk backwards
40+
if let characterIndex = findClosingPair(
41+
pair.1,
42+
pair.0,
43+
from: range.location - 1,
44+
limit: max((textView.visibleTextRange?.location ?? 0) - 4096,
45+
textView.documentRange.location),
46+
reverse: true
47+
) {
48+
emphasizeCharacter(characterIndex)
49+
if bracketPairEmphasis?.emphasizesSourceBracket ?? false {
50+
emphasizeCharacter(range.location - 1)
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
/// # Dev Note
60+
/// It's interesting to note that this problem could trivially be turned into a monoid, and the locations of each
61+
/// pair start/end location determined when the view is loaded. It could then be parallelized for initial speed
62+
/// and this lookup would be much faster.
63+
64+
/// Finds a closing character given a pair of characters, ignores pairs inside the given pair.
65+
///
66+
/// ```pseudocode
67+
/// { -- Start
68+
/// {
69+
/// } -- A naive algorithm may find this character as the closing pair, which would be incorrect.
70+
/// } -- Found
71+
/// ```
72+
///
73+
/// - Parameters:
74+
/// - open: The opening pair to look for.
75+
/// - close: The closing pair to look for.
76+
/// - from: The index to start from. This should not include the start character. Eg given `"{ }"` looking forward
77+
/// the index should be `1`
78+
/// - limit: A limiting index to stop at. When `reverse` is `true`, this is the minimum index. When `false` this
79+
/// is the maximum index.
80+
/// - reverse: Set to `true` to walk backwards from `from`.
81+
/// - Returns: The index of the found closing pair, if any.
82+
internal func findClosingPair(_ close: String, _ open: String, from: Int, limit: Int, reverse: Bool) -> Int? {
83+
// Walk the text, counting each close. When we find an open that makes closeCount < 0, return that index.
84+
var options: NSString.EnumerationOptions = .byCaretPositions
85+
if reverse {
86+
options = options.union(.reverse)
87+
}
88+
var closeCount = 0
89+
var index: Int?
90+
textView.textStorage.mutableString.enumerateSubstrings(
91+
in: reverse ?
92+
NSRange(location: limit, length: from - limit) :
93+
NSRange(location: from, length: limit - from),
94+
options: options,
95+
using: { substring, range, _, stop in
96+
if substring == close {
97+
closeCount += 1
98+
} else if substring == open {
99+
closeCount -= 1
100+
}
101+
102+
if closeCount < 0 {
103+
index = range.location
104+
stop.pointee = true
105+
}
106+
}
107+
)
108+
return index
109+
}
110+
111+
/// Adds a temporary emphasis effect to the character at the given location.
112+
/// - Parameters:
113+
/// - location: The location of the character to emphasize
114+
/// - scrollToRange: Set to true to scroll to the given range when emphasizing. Defaults to `false`.
115+
private func emphasizeCharacter(_ location: Int, scrollToRange: Bool = false) {
116+
guard let bracketPairEmphasis = bracketPairEmphasis,
117+
let rectToEmphasize = textView.layoutManager.rectForOffset(location) else {
118+
return
119+
}
120+
121+
let range = NSRange(location: location, length: 1)
122+
123+
switch bracketPairEmphasis {
124+
case .flash:
125+
textView.emphasisManager?.addEmphasis(
126+
Emphasis(
127+
range: range,
128+
style: .standard,
129+
flash: true,
130+
inactive: false
131+
),
132+
for: "bracketPairs"
133+
)
134+
case .bordered(let borderColor):
135+
textView.emphasisManager?.addEmphasis(
136+
Emphasis(
137+
range: range,
138+
style: .outline(color: borderColor),
139+
flash: false,
140+
inactive: false
141+
),
142+
for: "bracketPairs"
143+
)
144+
case .underline(let underlineColor):
145+
textView.emphasisManager?.addEmphasis(
146+
Emphasis(
147+
range: range,
148+
style: .underline(color: underlineColor),
149+
flash: false,
150+
inactive: false
151+
),
152+
for: "bracketPairs"
153+
)
154+
}
155+
}
156+
}

Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension TextViewController: FindPanelTarget {
1919
gutterView.frame.origin.y = -scrollView.contentInsets.top
2020
}
2121

22-
var emphasizeAPI: EmphasizeAPI? {
23-
textView?.emphasizeAPI
22+
var emphasisManager: EmphasisManager? {
23+
textView?.emphasisManager
2424
}
2525
}

0 commit comments

Comments
 (0)