Skip to content

Commit 1488a86

Browse files
tom-ludwigaustincondiff
authored andcommitted
Add search manager and search field
1 parent f28e759 commit 1488a86

File tree

4 files changed

+188
-8
lines changed

4 files changed

+188
-8
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
61621C612C74FB2200494A4A /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61621C602C74FB2200494A4A /* CodeEditSourceEditor */; };
11+
61CE772F2D19BF7D00908C57 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61CE772E2D19BF7D00908C57 /* CodeEditSourceEditor */; };
12+
61CE77322D19BFAA00908C57 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61CE77312D19BFAA00908C57 /* CodeEditSourceEditor */; };
1113
6C13652E2B8A7B94004A1D18 /* CodeEditSourceEditorExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C13652D2B8A7B94004A1D18 /* CodeEditSourceEditorExampleApp.swift */; };
1214
6C1365302B8A7B94004A1D18 /* CodeEditSourceEditorExampleDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C13652F2B8A7B94004A1D18 /* CodeEditSourceEditorExampleDocument.swift */; };
1315
6C1365322B8A7B94004A1D18 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1365312B8A7B94004A1D18 /* ContentView.swift */; };
@@ -41,6 +43,8 @@
4143
buildActionMask = 2147483647;
4244
files = (
4345
61621C612C74FB2200494A4A /* CodeEditSourceEditor in Frameworks */,
46+
61CE772F2D19BF7D00908C57 /* CodeEditSourceEditor in Frameworks */,
47+
61CE77322D19BFAA00908C57 /* CodeEditSourceEditor in Frameworks */,
4448
);
4549
runOnlyForDeploymentPostprocessing = 0;
4650
};
@@ -140,6 +144,8 @@
140144
name = CodeEditSourceEditorExample;
141145
packageProductDependencies = (
142146
61621C602C74FB2200494A4A /* CodeEditSourceEditor */,
147+
61CE772E2D19BF7D00908C57 /* CodeEditSourceEditor */,
148+
61CE77312D19BFAA00908C57 /* CodeEditSourceEditor */,
143149
);
144150
productName = CodeEditSourceEditorExample;
145151
productReference = 6C13652A2B8A7B94004A1D18 /* CodeEditSourceEditorExample.app */;
@@ -412,6 +418,14 @@
412418
isa = XCSwiftPackageProductDependency;
413419
productName = CodeEditSourceEditor;
414420
};
421+
61CE772E2D19BF7D00908C57 /* CodeEditSourceEditor */ = {
422+
isa = XCSwiftPackageProductDependency;
423+
productName = CodeEditSourceEditor;
424+
};
425+
61CE77312D19BFAA00908C57 /* CodeEditSourceEditor */ = {
426+
isa = XCSwiftPackageProductDependency;
427+
productName = CodeEditSourceEditor;
428+
};
415429
/* End XCSwiftPackageProductDependency section */
416430
};
417431
rootObject = 6C1365222B8A7B94004A1D18 /* Project object */;

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ 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
19+
1420
scrollView = NSScrollView()
1521
textView.postsFrameChangedNotifications = true
1622
textView.translatesAutoresizingMaskIntoConstraints = false
17-
1823
scrollView.translatesAutoresizingMaskIntoConstraints = false
1924
scrollView.contentView.postsFrameChangedNotifications = true
2025
scrollView.hasVerticalScroller = true
@@ -34,7 +39,46 @@ extension TextViewController {
3439
for: .horizontal
3540
)
3641

37-
self.view = scrollView
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)
71+
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
3882
if let _undoManager {
3983
textView.setUndoManager(_undoManager)
4084
}
@@ -46,10 +90,15 @@ extension TextViewController {
4690
setUpTextFormation()
4791

4892
NSLayoutConstraint.activate([
49-
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
50-
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
51-
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
52-
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
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)
53102
])
54103

