Skip to content

Commit 662a231

Browse files
committed
Add Find Toggle, Find Text to State
1 parent 013860a commit 662a231

File tree

6 files changed

+158
-30
lines changed

6 files changed

+158
-30
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ struct StatusBar: View {
105105
Text(getLabel(state.cursorPositions))
106106
}
107107
.foregroundStyle(.secondary)
108+
109+
Divider()
110+
.frame(height: 12)
111+
112+
Button {
113+
state.findPanelVisible.toggle()
114+
} label: {
115+
Text(state.findPanelVisible ? "Hide" : "Show") + Text(" Find")
116+
}
117+
.buttonStyle(.borderless)
118+
.foregroundStyle(.secondary)
119+
108120
Divider()
109121
.frame(height: 12)
110122
LanguagePicker(language: $language)
@@ -133,12 +145,37 @@ struct StatusBar: View {
133145
}
134146
}
135147

148+
var formatter: NumberFormatter {
149+
let formatter = NumberFormatter()
150+
formatter.numberStyle = .decimal
151+
formatter.maximumFractionDigits = 2
152+
formatter.minimumFractionDigits = 0
153+
formatter.allowsFloats = true
154+
return formatter
155+
}
156+
136157
@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("}")
158+
HStack(spacing: 0) {
159+
Text("{")
160+
TextField(
161+
"",
162+
value: Binding(get: { Double(state.scrollPosition?.x ?? 0.0) }, set: { state.scrollPosition?.x = $0 }),
163+
formatter: formatter
164+
)
165+
.textFieldStyle(.plain)
166+
.labelsHidden()
167+
.fixedSize()
168+
Text(",")
169+
TextField(
170+
"",
171+
value: Binding(get: { Double(state.scrollPosition?.y ?? 0.0) }, set: { state.scrollPosition?.y = $0 }),
172+
formatter: formatter
173+
)
174+
.textFieldStyle(.plain)
175+
.labelsHidden()
176+
.fixedSize()
177+
Text("}")
178+
}
142179
}
143180

