55// Created by Lukas Pistrol on 24.05.22.
66//
77
8+ import AppKit
89import SwiftUI
910import CodeEditTextView
1011import CodeEditLanguages
1112
13+ /// A SwiftUI View that provides source editing functionality.
1214public struct CodeEditSourceEditor : NSViewControllerRepresentable {
15+ package enum TextAPI {
16+ case binding( Binding < String > )
17+ case storage( NSTextStorage )
18+ }
1319
1420 /// Initializes a Text Editor
1521 /// - Parameters:
@@ -22,7 +28,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
2228 /// - lineHeight: The line height multiplier (e.g. `1.2`)
2329 /// - wrapLines: Whether lines wrap to the width of the editor
2430 /// - editorOverscroll: The distance to overscroll the editor by.
25- /// - cursorPosition : The cursor's position in the editor, measured in `(lineNum, columnNum)`
31+ /// - cursorPositions : The cursor's position in the editor, measured in `(lineNum, columnNum)`
2632 /// - useThemeBackground: Determines whether the editor uses the theme's background color, or a transparent
2733 /// background color
2834 /// - highlightProvider: A class you provide to perform syntax highlighting. Leave this as `nil` to use the
@@ -37,6 +43,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
3743 /// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
3844 /// See `BracketPairHighlight` for more information. Defaults to `nil`
3945 /// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
46+ /// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
4047 public init (
4148 _ text: Binding < String > ,
4249 language: CodeLanguage ,
@@ -58,7 +65,76 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
5865 undoManager: CEUndoManager ? = nil ,
5966 coordinators: [ any TextViewCoordinator ] = [ ]
6067 ) {
61- self . _text = text
68+ self . text = . binding( text)
69+ self . language = language
70+ self . theme = theme
71+ self . useThemeBackground = useThemeBackground
72+ self . font = font
73+ self . tabWidth = tabWidth
74+ self . indentOption = indentOption
75+ self . lineHeight = lineHeight
76+ self . wrapLines = wrapLines
77+ self . editorOverscroll = editorOverscroll
78+ self . cursorPositions = cursorPositions
79+ self . highlightProvider = highlightProvider
80+ self . contentInsets = contentInsets
81+ self . isEditable = isEditable
82+ self . isSelectable = isSelectable
83+ self . letterSpacing = letterSpacing
84+ self . bracketPairHighlight = bracketPairHighlight
85+ self . undoManager = undoManager
86+ self . coordinators = coordinators
87+ }
88+
89+ /// Initializes a Text Editor
90+ /// - Parameters:
91+ /// - text: The text content
92+ /// - language: The language for syntax highlighting
93+ /// - theme: The theme for syntax highlighting
94+ /// - font: The default font
95+ /// - tabWidth: The visual tab width in number of spaces
96+ /// - indentOption: The behavior to use when the tab key is pressed. Defaults to 4 spaces.
97+ /// - lineHeight: The line height multiplier (e.g. `1.2`)
98+ /// - wrapLines: Whether lines wrap to the width of the editor
99+ /// - editorOverscroll: The distance to overscroll the editor by.
100+ /// - cursorPositions: The cursor's position in the editor, measured in `(lineNum, columnNum)`
101+ /// - useThemeBackground: Determines whether the editor uses the theme's background color, or a transparent
102+ /// background color
103+ /// - highlightProvider: A class you provide to perform syntax highlighting. Leave this as `nil` to use the
104+ /// built-in `TreeSitterClient` highlighter.
105+ /// - contentInsets: Insets to use to offset the content in the enclosing scroll view. Leave as `nil` to let the
106+ /// scroll view automatically adjust content insets.
107+ /// - isEditable: A Boolean value that controls whether the text view allows the user to edit text.
108+ /// - isSelectable: A Boolean value that controls whether the text view allows the user to select text. If this
109+ /// value is true, and `isEditable` is false, the editor is selectable but not editable.
110+ /// - letterSpacing: The amount of space to use between letters, as a percent. Eg: `1.0` = no space, `1.5` = 1/2 a
111+ /// character's width between characters, etc. Defaults to `1.0`
112+ /// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
113+ /// See `BracketPairHighlight` for more information. Defaults to `nil`
114+ /// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
115+ /// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
116+ public init (
117+ _ text: NSTextStorage ,
118+ language: CodeLanguage ,
119+ theme: EditorTheme ,
120+ font: NSFont ,
121+ tabWidth: Int ,
122+ indentOption: IndentOption = . spaces( count: 4 ) ,
123+ lineHeight: Double ,
124+ wrapLines: Bool ,
125+ editorOverscroll: CGFloat = 0 ,
126+ cursorPositions: Binding < [ CursorPosition ] > ,
127+ useThemeBackground: Bool = true ,
128+ highlightProvider: HighlightProviding ? = nil ,
129+ contentInsets: NSEdgeInsets ? = nil ,
130+ isEditable: Bool = true ,
131+ isSelectable: Bool = true ,
132+ letterSpacing: Double = 1.0 ,
133+ bracketPairHighlight: BracketPairHighlight ? = nil ,
134+ undoManager: CEUndoManager ? = nil ,
135+ coordinators: [ any TextViewCoordinator ] = [ ]
136+ ) {
137+ self . text = . storage( text)
62138 self . language = language
63139 self . theme = theme
64140 self . useThemeBackground = useThemeBackground
@@ -68,7 +144,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
68144 self . lineHeight = lineHeight
69145 self . wrapLines = wrapLines
70146 self . editorOverscroll = editorOverscroll
71- self . _cursorPositions = cursorPositions
147+ self . cursorPositions = cursorPositions
72148 self . highlightProvider = highlightProvider
73149 self . contentInsets = contentInsets
74150 self . isEditable = isEditable
@@ -79,7 +155,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
79155 self . coordinators = coordinators
80156 }
81157
82- @ Binding private var text : String
158+ package var text : TextAPI
83159 private var language : CodeLanguage
84160 private var theme : EditorTheme
85161 private var font : NSFont
@@ -88,7 +164,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
88164 private var lineHeight : Double
89165 private var wrapLines : Bool
90166 private var editorOverscroll : CGFloat
91- @ Binding private var cursorPositions : [ CursorPosition ]
167+ package var cursorPositions : Binding < [ CursorPosition ] >
92168 private var useThemeBackground : Bool
93169 private var highlightProvider : HighlightProviding ?
94170 private var contentInsets : NSEdgeInsets ?
@@ -97,21 +173,21 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
97173 private var letterSpacing : Double
98174 private var bracketPairHighlight : BracketPairHighlight ?
99175 private var undoManager : CEUndoManager ?
100- private var coordinators : [ any TextViewCoordinator ]
176+ package var coordinators : [ any TextViewCoordinator ]
101177
102178 public typealias NSViewControllerType = TextViewController
103179
104180 public func makeNSViewController( context: Context ) -> TextViewController {
105181 let controller = TextViewController (
106- string: text ,
182+ string: " " ,
107183 language: language,
108184 font: font,
109185 theme: theme,
110186 tabWidth: tabWidth,
111187 indentOption: indentOption,
112188 lineHeight: lineHeight,
113189 wrapLines: wrapLines,
114- cursorPositions: cursorPositions,
190+ cursorPositions: cursorPositions. wrappedValue ,
115191 editorOverscroll: editorOverscroll,
116192 useThemeBackground: useThemeBackground,
117193 highlightProvider: highlightProvider,
@@ -122,11 +198,17 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
122198 bracketPairHighlight: bracketPairHighlight,
123199 undoManager: undoManager
124200 )
201+ switch text {
202+ case . binding( let binding) :
203+ controller. textView. setText ( binding. wrappedValue)
204+ case . storage( let textStorage) :
205+ controller. textView. setTextStorage ( textStorage)
206+ }
125207 if controller. textView == nil {
126208 controller. loadView ( )
127209 }
128210 if !cursorPositions. isEmpty {
129- controller. setCursorPositions ( cursorPositions)
211+ controller. setCursorPositions ( cursorPositions. wrappedValue )
130212 }
131213
132214 context. coordinator. controller = controller
@@ -144,7 +226,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
144226 if !context. coordinator. isUpdateFromTextView {
145227 // Prevent infinite loop of update notifications
146228 context. coordinator. isUpdatingFromRepresentable = true
147- controller. setCursorPositions ( cursorPositions)
229+ controller. setCursorPositions ( cursorPositions. wrappedValue )
148230 context. coordinator. isUpdatingFromRepresentable = false
149231 } else {
150232 context. coordinator. isUpdateFromTextView = false
@@ -216,67 +298,6 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
216298 controller. letterSpacing == letterSpacing &&
217299 controller. bracketPairHighlight == bracketPairHighlight
218300 }
219-
220- @MainActor
221- public class Coordinator : NSObject {
222- var parent : CodeEditSourceEditor
223- weak var controller : TextViewController ?
224- var isUpdatingFromRepresentable : Bool = false
225- var isUpdateFromTextView : Bool = false
226-
227- init ( parent: CodeEditSourceEditor ) {
228- self . parent = parent
229- super. init ( )
230-
231- NotificationCenter . default. addObserver (
232- self ,
233- selector: #selector( textViewDidChangeText ( _: ) ) ,
234- name: TextView . textDidChangeNotification,
235- object: nil
236- )
237-
238- NotificationCenter . default. addObserver (
239- self ,
240- selector: #selector( textControllerCursorsDidUpdate ( _: ) ) ,
241- name: TextViewController . cursorPositionUpdatedNotification,
242- object: nil
243- )
244- }
245-
246- @objc func textViewDidChangeText( _ notification: Notification ) {
247- guard let textView = notification. object as? TextView ,
248- let controller,
249- controller. textView === textView else {
250- return
251- }
252- parent. text = textView. string
253- parent. coordinators. forEach {
254- $0. textViewDidChangeText ( controller: controller)
255- }
256- }
257-
258- @objc func textControllerCursorsDidUpdate( _ notification: Notification ) {
259- guard !isUpdatingFromRepresentable else { return }
260- self . isUpdateFromTextView = true
261- self . parent. _cursorPositions. wrappedValue = self . controller? . cursorPositions ?? [ ]
262- if self . controller != nil {
263- self . parent. coordinators. forEach {
264- $0. textViewDidChangeSelection (
265- controller: self . controller!,
266- newPositions: self . controller!. cursorPositions
267- )
268- }
269- }
270- }
271-
272- deinit {
273- parent. coordinators. forEach {
274- $0. destroy ( )
275- }
276- parent. coordinators. removeAll ( )
277- NotificationCenter . default. removeObserver ( self )
278- }
279- }
280301}
281302
282303// swiftlint:disable:next line_length
0 commit comments