Skip to content

Commit dc5e6d8

Browse files
thecoolwinteraustincondiff
authored andcommitted
Add Search Container, Search Bar, Show/Hide Commands, Animations
1 parent 1488a86 commit dc5e6d8

File tree

9 files changed

+425
-182
lines changed

9 files changed

+425
-182
lines changed

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

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

Package.resolved

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

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ import AppKit
1111
extension TextViewController {
1212
// swiftlint:disable:next function_body_length
1313
override public func loadView() {
14-
let stackView = NSStackView()
15-
stackView.orientation = .vertical
16-
stackView.spacing = 10
17-
stackView.alignment = .leading
18-
stackView.translatesAutoresizingMaskIntoConstraints = false
14+
super.loadView()
1915

2016
scrollView = NSScrollView()
2117
textView.postsFrameChangedNotifications = true
@@ -39,46 +35,12 @@ extension TextViewController {
3935
for: .horizontal
4036
)
4137

42-
searchField = NSTextField()
43-
searchField.placeholderString = "Search..."
44-
searchField.controlSize = .regular // TODO: a
45-
searchField.focusRingType = .none
46-
searchField.bezelStyle = .roundedBezel
47-
searchField.drawsBackground = true
48-
searchField.translatesAutoresizingMaskIntoConstraints = false
49-
searchField.action = #selector(onSubmit)
50-
searchField.target = self
51-
52-
prevButton = NSButton(title: "◀︎", target: self, action: #selector(prevButtonClicked))
53-
prevButton.bezelStyle = .texturedRounded
54-
prevButton.controlSize = .small
55-
prevButton.translatesAutoresizingMaskIntoConstraints = false
56-
57-
nextButton = NSButton(title: "▶︎", target: self, action: #selector(nextButtonClicked))
58-
nextButton.bezelStyle = .texturedRounded
59-
nextButton.controlSize = .small
60-
nextButton.translatesAutoresizingMaskIntoConstraints = false
61-
62-
stackview = NSStackView()
63-
stackview.orientation = .horizontal
64-
stackview.spacing = 8
65-
stackview.edgeInsets = NSEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
66-
stackview.translatesAutoresizingMaskIntoConstraints = false
67-
68-
stackview.addView(searchField, in: .leading)
69-
stackview.addView(prevButton, in: .trailing)
70-
stackview.addView(nextButton, in: .trailing)
38+
let searchController = SearchViewController(target: self, childView: scrollView)
39+
addChild(searchController)
40+
self.view.addSubview(searchController.view)
41+
searchController.view.viewDidMoveToSuperview()
42+
self.searchController = searchController
7143

72-
NotificationCenter.default.addObserver(
73-
self,
74-
selector: #selector(searchFieldUpdated(_:)),
75-
name: NSControl.textDidChangeNotification,
76-
object: searchField
77-
)
78-
79-
stackView.addArrangedSubview(stackview)
80-
stackView.addArrangedSubview(scrollView)
81-
self.view = stackView
8244
if let _undoManager {
8345
textView.setUndoManager(_undoManager)
8446
}
@@ -90,15 +52,10 @@ extension TextViewController {
9052
setUpTextFormation()
9153

9254
NSLayoutConstraint.activate([
93-
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
94-
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
95-
stackView.topAnchor.constraint(equalTo: view.topAnchor),
96-
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
97-
98-
// scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
99-
// scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
100-
// scrollView.topAnchor.constraint(equalTo: view.topAnchor),
101-
// scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
55+
searchController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
56+
searchController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
57+
searchController.view.topAnchor.constraint(equalTo: view.topAnchor),
58+
searchController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
10259
])
10360

10461
if !cursorPositions.isEmpty {
@@ -162,18 +119,64 @@ extension TextViewController {
162119
if let localEventMonitor = self.localEvenMonitor {
163120
NSEvent.removeMonitor(localEventMonitor)
164121
}
165-
self.localEvenMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
122+
self.localEvenMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event -> NSEvent? in
166123
guard self?.view.window?.firstResponder == self?.textView else { return event }
124+
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
125+
126+
switch (modifierFlags, event.charactersIgnoringModifiers?.lowercased()) {
127+
case (.command, "/"):
128+
self?.handleCommandSlash()
129+
return nil
130+
case (.command, "f"):
131+
_ = self?.textView.resignFirstResponder()
132+
self?.searchController?.showSearchBar()
133+
return nil
134+
case ([], "\u{1b}"): // Escape key
135+
self?.searchController?.hideSearchBar()
136+
_ = self?.textView.becomeFirstResponder()
137+
self?.textView.selectionManager.setSelectedRanges(
138+
self?.textView.selectionManager.textSelections.map { $0.range } ?? []
139+
)
140+
return nil
141+
default:
142+
return event
143+
}
144+
}
145+
}
146+
func handleCommand(event: NSEvent, modifierFlags: UInt) -> NSEvent? {
147+
let commandKey = NSEvent.ModifierFlags.command.rawValue
148+
149+
switch (modifierFlags, event.charactersIgnoringModifiers) {
150+
case (commandKey, "/"):
151+
handleCommandSlash()
152+
return nil
153+
case (commandKey, "["):
154+
handleIndent(inwards: true)
155+
return nil
156+
case (commandKey, "]"):
157+
handleIndent()
158+
return nil
159+
case (_, _):
160+
return event
161+
}
162+
}
167163

168-
let tabKey: UInt16 = 0x30
169-
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask).rawValue
164+
/// Handles the tab key event.
165+
/// If the Shift key is pressed, it handles unindenting. If no modifier key is pressed, it checks if multiple lines
166+
/// are highlighted and handles indenting accordingly.
167+
///
168+
/// - Returns: The original event if it should be passed on, or `nil` to indicate handling within the method.
169+
func handleTab(event: NSEvent, modifierFalgs: UInt) -> NSEvent? {
170+
let shiftKey = NSEvent.ModifierFlags.shift.rawValue
170171

171-
if event.keyCode == tabKey {
172-
return self?.handleTab(event: event, modifierFalgs: modifierFlags)
173-
} else {
174-
return self?.handleCommand(event: event, modifierFlags: modifierFlags)
175-
}
172+
if modifierFalgs == shiftKey {
173+
handleIndent(inwards: true)
174+
} else {
175+
// Only allow tab to work if multiple lines are selected
176+
guard multipleLinesHighlighted() else { return event }
177+
handleIndent()
176178
}
179+
return nil
177180
}
178181
func handleCommand(event: NSEvent, modifierFlags: UInt) -> NSEvent? {
179182
let commandKey = NSEvent.ModifierFlags.command.rawValue

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

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

23+
weak var searchController: SearchViewController?
24+
2325
var scrollView: NSScrollView!
2426

2527
// SEARCH
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// SearchBar.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 3/10/25.
6+
//
7+
8+
import AppKit
9+
10+
protocol SearchBarDelegate: AnyObject {
11+
func searchBarOnSubmit()
12+
func searchBarOnCancel()
13+
func searchBarDidUpdate(_ searchText: String)
14+
func searchBarPrevButtonClicked()
15+
func searchBarNextButtonClicked()
16+
}
17+
18+
/// A control for searching a document and navigating results.
19+
final class SearchBar: NSStackView {
20+
weak var searchDelegate: SearchBarDelegate?
21+
22+
var searchField: NSTextField!
23+
var prevButton: NSButton!
24+
var nextButton: NSButton!
25+
26+
init(delegate: SearchBarDelegate?) {
27+
super.init(frame: .zero)
28+
29+
self.searchDelegate = delegate
30+
31+
searchField = NSTextField()
32+
searchField.placeholderString = "Search..."
33+
searchField.controlSize = .regular // TODO: a
34+
searchField.focusRingType = .none
35+
searchField.bezelStyle = .roundedBezel
36+
searchField.drawsBackground = true
37+
searchField.translatesAutoresizingMaskIntoConstraints = false
38+
searchField.action = #selector(onSubmit)
39+
searchField.target = self
40+
41+
prevButton = NSButton(title: "◀︎", target: self, action: #selector(prevButtonClicked))
42+
prevButton.bezelStyle = .texturedRounded
43+
prevButton.controlSize = .small
44+
prevButton.translatesAutoresizingMaskIntoConstraints = false
45+
46+
nextButton = NSButton(title: "▶︎", target: self, action: #selector(nextButtonClicked))
47+
nextButton.bezelStyle = .texturedRounded
48+
nextButton.controlSize = .small
49+
nextButton.translatesAutoresizingMaskIntoConstraints = false
50+
51+
self.orientation = .horizontal
52+
self.spacing = 8
53+
self.edgeInsets = NSEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
54+
self.translatesAutoresizingMaskIntoConstraints = false
55+
56+
self.addView(searchField, in: .leading)
57+
self.addView(prevButton, in: .trailing)
58+
self.addView(nextButton, in: .trailing)
59+
60+
NotificationCenter.default.addObserver(
61+
self,
62+
selector: #selector(searchFieldUpdated(_:)),
63+
name: NSControl.textDidChangeNotification,
64+
object: searchField
65+
)
66+
}
67+
68+
required init?(coder: NSCoder) {
69+
fatalError("init(coder:) has not been implemented")
70+
}
71+
72+
/// Hide the search bar when escape is pressed
73+
override func cancelOperation(_ sender: Any?) {
74+
searchDelegate?.searchBarOnCancel()
75+
}
76+
77+
// MARK: - Delegate Messaging
78+
79+
@objc func searchFieldUpdated(_ notification: Notification) {
80+
guard let searchField = notification.object as? NSTextField else { return }
81+
searchDelegate?.searchBarDidUpdate(searchField.stringValue)
82+
}
83+
84+
@objc func onSubmit() {
85+
searchDelegate?.searchBarOnSubmit()
86+
}
87+
88+
@objc func prevButtonClicked() {
89+
searchDelegate?.searchBarPrevButtonClicked()
90+
}
91+
92+
@objc func nextButtonClicked() {
93+
searchDelegate?.searchBarNextButtonClicked()
94+
}
95+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// SearchTarget.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 3/10/25.
6+
//
7+
8+
// This dependency is not ideal, maybe we could make this another protocol that the emphasize API conforms to similar
9+
// to this one?
10+
import CodeEditTextView
11+
12+
protocol SearchTarget: AnyObject {
13+
var emphasizeAPI: EmphasizeAPI? { get }
14+
var text: String { get }
15+
16+
var cursorPositions: [CursorPosition] { get }
17+
func setCursorPositions(_ positions: [CursorPosition])
18+
func updateCursorPosition()
19+
}

0 commit comments

Comments
 (0)