From 9b9bdd9647d90af146eaca9e4d56d98f9d35630e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 14:12:55 -0700 Subject: [PATCH] macos: avoid publishing search selection changes #13707 Search selection bindings update synchronously while SwiftUI is processing text edits. Publishing those changes re-entered view updates and could apply a stale String.Index range after deleting the needle. Keep selection state out of ObservableObject publishing. Needle changes still schedule view updates, while selection-only select-all operations explicitly notify observers. This is a bit of hail mary attempt at fixing #13707 because I couldn't actively reproduce it. But this keeps search working fine and conceptually addresses it. --- .../Ghostty/Surface View/OSSurfaceView.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift index 2f559f2ae..eecf54054 100644 --- a/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/OSSurfaceView.swift @@ -127,7 +127,12 @@ extension Ghostty.OSSurfaceView { @Published var total: UInt? /// The range of the needle's text selection in the find bar. - @Published private(set) var needleSelection: Range? + /// + /// This intentionally isn't published. SwiftUI updates this binding + /// synchronously while its text field is processing an edit. Publishing + /// from that callback re-enters the view update and can apply a stale + /// String.Index range to the newly edited needle. + private(set) var needleSelection: Range? init( from startSearch: Ghostty.Action.StartSearch, @@ -144,14 +149,22 @@ extension Ghostty.OSSurfaceView { /// Replaces the search needle while keeping its selection valid. func setNeedle(_ needle: String, selectAll: Bool = false) { - if needle != self.needle { + let needleChanged = needle != self.needle + if needleChanged { // String.Index values are only valid for the string that created - // them, so publish a nil selection before changing the string. + // them, so clear the selection before changing the string. needleSelection = nil self.needle = needle } if selectAll { + // A changed needle already schedules a view update through its + // @Published setter. If only the selection changes, schedule the + // update explicitly so programmatic select-all still takes effect. + if !needleChanged { + objectWillChange.send() + } + needleSelection = self.needle.startIndex..