native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200import AppKitimport ProwlCLISharedimport SwiftUIimport Testing
@testable import Prowl
@MainActorstruct PlainTextEditorTests { @Test func textUpdateAfterMiddleInsertionPreservesCaret() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.selectedRange = NSRange(location: 4, length: 0)
fixture.text = "abcXdef" fixture.update()
#expect(fixture.textView === textView) #expect(textView.string == "abcXdef") #expect(textView.selectedRange() == NSRange(location: 4, length: 0)) }
@Test func textUpdateAfterMiddleDeletionPreservesCaret() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.selectedRange = NSRange(location: 2, length: 0)
fixture.text = "abdef" fixture.update()
#expect(fixture.textView === textView) #expect(textView.string == "abdef") #expect(textView.selectedRange() == NSRange(location: 2, length: 0)) }
@Test func staleRefreshDuringMiddleTypingKeepsTextAndCaret() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.setSelectedRange(NSRange(location: 3, length: 0)) textView.insertText("X", replacementRange: NSRange(location: NSNotFound, length: 0)) #expect(fixture.text == "abcXdef") #expect(textView.selectedRange() == NSRange(location: 4, length: 0))
fixture.text = "abcdef" fixture.update()
#expect(textView.string == "abcXdef") #expect(textView.selectedRange() == NSRange(location: 4, length: 0))
fixture.text = "abcXdef" fixture.update()
#expect(textView.string == "abcXdef") #expect(textView.selectedRange() == NSRange(location: 4, length: 0)) }
@Test func staleRefreshDuringEndTypingKeepsTextAndCaret() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.setSelectedRange(NSRange(location: 6, length: 0)) textView.insertText("X", replacementRange: NSRange(location: NSNotFound, length: 0)) #expect(fixture.text == "abcdefX")
fixture.text = "abcdef" fixture.update()
#expect(textView.string == "abcdefX")
fixture.text = "abcdefX" fixture.update()
#expect(textView.string == "abcdefX") #expect(textView.selectedRange() == NSRange(location: 7, length: 0)) }
@Test func staleRefreshDuringMiddleDeletionKeepsTextAndCaret() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.setSelectedRange(NSRange(location: 3, length: 0)) textView.deleteBackward(nil) #expect(fixture.text == "abdef") #expect(textView.selectedRange() == NSRange(location: 2, length: 0))
fixture.text = "abcdef" fixture.update()
#expect(textView.string == "abdef")
fixture.text = "abdef" fixture.update()
#expect(textView.string == "abdef") #expect(textView.selectedRange() == NSRange(location: 2, length: 0)) }
@Test func externalChangeAfterEditRoundTripStillApplies() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.setSelectedRange(NSRange(location: 6, length: 0)) textView.insertText("X", replacementRange: NSRange(location: NSNotFound, length: 0)) fixture.update()
fixture.text = "xyz" fixture.update()
#expect(textView.string == "xyz") #expect(textView.selectedRange() == NSRange(location: 3, length: 0)) }
@Test func externalChangeClampsSelectionToNewLength() throws { let fixture = PlainTextEditorFixture(text: "abcdef") let textView = try #require(fixture.textView) textView.setSelectedRange(NSRange(location: 4, length: 2))
fixture.text = "abc" fixture.update()
#expect(textView.string == "abc") #expect(textView.selectedRange() == NSRange(location: 3, length: 0)) }}
@MainActorprivate final class PlainTextEditorFixture { var text: String { get { storage.text } set { storage.text = newValue } }
private let storage: PlainTextEditorTextStorage private var revision = 0 private let textBinding: Binding<String> private let window: NSWindow private let hostingView: NSHostingView<EditorHost>
init(text: String) { let storage = PlainTextEditorTextStorage(text: text) self.storage = storage textBinding = Binding( get: { storage.text }, set: { storage.text = $0 } ) window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 120), styleMask: [.titled], backing: .buffered, defer: true ) hostingView = NSHostingView(rootView: EditorHost(text: textBinding, revision: 0)) window.contentView = hostingView hostingView.layoutSubtreeIfNeeded() }
var textView: PlainTextEditor.PlaceholderTextView? { findTextView(in: hostingView) }
func update() { revision += 1 hostingView.rootView = EditorHost(text: textBinding, revision: revision) hostingView.layoutSubtreeIfNeeded() }
private func findTextView(in view: NSView) -> PlainTextEditor.PlaceholderTextView? { if let textView = view as? PlainTextEditor.PlaceholderTextView { return textView }
for subview in view.subviews { if let textView = findTextView(in: subview) { return textView } }
return nil }}
@MainActorprivate final class PlainTextEditorTextStorage { var text: String
init(text: String) { self.text = text }}
private struct EditorHost: View { let text: Binding<String> let revision: Int
var body: some View { PlainTextEditor( text: text, isMonospaced: true, placeholder: "Command \(revision)" ) }}