Skip to content

Commit 0680885

Browse files
committed
Document Adding a Parameter, Finish Merge Invisible Chars API
1 parent c25bbac commit 0680885

File tree

10 files changed

+129
-48
lines changed

10 files changed

+129
-48
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ struct ContentView: View {
3030
@AppStorage("showGutter") private var showGutter: Bool = true
3131
@AppStorage("showMinimap") private var showMinimap: Bool = true
3232
@AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false
33-
@State private var invisibleCharactersConfig: InvisibleCharactersConfig = .empty
33+
@State private var invisibleCharactersConfig: InvisibleCharactersConfiguration = .empty
34+
@State private var warningCharacters: Set<UInt16> = []
3435

3536
@State private var isInLongParse = false
3637
@State private var settingsIsPresented: Bool = false
@@ -61,7 +62,9 @@ struct ContentView: View {
6162
peripherals: .init(
6263
showGutter: showGutter,
6364
showMinimap: showMinimap,
64-
showReformattingGuide: showReformattingGuide
65+
showReformattingGuide: showReformattingGuide,
66+
invisibleCharactersConfiguration: invisibleCharactersConfig,
67+
warningCharacters: warningCharacters
6568
)
6669
),
6770
cursorPositions: $cursorPositions
@@ -81,7 +84,8 @@ struct ContentView: View {
8184
indentOption: $indentOption,
8285
reformatAtColumn: $reformatAtColumn,
8386
showReformattingGuide: $showReformattingGuide,
84-
invisibles: $invisibleCharactersConfig
87+
invisibles: $invisibleCharactersConfig,
88+
warningCharacters: $warningCharacters
8589
)
8690
}
8791
.ignoresSafeArea()

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ struct StatusBar: View {
2727
@Binding var indentOption: IndentOption
2828
@Binding var reformatAtColumn: Int
2929
@Binding var showReformattingGuide: Bool
30-
@Binding var invisibles: InvisibleCharactersConfig
30+
@Binding var invisibles: InvisibleCharactersConfiguration
31+
@Binding var warningCharacters: Set<UInt16>
3132

3233
var body: some View {
3334
HStack {
@@ -63,16 +64,16 @@ struct StatusBar: View {
6364
"Warning Characters",
6465
isOn: Binding(
6566
get: {
66-
!invisibles.warningCharacters.isEmpty
67+
!warningCharacters.isEmpty
6768
},
6869
set: { newValue in
6970
// In this example app, we only add one character
7071
// For real apps, consider providing a table where users can add UTF16
7172
// char codes to warn about, as well as a set of good defaults.
7273
if newValue {
73-
invisibles.warningCharacters.insert(0x200B) // zero-width space
74+
warningCharacters.insert(0x200B) // zero-width space
7475
} else {
75-
invisibles.warningCharacters.removeAll()
76+
warningCharacters.removeAll()
7677
}
7778
}
7879
)

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public class TextViewController: NSViewController {
3232
/// The reformatting guide view
3333
var reformattingGuideView: ReformattingGuideView!
3434

35+
/// Middleman between the text view to our invisible characters config, with knowledge of things like the
36+
/// /// user's theme and indent option to help correctly draw invisible character placeholders.
37+
var invisibleCharactersCoordinator: InvisibleCharactersCoordinator
38+
3539
var minimapXConstraint: NSLayoutConstraint?
3640

3741
var _undoManager: CEUndoManager!
@@ -140,6 +144,17 @@ public class TextViewController: NSViewController {
140144
/// Toggle the visibility of the reformatting guide in the editor.
141145
public var showReformattingGuide: Bool { configuration.peripherals.showReformattingGuide }
142146

147+
/// Configuration for drawing invisible characters.
148+
///
149+
/// See ``InvisibleCharactersConfiguration`` for more details.
150+
public var invisibleCharactersConfiguration: InvisibleCharactersConfiguration {
151+
configuration.peripherals.invisibleCharactersConfiguration
152+
}
153+
154+
/// Indicates characters that the user may not have meant to insert, such as a zero-width space: `(0x200D)` or a
155+
/// non-standard quote character: `“ (0x201C)`.
156+
public var warningCharacters: Set<UInt16> { configuration.peripherals.warningCharacters }
157+
143158
// MARK: - Internal Variables
144159

145160
var textCoordinators: [WeakCoordinator] = []
@@ -177,17 +192,18 @@ public class TextViewController: NSViewController {
177192
public init(
178193
string: String,
179194
language: CodeLanguage,
180-
config: SourceEditorConfiguration,
195+
configuration: SourceEditorConfiguration,
181196
cursorPositions: [CursorPosition],
182197
highlightProviders: [HighlightProviding] = [TreeSitterClient()],
183198
undoManager: CEUndoManager? = nil,
184199
coordinators: [TextViewCoordinator] = []
185200
) {
186201
self.language = language
187-
self.configuration = config
202+
self.configuration = configuration
188203
self.cursorPositions = cursorPositions
189204
self.highlightProviders = highlightProviders
190205
self._undoManager = undoManager
206+
self.invisibleCharactersCoordinator = InvisibleCharactersCoordinator(configuration: configuration)
191207

192208
super.init(nibName: nil, bundle: nil)
193209

Sources/CodeEditSourceEditor/InvisibleCharacters/InvisibleCharactersConfig.swift renamed to Sources/CodeEditSourceEditor/InvisibleCharacters/InvisibleCharactersConfiguration.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// InvisibleCharactersConfig.swift
2+
// InvisibleCharactersConfiguration.swift
33
// CodeEditSourceEditor
44
//
55
// Created by Khan Winter on 6/11/25.
@@ -9,10 +9,10 @@
99
///
1010
/// Enable specific categories using the ``showSpaces``, ``showTabs``, and ``showLineEndings`` toggles. Customize
1111
/// drawing further with the ``spaceReplacement`` and family variables.
12-
public struct InvisibleCharactersConfig: Equatable, Hashable, Sendable, Codable {
12+
public struct InvisibleCharactersConfiguration: Equatable, Hashable, Sendable, Codable {
1313
/// An empty configuration.
14-
public static var empty: InvisibleCharactersConfig {
15-
InvisibleCharactersConfig(showSpaces: false, showTabs: false, showLineEndings: false)
14+
public static var empty: InvisibleCharactersConfiguration {
15+
InvisibleCharactersConfiguration(showSpaces: false, showTabs: false, showLineEndings: false)
1616
}
1717

1818
/// Set to true to draw spaces with a dot.

Sources/CodeEditSourceEditor/InvisibleCharacters/InvisibleCharactersCoordinator.swift

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import CodeEditTextView
1919
/// theme, or font are updated, this object will tell the text view to clear it's cache. Keep updates to a minimum to
2020
/// retain as much cached data as possible.
2121
final class InvisibleCharactersCoordinator: InvisibleCharactersDelegate {
22-
var config: InvisibleCharactersConfig {
22+
var configuration: InvisibleCharactersConfiguration {
2323
didSet {
2424
updateTriggerCharacters()
2525
}
@@ -60,14 +60,24 @@ final class InvisibleCharactersCoordinator: InvisibleCharactersDelegate {
6060
/// The set of characters the text view should trigger a call to ``invisibleStyle`` for.
6161
var triggerCharacters: Set<UInt16> = []
6262

63+
convenience init(configuration: SourceEditorConfiguration) {
64+
self.init(
65+
configuration: configuration.peripherals.invisibleCharactersConfiguration,
66+
warningCharacters: configuration.peripherals.warningCharacters,
67+
indentOption: configuration.behavior.indentOption,
68+
theme: configuration.appearance.theme,
69+
font: configuration.appearance.font
70+
)
71+
}
72+
6373
init(
64-
config: InvisibleCharactersConfig,
74+
configuration: InvisibleCharactersConfiguration,
6575
warningCharacters: Set<UInt16>,
6676
indentOption: IndentOption,
6777
theme: EditorTheme,
6878
font: NSFont
6979
) {
70-
self.config = config
80+
self.configuration = configuration
7181
self.warningCharacters = warningCharacters
7282
self.indentOption = indentOption
7383
self.theme = theme
@@ -83,7 +93,7 @@ final class InvisibleCharactersCoordinator: InvisibleCharactersDelegate {
8393
}
8494

8595
private func updateTriggerCharacters() {
86-
triggerCharacters = config.triggerCharacters().union(warningCharacters)
96+
triggerCharacters = configuration.triggerCharacters().union(warningCharacters)
8797
}
8898

8999
/// Determines if the textview should clear cached styles.
@@ -103,62 +113,62 @@ final class InvisibleCharactersCoordinator: InvisibleCharactersDelegate {
103113
/// often and is cached in ``emphasizedFont``.
104114
func invisibleStyle(for character: UInt16, at range: NSRange, lineRange: NSRange) -> InvisibleCharacterStyle? {
105115
switch character {
106-
case InvisibleCharactersConfig.Symbols.space:
116+
case InvisibleCharactersConfiguration.Symbols.space:
107117
return spacesStyle(range: range, lineRange: lineRange)
108-
case InvisibleCharactersConfig.Symbols.tab:
118+
case InvisibleCharactersConfiguration.Symbols.tab:
109119
return tabStyle()
110-
case InvisibleCharactersConfig.Symbols.carriageReturn:
120+
case InvisibleCharactersConfiguration.Symbols.carriageReturn:
111121
return carriageReturnStyle()
112-
case InvisibleCharactersConfig.Symbols.lineFeed:
122+
case InvisibleCharactersConfiguration.Symbols.lineFeed:
113123
return lineFeedStyle()
114-
case InvisibleCharactersConfig.Symbols.paragraphSeparator:
124+
case InvisibleCharactersConfiguration.Symbols.paragraphSeparator:
115125
return paragraphSeparatorStyle()
116-
case InvisibleCharactersConfig.Symbols.lineSeparator:
126+
case InvisibleCharactersConfiguration.Symbols.lineSeparator:
117127
return lineSeparatorStyle()
118128
default:
119129
return warningCharacterStyle(for: character)
120130
}
121131
}
122132

123133
private func spacesStyle(range: NSRange, lineRange: NSRange) -> InvisibleCharacterStyle? {
124-
guard config.showSpaces else { return nil }
134+
guard configuration.showSpaces else { return nil }
125135
let locationInLine = range.location - lineRange.location
126136
let shouldBold = locationInLine % indentOption.charCount == indentOption.charCount - 1
127137
return .replace(
128-
replacementCharacter: config.spaceReplacement,
138+
replacementCharacter: configuration.spaceReplacement,
129139
color: invisibleColor,
130140
font: shouldBold ? emphasizedFont : font
131141
)
132142
}
133143

134144
private func tabStyle() -> InvisibleCharacterStyle? {
135-
guard config.showTabs else { return nil }
136-
return .replace(replacementCharacter: config.tabReplacement, color: invisibleColor, font: font)
145+
guard configuration.showTabs else { return nil }
146+
return .replace(replacementCharacter: configuration.tabReplacement, color: invisibleColor, font: font)
137147
}
138148

139149
private func carriageReturnStyle() -> InvisibleCharacterStyle? {
140-
guard config.showLineEndings else { return nil }
141-
return .replace(replacementCharacter: config.carriageReturnReplacement, color: invisibleColor, font: font)
150+
guard configuration.showLineEndings else { return nil }
151+
return .replace(replacementCharacter: configuration.carriageReturnReplacement, color: invisibleColor, font: font)
142152
}
143153

144154
private func lineFeedStyle() -> InvisibleCharacterStyle? {
145-
guard config.showLineEndings else { return nil }
146-
return .replace(replacementCharacter: config.lineFeedReplacement, color: invisibleColor, font: font)
155+
guard configuration.showLineEndings else { return nil }
156+
return .replace(replacementCharacter: configuration.lineFeedReplacement, color: invisibleColor, font: font)
147157
}
148158

149159
private func paragraphSeparatorStyle() -> InvisibleCharacterStyle? {
150-
guard config.showLineEndings else { return nil }
160+
guard configuration.showLineEndings else { return nil }
151161
return .replace(
152-
replacementCharacter: config.paragraphSeparatorReplacement,
162+
replacementCharacter: configuration.paragraphSeparatorReplacement,
153163
color: invisibleColor,
154164
font: font
155165
)
156166
}
157167

158168
private func lineSeparatorStyle() -> InvisibleCharacterStyle? {
159-
guard config.showLineEndings else { return nil }
169+
guard configuration.showLineEndings else { return nil }
160170
return .replace(
161-
replacementCharacter: config.lineSeparatorReplacement,
171+
replacementCharacter: configuration.lineSeparatorReplacement,
162172
color: invisibleColor,
163173
font: font
164174
)

Sources/CodeEditSourceEditor/SourceEditor/SourceEditor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public struct SourceEditor: NSViewControllerRepresentable {
8989
let controller = TextViewController(
9090
string: "",
9191
language: language,
92-
config: configuration,
92+
configuration: configuration,
9393
cursorPositions: cursorPositions.wrappedValue,
9494
highlightProviders: context.coordinator.highlightProviders,
9595
undoManager: undoManager,

Sources/CodeEditSourceEditor/SourceEditorConfiguration/SourceEditorConfiguration+Peripherals.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,27 @@ extension SourceEditorConfiguration {
1616
/// Whether to show the reformatting guide.
1717
public var showReformattingGuide: Bool
1818

19+
/// Configuration for drawing invisible characters.
20+
///
21+
/// See ``InvisibleCharactersConfiguration`` for more details.
22+
public var invisibleCharactersConfiguration: InvisibleCharactersConfiguration
23+
24+
/// Indicates characters that the user may not have meant to insert, such as a zero-width space: `(0x200D)` or a
25+
/// non-standard quote character: `“ (0x201C)`.
26+
public var warningCharacters: Set<UInt16>
27+
1928
public init(
2029
showGutter: Bool = true,
2130
showMinimap: Bool = true,
22-
showReformattingGuide: Bool = false
31+
showReformattingGuide: Bool = false,
32+
invisibleCharactersConfiguration: InvisibleCharactersConfiguration = .empty,
33+
warningCharacters: Set<UInt16> = []
2334
) {
2435
self.showGutter = showGutter
2536
self.showMinimap = showMinimap
2637
self.showReformattingGuide = showReformattingGuide
38+
self.invisibleCharactersConfiguration = invisibleCharactersConfiguration
39+
self.warningCharacters = warningCharacters
2740
}
2841

2942
@MainActor
@@ -45,6 +58,14 @@ extension SourceEditorConfiguration {
4558
controller.reformattingGuideView.updatePosition(in: controller)
4659
}
4760

61+
if oldConfig?.invisibleCharactersConfiguration != invisibleCharactersConfiguration {
62+
controller.invisibleCharactersCoordinator.configuration = invisibleCharactersConfiguration
63+
}
64+
65+
if oldConfig?.warningCharacters != warningCharacters {
66+
controller.invisibleCharactersCoordinator.warningCharacters = warningCharacters
67+
}
68+
4869
if shouldUpdateInsets && controller.scrollView != nil { // Check for view existence
4970
controller.updateContentInsets()
5071
controller.updateTextInsets()

Sources/CodeEditSourceEditor/SourceEditorConfiguration/SourceEditorConfiguration.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@
77

88
import AppKit
99

10+
/// # Dev Note
11+
///
12+
/// If you're looking to **add a parameter**, make sure you check these off:
13+
/// - Determine what category it should go in. If what your adding changes often during editing (like cursor positions),
14+
/// *it doesn't belong here*. These should be configurable, but (mostly) constant options (like the user's font).
15+
/// - Add the parameter as a *public, mutable, variable* on that category. If it should have a default value, add it
16+
/// to the variable.
17+
/// - Add the parameter to that category's initializer, if it should have a default value, add it here too.
18+
/// - Add a public variable to `TextViewController` in the "Config Helpers" mark with the same name and type.
19+
/// The variable should be a passthrough variable to the configuration object. Eg:
20+
/// ```swift
21+
/// // in config:
22+
/// var myVariable: Bool
23+
///
24+
/// // in TextViewController
25+
/// public var myVariable: Bool { configuration.category.myVariable }
26+
/// ```
27+
/// - Add a new case to the category's `didSetOnController` method. You should check if the parameter has changed, and
28+
/// update the text view controller as necessary to reflect the updated configuration.
29+
/// - Add documentation in:
30+
/// - The variable in the category.
31+
/// - The category initializer.
32+
/// - The passthrough variable in `TextViewController`.
33+
34+
1035
/// Configuration object for the ``SourceEditor``. Determines appearance, behavior, layout and what features are
1136
/// enabled (peripherals).
1237
///

0 commit comments

Comments
 (0)