144181
private func detectLanguage(fileURL: URL?) -> CodeLanguage? {

Sources/CodeEditSourceEditor/Find/FindViewController+Toggle.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ extension FindViewController {
4444

4545
viewModel.isFocused = true
4646
findPanel.addEventMonitor()
47+
48+
NotificationCenter.default.post(
49+
name: FindPanelViewModel.findPanelDidToggleNotification,
50+
object: viewModel.target
51+
)
4752
}
4853

4954
/// Hide the find panel
@@ -70,6 +75,11 @@ extension FindViewController {
7075
if let target = viewModel.target {
7176
_ = target.findPanelTargetView.window?.makeFirstResponder(target.findPanelTargetView)
7277
}
78+
79+
NotificationCenter.default.post(
80+
name: FindPanelViewModel.findPanelDidToggleNotification,
81+
object: viewModel.target
82+
)
7383
}
7484

7585
/// Performs an animation with a completion handler, conditionally animating the changes.

Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import Combine
1010
import CodeEditTextView
1111

1212
class FindPanelViewModel: ObservableObject {
13+
static let findPanelTextDidChangeNotification = Notification.Name("FindPanelViewModel.findPanelTextDidChange")
14+
static let findPanelDidToggleNotification = Notification.Name("FindPanelViewModel.findPanelDidToggle")
15+
1316
weak var target: FindPanelTarget?
1417
var dismiss: (() -> Void)?
1518

@@ -99,5 +102,7 @@ class FindPanelViewModel: ObservableObject {
99102
// Clear existing emphases before performing new find
100103
target?.textView.emphasisManager?.removeEmphases(for: EmphasisGroup.find)
101104
find()
105+
106+
NotificationCenter.default.post(name: Self.findPanelTextDidChangeNotification, object: target)
102107
}
103108
}

Sources/CodeEditSourceEditor/SourceEditor/SourceEditor+Coordinator.swift

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension SourceEditor {
3232

3333
func setController(_ controller: TextViewController) {
3434
self.controller = controller
35-
// swiftlint:disable:this notification_center_detachment
35+
// swiftlint:disable:next notification_center_detachment
3636
NotificationCenter.default.removeObserver(self)
3737

3838
NotificationCenter.default.addObserver(
@@ -49,7 +49,7 @@ extension SourceEditor {
4949
object: controller
5050
)
5151

52-
// Needs to be put on the main runloop or SwiftUI gets mad
52+
// Needs to be put on the main runloop or SwiftUI gets mad about updating state during view updates.
5353
NotificationCenter.default
5454
.publisher(
5555
for: TextViewController.scrollPositionDidUpdateNotification,
@@ -60,6 +60,28 @@ extension SourceEditor {
6060
self?.textControllerScrollDidChange(notification)
6161
}
6262
.store(in: &cancellables)
63+
64+
NotificationCenter.default
65+
.publisher(
66+
for: FindPanelViewModel.findPanelTextDidChangeNotification,
67+
object: controller
68+
)
69+
.receive(on: RunLoop.main)
70+
.sink { [weak self] notification in
71+
self?.textControllerFindTextDidChange(notification)
72+
}
73+
.store(in: &cancellables)
74+
75+
NotificationCenter.default
76+
.publisher(
77+
for: FindPanelViewModel.findPanelDidToggleNotification,
78+
object: controller
79+
)
80+
.receive(on: RunLoop.main)
81+
.sink { [weak self] notification in
82+
self?.textControllerFindDidToggle(notification)
83+
}
84+
.store(in: &cancellables)
6385
}
6486

6587
func updateHighlightProviders(_ highlightProviders: [any HighlightProviding]?) {
@@ -91,7 +113,26 @@ extension SourceEditor {
91113
guard let controller = notification.object as? TextViewController else {
92114
return
93115
}
94-
updateState { $0.scrollPosition = controller.scrollView.contentView.bounds.origin }
116+
let currentPosition = controller.scrollView.contentView.bounds.origin
117+
if editorState.scrollPosition != currentPosition {
118+
updateState { $0.scrollPosition = currentPosition }
119+
}
120+
}
121+
122+
func textControllerFindTextDidChange(_ notification: Notification) {
123+
guard let controller = notification.object as? TextViewController,
124+
let findModel = controller.findViewController?.viewModel else {
125+
return
126+
}
127+
updateState { $0.findText = findModel.findText }
128+
}
129+
130+
func textControllerFindDidToggle(_ notification: Notification) {
131+
guard let controller = notification.object as? TextViewController,
132+
let findModel = controller.findViewController?.viewModel else {
133+
return
134+
}
135+
updateState { $0.findPanelVisible = findModel.isShowingFindPanel }
95136
}
96137

97138
private func updateState(_ modifyCallback: (inout SourceEditorState) -> Void) {

Sources/CodeEditSourceEditor/SourceEditor/SourceEditor.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,6 @@ 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-
3213
/// A SwiftUI View that provides source editing functionality.
3314
public struct SourceEditor: NSViewControllerRepresentable {
3415
enum TextAPI {
@@ -137,14 +118,41 @@ public struct SourceEditor: NSViewControllerRepresentable {
137118

138119
public func updateNSViewController(_ controller: TextViewController, context: Context) {
139120
context.coordinator.updateHighlightProviders(highlightProviders)
121+
print(
122+
context.coordinator.isUpdateFromTextView,
123+
state.findPanelVisible,
124+
controller.findViewController?.viewModel.isShowingFindPanel ?? false
125+
)
140126

127+
// Prevent infinite loop of update notifications
141128
if context.coordinator.isUpdateFromTextView {
142129
context.coordinator.isUpdateFromTextView = false
143130
} else {
144-
// Prevent infinite loop of update notifications
145131
context.coordinator.isUpdatingFromRepresentable = true
146-
// controller.setCursorPositions(state.cursorPositions)
147-
// TODO: Set scroll position, find text, etc.
132+
controller.setCursorPositions(state.cursorPositions)
133+
134+
if let scrollPosition = state.scrollPosition {
135+
controller.scrollView.scroll(controller.scrollView.contentView, to: scrollPosition)
136+
controller.scrollView.reflectScrolledClipView(controller.scrollView.contentView)
137+
controller.gutterView.needsDisplay = true
138+
}
139+
140+
if let findText = state.findText {
141+
controller.findViewController?.viewModel.findText = findText
142+
}
143+
144+
if let findController = controller.findViewController,
145+
findController.viewModel.isShowingFindPanel != state.findPanelVisible {
146+
// Needs to be on the next runloop, not many great ways to do this besides a dispatch...
147+
DispatchQueue.main.async {
148+
if state.findPanelVisible {
149+
findController.showFindPanel()
150+
} else {
151+
findController.hideFindPanel()
152+
}
153+
}
154+
}
155+
148156
context.coordinator.isUpdatingFromRepresentable = false
149157
}
150158

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// SourceEditorState.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 6/19/25.
6+
//
7+
8+
import AppKit
9+
10+
public struct SourceEditorState: Equatable, Hashable, Sendable, Codable {
11+
public var cursorPositions: [CursorPosition] = []
12+
public var scrollPosition: CGPoint?
13+
public var findText: String?
14+
public var findPanelVisible: Bool = false
15+
16+
public init(
17+
cursorPositions: [CursorPosition],
18+
scrollPosition: CGPoint? = nil,
19+
findText: String? = nil,
20+
findPanelVisible: Bool = false
21+
) {
22+
self.cursorPositions = cursorPositions
23+
self.scrollPosition = scrollPosition
24+
self.findText = findText
25+
self.findPanelVisible = findPanelVisible
26+
}
27+
}

0 commit comments

Comments
 (0)