From b828b3a9c7bef0b4314ef01c96ffe5c6a85137c4 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Wed, 28 May 2025 15:54:23 -0500 Subject: [PATCH 1/6] Added find method picker that changes the method by which text is found. Options include: contains, match word, starts with, ends with, and regular expression. --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Find/FindMethod.swift | 29 +++ .../Find/PanelView/FindMethodPicker.swift | 222 ++++++++++++++++++ .../Find/PanelView/FindSearchField.swift | 2 + .../ViewModel/FindPanelViewModel+Find.swift | 39 ++- .../Find/ViewModel/FindPanelViewModel.swift | 7 + 6 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 Sources/CodeEditSourceEditor/Find/FindMethod.swift create mode 100644 Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f475425b..5647bc0c2 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", "state" : { - "revision" : "a5912e60f6bac25cd1cdf8bb532e1125b21cf7f7", - "version" : "0.10.1" + "revision" : "d51f3ad8370457acc431fa01eede555c3e58b86d", + "version" : "0.11.0" } }, { diff --git a/Sources/CodeEditSourceEditor/Find/FindMethod.swift b/Sources/CodeEditSourceEditor/Find/FindMethod.swift new file mode 100644 index 000000000..1abe7e14e --- /dev/null +++ b/Sources/CodeEditSourceEditor/Find/FindMethod.swift @@ -0,0 +1,29 @@ +// +// FindMethod.swift +// CodeEditSourceEditor +// +// Created by Austin Condiff on 5/2/25. +// + +enum FindMethod: CaseIterable { + case contains + case matchesWord + case startsWith + case endsWith + case regularExpression + + var displayName: String { + switch self { + case .contains: + return "Contains" + case .matchesWord: + return "Matches Word" + case .startsWith: + return "Starts With" + case .endsWith: + return "Ends With" + case .regularExpression: + return "Regular Expression" + } + } +} diff --git a/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift b/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift new file mode 100644 index 000000000..3462df13d --- /dev/null +++ b/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift @@ -0,0 +1,222 @@ +// +// FindMethodPicker.swift +// CodeEditSourceEditor +// +// Created by Austin Condiff on 5/2/25. +// + +import SwiftUI + +/// A SwiftUI view that provides a method picker for the find panel. +/// +/// The `FindMethodPicker` view is responsible for: +/// - Displaying a dropdown menu to switch between different find methods +/// - Managing the selected find method +/// - Providing a visual indicator for the current method +/// - Adapting its appearance based on the control's active state +/// - Handling method selection +struct FindMethodPicker: NSViewRepresentable { + @Binding var method: FindMethod + @Environment(\.controlActiveState) var activeState + var condensed: Bool = false + + private func createPopupButton(context: Context) -> NSPopUpButton { + let popup = NSPopUpButton(frame: .zero, pullsDown: false) + popup.bezelStyle = .regularSquare + popup.isBordered = false + popup.controlSize = .small + popup.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) + popup.autoenablesItems = false + popup.setContentHuggingPriority(.defaultHigh, for: .horizontal) + popup.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + popup.title = method.displayName + if condensed { + popup.isTransparent = true + popup.alphaValue = 0 + } + return popup + } + + private func createIconLabel() -> NSImageView { + let imageView = NSImageView() + let symbolName = method == .contains + ? "line.horizontal.3.decrease.circle" + : "line.horizontal.3.decrease.circle.fill" + imageView.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)? + .withSymbolConfiguration(.init(pointSize: 14, weight: .regular)) + imageView.contentTintColor = method == .contains + ? (activeState == .inactive ? .tertiaryLabelColor : .labelColor) + : (activeState == .inactive ? .tertiaryLabelColor : .controlAccentColor) + return imageView + } + + private func createChevronLabel() -> NSImageView { + let imageView = NSImageView() + imageView.image = NSImage(systemSymbolName: "chevron.down", accessibilityDescription: nil)? + .withSymbolConfiguration(.init(pointSize: 8, weight: .black)) + imageView.contentTintColor = activeState == .inactive ? .tertiaryLabelColor : .secondaryLabelColor + return imageView + } + + private func createMenu(context: Context) -> NSMenu { + let menu = NSMenu() + + // Add method items + FindMethod.allCases.forEach { method in + let item = NSMenuItem( + title: method.displayName, + action: #selector(Coordinator.methodSelected(_:)), + keyEquivalent: "" + ) + item.target = context.coordinator + item.tag = FindMethod.allCases.firstIndex(of: method) ?? 0 + item.state = method == self.method ? .on : .off + menu.addItem(item) + } + + // Add separator before regular expression + menu.insertItem(.separator(), at: 4) + + return menu + } + + private func setupConstraints( + container: NSView, + popup: NSPopUpButton, + iconLabel: NSImageView? = nil, + chevronLabel: NSImageView? = nil + ) { + popup.translatesAutoresizingMaskIntoConstraints = false + iconLabel?.translatesAutoresizingMaskIntoConstraints = false + chevronLabel?.translatesAutoresizingMaskIntoConstraints = false + + var constraints: [NSLayoutConstraint] = [] + + if condensed { + constraints += [ + popup.leadingAnchor.constraint(equalTo: container.leadingAnchor), + popup.trailingAnchor.constraint(equalTo: container.trailingAnchor), + popup.topAnchor.constraint(equalTo: container.topAnchor), + popup.bottomAnchor.constraint(equalTo: container.bottomAnchor), + popup.widthAnchor.constraint(equalToConstant: 36), + popup.heightAnchor.constraint(equalToConstant: 20) + ] + } else { + constraints += [ + popup.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 4), + popup.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4), + popup.topAnchor.constraint(equalTo: container.topAnchor), + popup.bottomAnchor.constraint(equalTo: container.bottomAnchor) + ] + } + + if let iconLabel = iconLabel { + constraints += [ + iconLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8), + iconLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor), + iconLabel.widthAnchor.constraint(equalToConstant: 14), + iconLabel.heightAnchor.constraint(equalToConstant: 14) + ] + } + + if let chevronLabel = chevronLabel { + constraints += [ + chevronLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -6), + chevronLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor), + chevronLabel.widthAnchor.constraint(equalToConstant: 8), + chevronLabel.heightAnchor.constraint(equalToConstant: 8) + ] + } + + NSLayoutConstraint.activate(constraints) + } + + func makeNSView(context: Context) -> NSView { + let container = NSView() + container.wantsLayer = true + + let popup = createPopupButton(context: context) + popup.menu = createMenu(context: context) + popup.selectItem(at: FindMethod.allCases.firstIndex(of: method) ?? 0) + + container.addSubview(popup) + + if condensed { + let iconLabel = createIconLabel() + let chevronLabel = createChevronLabel() + container.addSubview(iconLabel) + container.addSubview(chevronLabel) + setupConstraints(container: container, popup: popup, iconLabel: iconLabel, chevronLabel: chevronLabel) + } else { + setupConstraints(container: container, popup: popup) + } + + return container + } + + func updateNSView(_ container: NSView, context: Context) { + guard let popup = container.subviews.first as? NSPopUpButton else { return } + + // Update selection, title, and color + popup.selectItem(at: FindMethod.allCases.firstIndex(of: method) ?? 0) + popup.title = method.displayName + popup.contentTintColor = activeState == .inactive ? .tertiaryLabelColor : .labelColor + if condensed { + popup.isTransparent = true + popup.alphaValue = 0 + } else { + popup.isTransparent = false + popup.alphaValue = 1 + } + + // Update menu items state + popup.menu?.items.forEach { item in + let index = item.tag + if index < FindMethod.allCases.count { + item.state = FindMethod.allCases[index] == method ? .on : .off + } + } + + // Update icon and chevron colors + if condensed { + if let iconLabel = container.subviews[1] as? NSImageView { + let symbolName = method == .contains + ? "line.horizontal.3.decrease.circle" + : "line.horizontal.3.decrease.circle.fill" + iconLabel.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)? + .withSymbolConfiguration(.init(pointSize: 14, weight: .regular)) + iconLabel.contentTintColor = method == .contains + ? (activeState == .inactive ? .tertiaryLabelColor : .labelColor) + : (activeState == .inactive ? .tertiaryLabelColor : .controlAccentColor) + } + if let chevronLabel = container.subviews[2] as? NSImageView { + chevronLabel.contentTintColor = activeState == .inactive ? .tertiaryLabelColor : .secondaryLabelColor + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(method: $method) + } + + var body: some View { + self.fixedSize() + } + + class Coordinator: NSObject { + @Binding var method: FindMethod + + init(method: Binding) { + self._method = method + } + + @objc func methodSelected(_ sender: NSMenuItem) { + method = FindMethod.allCases[sender.tag] + } + } +} + +#Preview("Find Method Picker") { + FindMethodPicker(method: .constant(.contains)) + .padding() +} diff --git a/Sources/CodeEditSourceEditor/Find/PanelView/FindSearchField.swift b/Sources/CodeEditSourceEditor/Find/PanelView/FindSearchField.swift index 0d81ad81f..12d19fcdf 100644 --- a/Sources/CodeEditSourceEditor/Find/PanelView/FindSearchField.swift +++ b/Sources/CodeEditSourceEditor/Find/PanelView/FindSearchField.swift @@ -94,6 +94,8 @@ struct FindSearchField: View { .frame(width: 30, height: 20) }) .toggleStyle(.icon) + Divider() + FindMethodPicker(method: $viewModel.findMethod, condensed: condensed) }, helperText: helperText, clearable: true diff --git a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift index ea75fff2c..90c5fe7c1 100644 --- a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift +++ b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift @@ -20,17 +20,48 @@ extension FindPanelViewModel { } // Set case sensitivity based on matchCase property - let findOptions: NSRegularExpression.Options = matchCase ? [] : [.caseInsensitive] - let escapedQuery = NSRegularExpression.escapedPattern(for: findText) + var findOptions: NSRegularExpression.Options = matchCase ? [] : [.caseInsensitive] - guard let regex = try? NSRegularExpression(pattern: escapedQuery, options: findOptions) else { + // Add multiline options for regular expressions + if findMethod == .regularExpression { + findOptions.insert(.dotMatchesLineSeparators) + findOptions.insert(.anchorsMatchLines) + } + + let pattern: String + + switch findMethod { + case .contains: + // Simple substring match, escape special characters + pattern = NSRegularExpression.escapedPattern(for: findText) + + case .matchesWord: + // Match whole words only using word boundaries + pattern = "\\b" + NSRegularExpression.escapedPattern(for: findText) + "\\b" + + case .startsWith: + // Match at the start of a line or after a word boundary + pattern = "(?:^|\\b)" + NSRegularExpression.escapedPattern(for: findText) + + case .endsWith: + // Match at the end of a line or before a word boundary + pattern = NSRegularExpression.escapedPattern(for: findText) + "(?:$|\\b)" + + case .regularExpression: + // Use the pattern directly without additional escaping + pattern = findText + } + + guard let regex = try? NSRegularExpression(pattern: pattern, options: findOptions) else { self.findMatches = [] self.currentFindMatchIndex = 0 return } let text = target.textView.string - let matches = regex.matches(in: text, range: NSRange(location: 0, length: text.utf16.count)) + let nsText = text as NSString + let range = NSRange(location: 0, length: nsText.length) + let matches = regex.matches(in: text, range: range) self.findMatches = matches.map(\.range) diff --git a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel.swift b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel.swift index d6975d112..6bbb02816 100644 --- a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel.swift +++ b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel.swift @@ -24,6 +24,13 @@ class FindPanelViewModel: ObservableObject { self.target?.findPanelModeDidChange(to: mode) } } + @Published var findMethod: FindMethod = .contains { + didSet { + if !findText.isEmpty { + find() + } + } + } @Published var isFocused: Bool = false From 5b8a05222a496b2c7b9d6bb372e0e2caa5a546bc Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Wed, 28 May 2025 16:13:54 -0500 Subject: [PATCH 2/6] Fixed SwiftLint errors --- .../Find/PanelView/FindMethodPicker.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift b/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift index 3462df13d..191d94ddc 100644 --- a/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift +++ b/Sources/CodeEditSourceEditor/Find/PanelView/FindMethodPicker.swift @@ -39,7 +39,7 @@ struct FindMethodPicker: NSViewRepresentable { private func createIconLabel() -> NSImageView { let imageView = NSImageView() - let symbolName = method == .contains + let symbolName = method == .contains ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill" imageView.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)? @@ -180,12 +180,12 @@ struct FindMethodPicker: NSViewRepresentable { // Update icon and chevron colors if condensed { if let iconLabel = container.subviews[1] as? NSImageView { - let symbolName = method == .contains + let symbolName = method == .contains ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill" iconLabel.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)? .withSymbolConfiguration(.init(pointSize: 14, weight: .regular)) - iconLabel.contentTintColor = method == .contains + iconLabel.contentTintColor = method == .contains ? (activeState == .inactive ? .tertiaryLabelColor : .labelColor) : (activeState == .inactive ? .tertiaryLabelColor : .controlAccentColor) } From aa7829a279987a07d7beeba4d4edf0f6105ccfbb Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 29 May 2025 12:19:39 -0500 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> --- .../Find/ViewModel/FindPanelViewModel+Find.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift index 90c5fe7c1..eba041fb8 100644 --- a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift +++ b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift @@ -54,14 +54,13 @@ extension FindPanelViewModel { guard let regex = try? NSRegularExpression(pattern: pattern, options: findOptions) else { self.findMatches = [] - self.currentFindMatchIndex = 0 + self.currentFindMatchIndex = nil return } let text = target.textView.string - let nsText = text as NSString - let range = NSRange(location: 0, length: nsText.length) - let matches = regex.matches(in: text, range: range) + let range = target.textView.documentRange + let matches = regex.matches(in: text, range: range).filter { !$0.range.isEmpty } self.findMatches = matches.map(\.range) From 6d4892576721be2b84463b2e4d72ed418dda7c2b Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 29 May 2025 13:03:17 -0500 Subject: [PATCH 4/6] Started writing a few tests for the find panel. --- .../FindPanelTests.swift | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 Tests/CodeEditSourceEditorTests/FindPanelTests.swift diff --git a/Tests/CodeEditSourceEditorTests/FindPanelTests.swift b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift new file mode 100644 index 000000000..adbb8fabc --- /dev/null +++ b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift @@ -0,0 +1,232 @@ +import Testing +import AppKit +import CodeEditTextView +@testable import CodeEditSourceEditor + +@MainActor +struct FindPanelTests { + class MockPanelTarget: FindPanelTarget { + var emphasisManager: EmphasisManager? + var findPanelTargetView: NSView + var cursorPositions: [CursorPosition] = [] + var textView: TextView! + var findPanelWillShowCalled = false + var findPanelWillHideCalled = false + var findPanelModeDidChangeCalled = false + var lastMode: FindPanelMode? + + @MainActor init(text: String = "") { + findPanelTargetView = NSView() + textView = TextView(string: text) + } + + func setCursorPositions(_ positions: [CursorPosition], scrollToVisible: Bool) { } + func updateCursorPosition() { } + func findPanelWillShow(panelHeight: CGFloat) { + findPanelWillShowCalled = true + } + func findPanelWillHide(panelHeight: CGFloat) { + findPanelWillHideCalled = true + } + func findPanelModeDidChange(to mode: FindPanelMode) { + findPanelModeDidChangeCalled = true + lastMode = mode + } + } + + @Test func findPanelShowsOnCommandF() async throws { + let target = MockPanelTarget() + let viewModel = FindPanelViewModel(target: target) + let viewController = FindViewController(target: target, childView: NSView()) + + // Show find panel + viewController.showFindPanel() + + // Verify panel is shown + #expect(viewModel.isShowingFindPanel == true) + #expect(target.findPanelWillShowCalled == true) + + // Hide find panel + viewController.hideFindPanel() + + // Verify panel is hidden + #expect(viewModel.isShowingFindPanel == false) + #expect(target.findPanelWillHideCalled == true) + } + + @Test func replaceFieldShowsWhenReplaceModeSelected() async throws { + let target = MockPanelTarget() + let viewModel = FindPanelViewModel(target: target) + + // Switch to replace mode + viewModel.mode = .replace + + // Verify mode change + #expect(viewModel.mode == .replace) + #expect(target.findPanelModeDidChangeCalled == true) + #expect(target.lastMode == .replace) + #expect(viewModel.panelHeight == 54) // Height should be larger in replace mode + + // Switch back to find mode + viewModel.mode = .find + + // Verify mode change + #expect(viewModel.mode == .find) + #expect(viewModel.panelHeight == 28) // Height should be smaller in find mode + } + + @Test func wrapAroundEnabled() async throws { + let target = MockPanelTarget(text: "test1\ntest2\ntest3") + let viewModel = FindPanelViewModel(target: target) + viewModel.findText = "test" + viewModel.wrapAround = true + + // Perform initial find + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Move to last match + viewModel.currentFindMatchIndex = 2 + + // Move to next (should wrap to first) + viewModel.moveToNextMatch() + #expect(viewModel.currentFindMatchIndex == 0) + + // Move to previous (should wrap to last) + viewModel.moveToPreviousMatch() + #expect(viewModel.currentFindMatchIndex == 2) + } + + @Test func wrapAroundDisabled() async throws { + let target = MockPanelTarget(text: "test1\ntest2\ntest3") + let viewModel = FindPanelViewModel(target: target) + viewModel.findText = "test" + viewModel.wrapAround = false + + // Perform initial find + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Move to last match + viewModel.currentFindMatchIndex = 2 + + // Move to next (should stay at last) + viewModel.moveToNextMatch() + #expect(viewModel.currentFindMatchIndex == 2) + + // Move to first match + viewModel.currentFindMatchIndex = 0 + + // Move to previous (should stay at first) + viewModel.moveToPreviousMatch() + #expect(viewModel.currentFindMatchIndex == 0) + } + + @Test func findMatches() async throws { + let target = MockPanelTarget(text: "test1\ntest2\ntest3") + let viewModel = FindPanelViewModel(target: target) + viewModel.findText = "test" + + viewModel.find() + + #expect(viewModel.findMatches.count == 3) + #expect(viewModel.findMatches[0].location == 0) + #expect(viewModel.findMatches[1].location == 6) + #expect(viewModel.findMatches[2].location == 12) + } + + @Test func noMatchesFound() async throws { + let target = MockPanelTarget(text: "test1\ntest2\ntest3") + let viewModel = FindPanelViewModel(target: target) + viewModel.findText = "nonexistent" + + viewModel.find() + + #expect(viewModel.findMatches.isEmpty) + #expect(viewModel.currentFindMatchIndex == nil) + } + + @Test func matchCaseToggle() async throws { + let target = MockPanelTarget(text: "Test1\ntest2\nTEST3") + let viewModel = FindPanelViewModel(target: target) + + // Test case-sensitive + viewModel.matchCase = true + viewModel.findText = "Test" + viewModel.find() + #expect(viewModel.findMatches.count == 1) + + // Test case-insensitive + viewModel.matchCase = false + viewModel.find() + #expect(viewModel.findMatches.count == 3) + } + + @Test func findMethodPickerOptions() async throws { + let target = MockPanelTarget(text: "test1 test2 test3") + let viewModel = FindPanelViewModel(target: target) + + // Test contains + viewModel.findMethod = .contains + viewModel.findText = "test" + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Test matchesWord + viewModel.findMethod = .matchesWord + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Test startsWith + viewModel.findMethod = .startsWith + viewModel.findText = "test" + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Test endsWith + viewModel.findMethod = .endsWith + viewModel.findText = "test3" + viewModel.find() + #expect(viewModel.findMatches.count == 1) + + // Test regularExpression + viewModel.findMethod = .regularExpression + viewModel.findText = "test\\d" + viewModel.find() + #expect(viewModel.findMatches.count == 3) + } + + @Test func findMethodPickerOptionsWithComplexText() async throws { + let target = MockPanelTarget(text: "test1 test2 test3\nprefix_test suffix_test\nword_test_word") + let viewModel = FindPanelViewModel(target: target) + + // Test contains with partial matches + viewModel.findMethod = .contains + viewModel.findText = "test" + viewModel.find() + #expect(viewModel.findMatches.count == 6) + + // Test matchesWord with word boundaries + viewModel.findMethod = .matchesWord + viewModel.find() + #expect(viewModel.findMatches.count == 3) + + // Test startsWith with prefixes + viewModel.findMethod = .startsWith + viewModel.findText = "prefix" + viewModel.find() + #expect(viewModel.findMatches.count == 1) + + // Test endsWith with suffixes + viewModel.findMethod = .endsWith + viewModel.findText = "suffix" + viewModel.find() + #expect(viewModel.findMatches.count == 1) + + // Test regularExpression with complex pattern + viewModel.findMethod = .regularExpression + viewModel.findText = "test\\d" + viewModel.find() + #expect(viewModel.findMatches.count == 3) + } +} From 82efbfef03818639505c49b18600f853836964fa Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 29 May 2025 16:37:59 -0500 Subject: [PATCH 5/6] Get Tests Going --- .../ViewModel/FindPanelViewModel+Find.swift | 2 +- .../FindPanelTests.swift | 47 ++++++++++--------- .../FindPanelViewModelTests.swift | 42 ----------------- 3 files changed, 27 insertions(+), 64 deletions(-) delete mode 100644 Tests/CodeEditSourceEditorTests/FindPanelViewModelTests.swift diff --git a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift index eba041fb8..ded2f09fa 100644 --- a/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift +++ b/Sources/CodeEditSourceEditor/Find/ViewModel/FindPanelViewModel+Find.swift @@ -65,7 +65,7 @@ extension FindPanelViewModel { self.findMatches = matches.map(\.range) // Find the nearest match to the current cursor position - currentFindMatchIndex = getNearestEmphasisIndex(matchRanges: findMatches) ?? 0 + currentFindMatchIndex = getNearestEmphasisIndex(matchRanges: findMatches) // Only add emphasis layers if the find panel is focused if isFocused { diff --git a/Tests/CodeEditSourceEditorTests/FindPanelTests.swift b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift index adbb8fabc..7b3acf833 100644 --- a/Tests/CodeEditSourceEditorTests/FindPanelTests.swift +++ b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift @@ -34,11 +34,26 @@ struct FindPanelTests { } } - @Test func findPanelShowsOnCommandF() async throws { - let target = MockPanelTarget() - let viewModel = FindPanelViewModel(target: target) - let viewController = FindViewController(target: target, childView: NSView()) + let target = MockPanelTarget() + let viewModel: FindPanelViewModel + let viewController: FindViewController + + init() { + viewController = FindViewController(target: target, childView: NSView()) + viewModel = viewController.viewModel + viewController.loadView() + } + @Test func viewModelHeightUpdates() async throws { + let model = FindPanelViewModel(target: MockPanelTarget()) + model.mode = .find + #expect(model.panelHeight == 28) + + model.mode = .replace + #expect(model.panelHeight == 54) + } + + @Test func findPanelShowsOnCommandF() async throws { // Show find panel viewController.showFindPanel() @@ -55,9 +70,6 @@ struct FindPanelTests { } @Test func replaceFieldShowsWhenReplaceModeSelected() async throws { - let target = MockPanelTarget() - let viewModel = FindPanelViewModel(target: target) - // Switch to replace mode viewModel.mode = .replace @@ -76,8 +88,7 @@ struct FindPanelTests { } @Test func wrapAroundEnabled() async throws { - let target = MockPanelTarget(text: "test1\ntest2\ntest3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1\ntest2\ntest3" viewModel.findText = "test" viewModel.wrapAround = true @@ -98,8 +109,7 @@ struct FindPanelTests { } @Test func wrapAroundDisabled() async throws { - let target = MockPanelTarget(text: "test1\ntest2\ntest3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1\ntest2\ntest3" viewModel.findText = "test" viewModel.wrapAround = false @@ -123,8 +133,7 @@ struct FindPanelTests { } @Test func findMatches() async throws { - let target = MockPanelTarget(text: "test1\ntest2\ntest3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1\ntest2\ntest3" viewModel.findText = "test" viewModel.find() @@ -136,8 +145,7 @@ struct FindPanelTests { } @Test func noMatchesFound() async throws { - let target = MockPanelTarget(text: "test1\ntest2\ntest3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1\ntest2\ntest3" viewModel.findText = "nonexistent" viewModel.find() @@ -147,8 +155,7 @@ struct FindPanelTests { } @Test func matchCaseToggle() async throws { - let target = MockPanelTarget(text: "Test1\ntest2\nTEST3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "Test1\ntest2\nTEST3" // Test case-sensitive viewModel.matchCase = true @@ -163,8 +170,7 @@ struct FindPanelTests { } @Test func findMethodPickerOptions() async throws { - let target = MockPanelTarget(text: "test1 test2 test3") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1 test2 test3" // Test contains viewModel.findMethod = .contains @@ -197,8 +203,7 @@ struct FindPanelTests { } @Test func findMethodPickerOptionsWithComplexText() async throws { - let target = MockPanelTarget(text: "test1 test2 test3\nprefix_test suffix_test\nword_test_word") - let viewModel = FindPanelViewModel(target: target) + target.textView.string = "test1 test2 test3\nprefix_test suffix_test\nword_test_word" // Test contains with partial matches viewModel.findMethod = .contains diff --git a/Tests/CodeEditSourceEditorTests/FindPanelViewModelTests.swift b/Tests/CodeEditSourceEditorTests/FindPanelViewModelTests.swift deleted file mode 100644 index ba2eb1530..000000000 --- a/Tests/CodeEditSourceEditorTests/FindPanelViewModelTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// FindPanelViewModelTests.swift -// CodeEditSourceEditor -// -// Created by Khan Winter on 4/25/25. -// - -import Testing -import AppKit -import CodeEditTextView -@testable import CodeEditSourceEditor - -@MainActor -struct FindPanelViewModelTests { - class MockPanelTarget: FindPanelTarget { - var emphasisManager: EmphasisManager? - var text: String = "" - var findPanelTargetView: NSView - var cursorPositions: [CursorPosition] = [] - var textView: TextView! - - @MainActor init() { - findPanelTargetView = NSView() - textView = TextView(string: text) - } - - func setCursorPositions(_ positions: [CursorPosition], scrollToVisible: Bool) { } - func updateCursorPosition() { } - func findPanelWillShow(panelHeight: CGFloat) { } - func findPanelWillHide(panelHeight: CGFloat) { } - func findPanelModeDidChange(to mode: FindPanelMode) { } - } - - @Test func viewModelHeightUpdates() async throws { - let model = FindPanelViewModel(target: MockPanelTarget()) - model.mode = .find - #expect(model.panelHeight == 28) - - model.mode = .replace - #expect(model.panelHeight == 54) - } -} From c8ab807516fcf867418f4f0f67495718c9f164f2 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 29 May 2025 17:30:58 -0500 Subject: [PATCH 6/6] Fixed tests --- Tests/CodeEditSourceEditorTests/FindPanelTests.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/CodeEditSourceEditorTests/FindPanelTests.swift b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift index 7b3acf833..4ddacbc4c 100644 --- a/Tests/CodeEditSourceEditorTests/FindPanelTests.swift +++ b/Tests/CodeEditSourceEditorTests/FindPanelTests.swift @@ -180,8 +180,9 @@ struct FindPanelTests { // Test matchesWord viewModel.findMethod = .matchesWord + viewModel.findText = "test1" viewModel.find() - #expect(viewModel.findMatches.count == 3) + #expect(viewModel.findMatches.count == 1) // Test startsWith viewModel.findMethod = .startsWith @@ -191,7 +192,7 @@ struct FindPanelTests { // Test endsWith viewModel.findMethod = .endsWith - viewModel.findText = "test3" + viewModel.findText = "3" viewModel.find() #expect(viewModel.findMatches.count == 1) @@ -203,7 +204,7 @@ struct FindPanelTests { } @Test func findMethodPickerOptionsWithComplexText() async throws { - target.textView.string = "test1 test2 test3\nprefix_test suffix_test\nword_test_word" + target.textView.string = "test1 test2 test3\nprefix_test test_suffix\nword_test_word" // Test contains with partial matches viewModel.findMethod = .contains @@ -213,8 +214,9 @@ struct FindPanelTests { // Test matchesWord with word boundaries viewModel.findMethod = .matchesWord + viewModel.findText = "test1" viewModel.find() - #expect(viewModel.findMatches.count == 3) + #expect(viewModel.findMatches.count == 1) // Test startsWith with prefixes viewModel.findMethod = .startsWith