Skip to content

Commit 013860a

Browse files
committed
Use Generic State Type, Track Scroll Position
1 parent d13e645 commit 013860a

File tree

8 files changed

+107
-51
lines changed

8 files changed

+107
-51
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ struct ContentView: View {
1919

2020
@State private var language: CodeLanguage = .default
2121
@State private var theme: EditorTheme = .light
22-
@State private var cursorPositions: [CursorPosition] = [.init(line: 1, column: 1)]
22+
@State private var editorState = SourceEditorState(
23+
cursorPositions: [CursorPosition(line: 1, column: 1)]
24+
)
2325

2426
@State private var font: NSFont = NSFont.monospacedSystemFont(ofSize: 12, weight: .medium)
2527
@AppStorage("wrapLines") private var wrapLines: Bool = true
@@ -67,15 +69,15 @@ struct ContentView: View {
6769
warningCharacters: warningCharacters
6870
)
6971
),
70-
cursorPositions: $cursorPositions
72+
state: $editorState
7173
)
7274
.overlay(alignment: .bottom) {
7375
StatusBar(
7476
fileURL: fileURL,
7577
document: $document,
7678
wrapLines: $wrapLines,
7779
useSystemCursor: $useSystemCursor,
78-
cursorPositions: $cursorPositions,
80+
state: $editorState,
7981
isInLongParse: $isInLongParse,
8082
language: $language,
8183
theme: $theme,

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct StatusBar: View {
1818
@Binding var document: CodeEditSourceEditorExampleDocument
1919
@Binding var wrapLines: Bool
2020
@Binding var useSystemCursor: Bool
21-
@Binding var cursorPositions: [CursorPosition]
21+
@Binding var state: SourceEditorState
2222
@Binding var isInLongParse: Bool
2323
@Binding var language: CodeLanguage
2424
@Binding var theme: EditorTheme
@@ -100,9 +100,9 @@ struct StatusBar: View {
100100
.controlSize(.small)
101101
Text("Parsing Document")
102102
}
103-
} else {
104-
Text(getLabel(cursorPositions))
105103
}
104+
scrollPosition
105+
Text(getLabel(state.cursorPositions))
106106
}
107107
.foregroundStyle(.secondary)
108108
Divider()
@@ -133,6 +133,14 @@ struct StatusBar: View {
133133
}
134134
}
135135

