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.
This commit is contained in:
Mitchell Hashimoto
2026-08-09 14:12:55 -07:00
parent 9d8fbd15b3
commit 9b9bdd9647

View File

@@ -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<String.Index>?
///
/// 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<String.Index>?
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..<self.needle.endIndex
}
}