55104
if !cursorPositions.isEmpty {

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ public class TextViewController: NSViewController {
2121
public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification")
2222

2323
var scrollView: NSScrollView!
24-
private(set) public var textView: TextView!
24+
25+
// SEARCH
26+
var stackview: NSStackView!
27+
var searchField: NSTextField!
28+
var prevButton: NSButton!
29+
var nextButton: NSButton!
30+
31+
var textView: TextView!
2532
var gutterView: GutterView!
26-
internal var _undoManager: CEUndoManager?
33+
internal var _undoManager: CEUndoManager!
2734
/// Internal reference to any injected layers in the text view.
2835
internal var highlightLayers: [CALayer] = []
2936
internal var systemAppearance: NSAppearance.Name?
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//
2+
// SearchManager.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Tommy Ludwig on 03.02.25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
11+
extension TextViewController {
12+
@objc func searchFieldUpdated(_ notification: Notification) {
13+
if let textField = notification.object as? NSTextField {
14+
searchFile(query: textField.stringValue)
15+
}
16+
}
17+
18+
@objc func onSubmit() {
19+
if let highlightedRange = textView.emphasizeAPI?.emphasizedRanges[textView.emphasizeAPI?.emphasizedRangeIndex ?? 0] {
20+
setCursorPositions([CursorPosition(range: highlightedRange.range)])
21+
updateCursorPosition()
22+
}
23+
}
24+
25+
@objc func prevButtonClicked() {
26+
textView?.emphasizeAPI?.highlightPrevious()
27+
if let currentRange = textView.emphasizeAPI?.emphasizedRanges[(textView.emphasizeAPI?.emphasizedRangeIndex) ?? 0].range {
28+
textView.scrollToRange(currentRange)
29+
}
30+
}
31+
32+
@objc func nextButtonClicked() {
33+
textView?.emphasizeAPI?.highlightNext()
34+
if let currentRange = textView.emphasizeAPI?.emphasizedRanges[(textView.emphasizeAPI?.emphasizedRangeIndex) ?? 0].range {
35+
textView.scrollToRange(currentRange)
36+
self.gutterView.needsDisplay = true
37+
}
38+
}
39+
40+
func searchFile(query: String) {
41+
let searchOptions: NSRegularExpression.Options = smartCase(str: query) ? [] : [.caseInsensitive]
42+
let escapedQuery = NSRegularExpression.escapedPattern(for: query)
43+
44+
guard let regex = try? NSRegularExpression(pattern: escapedQuery, options: searchOptions) else {
45+
textView?.emphasizeAPI?.removeEmphasizeLayers()
46+
return
47+
}
48+
49+
let matches = regex.matches(in: text, range: NSRange(location: 0, length: text.utf16.count))
50+
guard !matches.isEmpty else {
51+
textView?.emphasizeAPI?.removeEmphasizeLayers()
52+
return
53+
}
54+
55+
let searchResults = matches.map { $0.range }
56+
let bestHighlightIndex = getNearestHighlightIndex(ranges: searchResults) ?? 0
57+
print(searchResults.count)
58+
textView?.emphasizeAPI?.emphasizeRanges(ranges: searchResults, activeIndex: bestHighlightIndex)
59+
cursorPositions = [CursorPosition(range: searchResults[bestHighlightIndex])]
60+
}
61+
62+
private func getNearestHighlightIndex(ranges: [NSRange]) -> Int? {
63+
// order the array as follows
64+
// Found: 1 -> 2 -> 3 -> 4
65+
// Cursor: |
66+
// Result: 3 -> 4 -> 1 -> 2
67+
guard let cursorPosition = cursorPositions.first else { return nil }
68+
let start = cursorPosition.range.location
69+
70+
var left = 0
71+
var right = ranges.count - 1
72+
var bestIndex = -1
73+
var bestDiff = Int.max // Stores the closest difference
74+
75+
while left <= right {
76+
let mid = left + (right - left) / 2
77+
let midStart = ranges[mid].location
78+
let diff = abs(midStart - start)
79+
80+
// If it's an exact match, return immediately
81+
if diff == 0 {
82+
return mid
83+
}
84+
85+
// If this is the closest so far, update the best index
86+
if diff < bestDiff {
87+
bestDiff = diff
88+
bestIndex = mid
89+
}
90+
91+
// Move left or right based on the cursor position
92+
if midStart < start {
93+
left = mid + 1
94+
} else {
95+
right = mid - 1
96+
}
97+
}
98+
99+
return bestIndex >= 0 ? bestIndex : nil
100+
}
101+
102+
// Only re-serach the part of the file that changed upwards
103+
private func reSearch() { }
104+
105+
// Returns true if string contains uppercase letter
106+
// used for: ignores letter case if the search query is all lowercase
107+
private func smartCase(str: String) -> Bool {
108+
return str.range(of: "[A-Z]", options: .regularExpression) != nil
109+
}
110+
}

0 commit comments

Comments
 (0)