136+
@ViewBuilder private var scrollPosition: some View {
137+
Text("{")
138+
+ Text(Double(state.scrollPosition?.x ?? -1), format: .number.precision(.fractionLength(1)))
139+
+ Text(",")
140+
+ Text(Double(state.scrollPosition?.y ?? -1), format: .number.precision(.fractionLength(1)))
141+
+ Text("}")
142+
}
143+
136144
private func detectLanguage(fileURL: URL?) -> CodeLanguage? {
137145
guard let fileURL else { return nil }
138146
return CodeLanguage.detectLanguageFrom(

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ let package = Package(
1616
dependencies: [
1717
// A fast, efficient, text view for code.
1818
.package(
19-
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20-
from: "0.11.2"
19+
path: "../CodeEditTextView"
20+
// url: "https://github.com/CodeEditApp/CodeEditTextView.git",
21+
// from: "0.11.2"
2122
),
2223
// tree-sitter languages
2324
.package(

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ extension TextViewController {
103103
guard let clipView = notification.object as? NSClipView else { return }
104104
self?.gutterView.needsDisplay = true
105105
self?.minimapXConstraint?.constant = clipView.bounds.origin.x
106+
NotificationCenter.default.post(name: Self.scrollPositionDidUpdateNotification, object: self)
106107
}
107108
}
108109

@@ -115,6 +116,7 @@ extension TextViewController {
115116
self?.gutterView.needsDisplay = true
116117
self?.emphasisManager?.removeEmphases(for: EmphasisGroup.brackets)
117118
self?.updateTextInsets()
119+
NotificationCenter.default.post(name: Self.scrollPositionDidUpdateNotification, object: self)
118120
}
119121
}
120122

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import TextFormation
1919
public class TextViewController: NSViewController {
2020
// swiftlint:disable:next line_length
2121
public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification")
22+
// swiftlint:disable:next line_length
23+
public static let scrollPositionDidUpdateNotification: Notification.Name = .init("TextViewController.scrollPositionDidUpdateNotification")
2224

2325
// MARK: - Views and Child VCs
2426

Sources/CodeEditSourceEditor/SourceEditor/SourceEditor+Coordinator.swift

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,61 @@
55
// Created by Khan Winter on 5/20/24.
66
//
77

8-
import Foundation
8+
import AppKit
99
import SwiftUI
10+
import Combine
1011
import CodeEditTextView
1112

1213
extension SourceEditor {
1314
@MainActor
1415
public class Coordinator: NSObject {
15-
weak var controller: TextViewController?
16+
private weak var controller: TextViewController?
1617
var isUpdatingFromRepresentable: Bool = false
1718
var isUpdateFromTextView: Bool = false
1819
var text: TextAPI
19-
@Binding var cursorPositions: [CursorPosition]
20+
@Binding var editorState: SourceEditorState
2021

2122
private(set) var highlightProviders: [any HighlightProviding]
2223

23-
init(text: TextAPI, cursorPositions: Binding<[CursorPosition]>, highlightProviders: [any HighlightProviding]?) {
24+
private var cancellables: Set<AnyCancellable> = []
25+
26+
init(text: TextAPI, editorState: Binding<SourceEditorState>, highlightProviders: [any HighlightProviding]?) {
2427
self.text = text
25-
self._cursorPositions = cursorPositions
28+
self._editorState = editorState
2629
self.highlightProviders = highlightProviders ?? [TreeSitterClient()]
2730
super.init()
31+
}
32+
33+
func setController(_ controller: TextViewController) {
34+
self.controller = controller
35+
// swiftlint:disable:this notification_center_detachment
36+
NotificationCenter.default.removeObserver(self)
2837

2938
NotificationCenter.default.addObserver(
3039
self,
3140
selector: #selector(textViewDidChangeText(_:)),
3241
name: TextView.textDidChangeNotification,
33-
object: nil
42+
object: controller.textView
3443
)
3544

3645
NotificationCenter.default.addObserver(
3746
self,
3847
selector: #selector(textControllerCursorsDidUpdate(_:)),
3948
name: TextViewController.cursorPositionUpdatedNotification,
40-
object: nil
49+
object: controller
4150
)
51+
52+
// Needs to be put on the main runloop or SwiftUI gets mad
53+
NotificationCenter.default
54+
.publisher(
55+
for: TextViewController.scrollPositionDidUpdateNotification,
56+
object: controller
57+
)
58+
.receive(on: RunLoop.main)
59+
.sink { [weak self] notification in
60+
self?.textControllerScrollDidChange(notification)
61+
}
62+
.store(in: &cancellables)
4263
}
4364

4465
func updateHighlightProviders(_ highlightProviders: [any HighlightProviding]?) {
@@ -50,24 +71,33 @@ extension SourceEditor {
5071
}
5172

5273
@objc func textViewDidChangeText(_ notification: Notification) {
53-
guard let textView = notification.object as? TextView,
54-
let controller,
55-
controller.textView === textView else {
74+
guard let textView = notification.object as? TextView else {
5675
return
5776
}
77+
// A plain string binding is one-way (from this view, up the hierarchy) so it's not in the state binding
5878
if case .binding(let binding) = text {
5979
binding.wrappedValue = textView.string
6080
}
6181
}
6282

6383
@objc func textControllerCursorsDidUpdate(_ notification: Notification) {
64-
guard let notificationController = notification.object as? TextViewController,
65-
notificationController === controller else {
84+
guard let controller = notification.object as? TextViewController else {
6685
return
6786
}
87+
updateState { $0.cursorPositions = controller.cursorPositions }
88+
}
89+
90+
func textControllerScrollDidChange(_ notification: Notification) {
91+
guard let controller = notification.object as? TextViewController else {
92+
return
93+
}
94+
updateState { $0.scrollPosition = controller.scrollView.contentView.bounds.origin }
95+
}
96+
97+
private func updateState(_ modifyCallback: (inout SourceEditorState) -> Void) {
6898
guard !isUpdatingFromRepresentable else { return }
6999
self.isUpdateFromTextView = true
70-
cursorPositions = notificationController.cursorPositions
100+
modifyCallback(&editorState)
71101
}
72102

73103
deinit {

Sources/CodeEditSourceEditor/SourceEditor/SourceEditor.swift

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,28 @@ import SwiftUI
1010
import CodeEditTextView
1111
import CodeEditLanguages
1212

13+
public struct SourceEditorState: Equatable, Hashable, Sendable, Codable {
14+
public var cursorPositions: [CursorPosition] = []
15+
public var scrollPosition: CGPoint?
16+
public var findText: String?
17+
public var isShowingFindResults: Bool = false
18+
19+
public init(
20+
cursorPositions: [CursorPosition],
21+
scrollPosition: CGPoint? = nil,
22+
findText: String? = nil,
23+
isShowingFindResults: Bool = false
24+
) {
25+
self.cursorPositions = cursorPositions
26+
self.scrollPosition = scrollPosition
27+
self.findText = findText
28+
self.isShowingFindResults = isShowingFindResults
29+
}
30+
}
31+
1332
/// A SwiftUI View that provides source editing functionality.
1433
public struct SourceEditor: NSViewControllerRepresentable {
15-
package enum TextAPI {
34+
enum TextAPI {
1635
case binding(Binding<String>)
1736
case storage(NSTextStorage)
1837
}
@@ -32,15 +51,15 @@ public struct SourceEditor: NSViewControllerRepresentable {
3251
_ text: Binding<String>,
3352
language: CodeLanguage,
3453
configuration: SourceEditorConfiguration,
35-
cursorPositions: Binding<[CursorPosition]>,
54+
state: Binding<SourceEditorState>,
3655
highlightProviders: [any HighlightProviding]? = nil,
3756
undoManager: CEUndoManager? = nil,
3857
coordinators: [any TextViewCoordinator] = []
3958
) {
4059
self.text = .binding(text)
4160
self.language = language
4261
self.configuration = configuration
43-
self.cursorPositions = cursorPositions
62+
self._state = state
4463
self.highlightProviders = highlightProviders
4564
self.undoManager = undoManager
4665
self.coordinators = coordinators
@@ -61,27 +80,27 @@ public struct SourceEditor: NSViewControllerRepresentable {
6180
_ text: NSTextStorage,
6281
language: CodeLanguage,
6382
configuration: SourceEditorConfiguration,
64-
cursorPositions: Binding<[CursorPosition]>,
83+
state: Binding<SourceEditorState>,
6584
highlightProviders: [any HighlightProviding]? = nil,
6685
undoManager: CEUndoManager? = nil,
6786
coordinators: [any TextViewCoordinator] = []
6887
) {
6988
self.text = .storage(text)
7089
self.language = language
7190
self.configuration = configuration
72-
self.cursorPositions = cursorPositions
91+
self._state = state
7392
self.highlightProviders = highlightProviders
7493
self.undoManager = undoManager
7594
self.coordinators = coordinators
7695
}
7796

78-
package var text: TextAPI
79-
private var language: CodeLanguage
80-
private var configuration: SourceEditorConfiguration
81-
package var cursorPositions: Binding<[CursorPosition]>
82-
private var highlightProviders: [any HighlightProviding]?
83-
private var undoManager: CEUndoManager?
84-
package var coordinators: [any TextViewCoordinator]
97+
var text: TextAPI
98+
var language: CodeLanguage
99+
var configuration: SourceEditorConfiguration
100+
@Binding var state: SourceEditorState
101+
var highlightProviders: [any HighlightProviding]?
102+
var undoManager: CEUndoManager?
103+
var coordinators: [any TextViewCoordinator]
85104

86105
public typealias NSViewControllerType = TextViewController
87106

@@ -90,7 +109,7 @@ public struct SourceEditor: NSViewControllerRepresentable {
90109
string: "",
91110
language: language,
92111
configuration: configuration,
93-
cursorPositions: cursorPositions.wrappedValue,
112+
cursorPositions: state.cursorPositions,
94113
highlightProviders: context.coordinator.highlightProviders,
95114
undoManager: undoManager,
96115
coordinators: coordinators
@@ -104,28 +123,29 @@ public struct SourceEditor: NSViewControllerRepresentable {
104123
if controller.textView == nil {
105124
controller.loadView()
106125
}
107-
if !cursorPositions.isEmpty {
108-
controller.setCursorPositions(cursorPositions.wrappedValue)
126+
if !state.cursorPositions.isEmpty {
127+
controller.setCursorPositions(state.cursorPositions)
109128
}
110129

111-
context.coordinator.controller = controller
130+
context.coordinator.setController(controller)
112131
return controller
113132
}
114133

115134
public func makeCoordinator() -> Coordinator {
116-
Coordinator(text: text, cursorPositions: cursorPositions, highlightProviders: highlightProviders)
135+
Coordinator(text: text, editorState: $state, highlightProviders: highlightProviders)
117136
}
118137

119138
public func updateNSViewController(_ controller: TextViewController, context: Context) {
120139
context.coordinator.updateHighlightProviders(highlightProviders)
121140

122-
if !context.coordinator.isUpdateFromTextView {
141+
if context.coordinator.isUpdateFromTextView {
142+
context.coordinator.isUpdateFromTextView = false
143+
} else {
123144
// Prevent infinite loop of update notifications
124145
context.coordinator.isUpdatingFromRepresentable = true
125-
controller.setCursorPositions(cursorPositions.wrappedValue)
146+
// controller.setCursorPositions(state.cursorPositions)
147+
// TODO: Set scroll position, find text, etc.
126148
context.coordinator.isUpdatingFromRepresentable = false
127-
} else {
128-
context.coordinator.isUpdateFromTextView = false
129149
}
130150

131151
// Set this no matter what to avoid having to compare object pointers.

0 commit comments

Comments
 (0)