mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 00:21:46 +00:00
@@ -117,18 +117,25 @@ extension Ghostty {
|
||||
|
||||
extension Ghostty.OSSurfaceView {
|
||||
@MainActor class SearchState: ObservableObject {
|
||||
|
||||
/// We should always change needle's text and its selection together
|
||||
struct Needle: Equatable {
|
||||
var text: String
|
||||
var selection: Range<String.Index>?
|
||||
|
||||
static let empty = Needle(text: "", selection: nil)
|
||||
}
|
||||
|
||||
/// The pasteboard used to persist the search needle.
|
||||
///
|
||||
/// The `.find` pasteboard lets us sync our needle across the system and other find bars.
|
||||
private let pasteboard: OSPasteboard
|
||||
|
||||
@Published private(set) var needle: String = ""
|
||||
@Published var needle = Needle.empty
|
||||
|
||||
@Published var selected: UInt?
|
||||
@Published var total: UInt?
|
||||
|
||||
/// The range of the needle's text selection in the find bar.
|
||||
@Published private(set) var needleSelection: Range<String.Index>?
|
||||
|
||||
init(
|
||||
from startSearch: Ghostty.Action.StartSearch,
|
||||
pasteboard: OSPasteboard = OSPasteboard.find
|
||||
@@ -144,45 +151,21 @@ extension Ghostty.OSSurfaceView {
|
||||
|
||||
/// Replaces the search needle while keeping its selection valid.
|
||||
func setNeedle(_ needle: String, selectAll: Bool = false) {
|
||||
if needle != self.needle {
|
||||
// String.Index values are only valid for the string that created
|
||||
// them, so publish a nil selection before changing the string.
|
||||
needleSelection = nil
|
||||
self.needle = needle
|
||||
}
|
||||
|
||||
if selectAll {
|
||||
needleSelection = self.needle.startIndex..<self.needle.endIndex
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the selection only when both indices are valid for the needle.
|
||||
func setNeedleSelection(_ selection: Range<String.Index>?) {
|
||||
guard let selection else {
|
||||
needleSelection = nil
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let lowerBound = String.Index(selection.lowerBound, within: needle),
|
||||
let upperBound = String.Index(selection.upperBound, within: needle)
|
||||
else {
|
||||
needleSelection = nil
|
||||
return
|
||||
}
|
||||
|
||||
needleSelection = lowerBound..<upperBound
|
||||
self.needle = .init(
|
||||
text: needle,
|
||||
selection: selectAll ? needle.startIndex..<needle.endIndex : nil
|
||||
)
|
||||
}
|
||||
|
||||
func readPasteboardNeedle() {
|
||||
let pasteboardNeedle = pasteboard.string
|
||||
if let pasteboardNeedle, pasteboardNeedle != needle {
|
||||
if let pasteboardNeedle, pasteboardNeedle != needle.text {
|
||||
setNeedle(pasteboardNeedle, selectAll: true)
|
||||
}
|
||||
}
|
||||
|
||||
func writePasteboardNeedle() {
|
||||
pasteboard.string = needle
|
||||
pasteboard.string = needle.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -379,14 +379,8 @@ extension Ghostty {
|
||||
HStack(spacing: 4) {
|
||||
BackportSelectionTextField(
|
||||
"Search",
|
||||
text: Binding(
|
||||
get: { searchState.needle },
|
||||
set: { searchState.setNeedle($0) }
|
||||
),
|
||||
selection: Binding(
|
||||
get: { searchState.needleSelection },
|
||||
set: { searchState.setNeedleSelection($0) }
|
||||
)
|
||||
text: $searchState.needle.text,
|
||||
selection: $searchState.needle.selection
|
||||
)
|
||||
.textFieldStyle(.plain)
|
||||
.frame(width: 180)
|
||||
@@ -411,7 +405,7 @@ extension Ghostty {
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchState.needle) { _ in
|
||||
.onChange(of: searchState.needle.text) { _ in
|
||||
searchState.writePasteboardNeedle()
|
||||
}
|
||||
.onReceive(
|
||||
@@ -428,7 +422,7 @@ extension Ghostty {
|
||||
}
|
||||
#if canImport(AppKit)
|
||||
.onExitCommand {
|
||||
if searchState.needle.isEmpty {
|
||||
if searchState.needle.text.isEmpty {
|
||||
onClose()
|
||||
} else {
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
|
||||
@@ -48,6 +48,7 @@ extension Ghostty {
|
||||
// needle is less than 3 chars, we debounce it for a few hundred ms to
|
||||
// avoid kicking off expensive searches.
|
||||
searchNeedleCancellable = searchState.$needle
|
||||
.map(\.text)
|
||||
.removeDuplicates()
|
||||
.map { needle -> AnyPublisher<String, Never> in
|
||||
if needle.isEmpty || needle.count >= 3 {
|
||||
|
||||
@@ -19,7 +19,7 @@ import Testing
|
||||
from: StartSearch(c: .init(needle: nil)),
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
#expect(sut.needle == "pb")
|
||||
#expect(sut.needle.text == "pb")
|
||||
}
|
||||
|
||||
@Test func init_withEmptyNeedle_readsPasteboardNeedle() {
|
||||
@@ -28,7 +28,7 @@ import Testing
|
||||
from: StartSearch(c: .init(needle: needle)),
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
#expect(sut.needle == "pb")
|
||||
#expect(sut.needle.text == "pb")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import Testing
|
||||
from: StartSearch(c: .init(needle: needle)),
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
#expect(sut.needle == "start")
|
||||
#expect(sut.needle.text == "start")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,25 +67,12 @@ import Testing
|
||||
from: StartSearch(c: .init(needle: nil)),
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
sut.setNeedleSelection(sut.needle.startIndex..<sut.needle.endIndex)
|
||||
sut.needle.selection = sut.needle.text.startIndex..<sut.needle.text.endIndex
|
||||
|
||||
sut.setNeedle("x")
|
||||
|
||||
#expect(sut.needle == "x")
|
||||
#expect(sut.needleSelection == nil)
|
||||
}
|
||||
|
||||
@Test func setNeedleSelection_ignoresRangeFromLongerNeedle() {
|
||||
let sut = SearchState(
|
||||
from: StartSearch(c: .init(needle: nil)),
|
||||
pasteboard: pasteboard
|
||||
)
|
||||
let oldNeedle = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
sut.setNeedle("x")
|
||||
sut.setNeedleSelection(oldNeedle.startIndex..<oldNeedle.endIndex)
|
||||
|
||||
#expect(sut.needleSelection == nil)
|
||||
#expect(sut.needle.text == "x")
|
||||
#expect(sut.needle.selection == nil)
|
||||
}
|
||||
|
||||
@Test func readPasteboardNeedle_whenPasteboardNeedleIsNil() {
|
||||
@@ -96,7 +83,7 @@ import Testing
|
||||
pasteboard.clearContents()
|
||||
sut.setNeedle("sut")
|
||||
sut.readPasteboardNeedle()
|
||||
#expect(sut.needle == "sut")
|
||||
#expect(sut.needle.text == "sut")
|
||||
}
|
||||
|
||||
@Test func readPasteboardNeedle_whenPasteboardNeedleIsValid() {
|
||||
@@ -106,7 +93,7 @@ import Testing
|
||||
)
|
||||
sut.setNeedle("sut")
|
||||
sut.readPasteboardNeedle()
|
||||
#expect(sut.needle == "pb")
|
||||
#expect(sut.needle.text == "pb")
|
||||
}
|
||||
|
||||
@Test func readPasteboardNeedle_setsNeedleSelectionRange() {
|
||||
@@ -118,6 +105,6 @@ import Testing
|
||||
sut.readPasteboardNeedle()
|
||||
|
||||
let expected = "pb".startIndex..<"pb".endIndex
|
||||
#expect(sut.needleSelection == expected)
|
||||
#expect(sut.needle.selection == expected)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user