Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ private enum TerminalInputSequence {
private final class TerminalInputField: UITextField {
var onDeleteBackward: (() -> Void)?
var onInsert: ((String) -> Void)?
private let deletionRouter = TerminalDeletionRouter()

private static let hardwareKeyCommands = TerminalHardwareKeyEncoder.makeKeyCommands(
action: #selector(handleHardwareKeyCommand(_:))
Expand All @@ -147,8 +148,14 @@ private final class TerminalInputField: UITextField {
}

override func deleteBackward() {
onDeleteBackward?()
super.deleteBackward()
deletionRouter.deleteBackward(
emit: { [weak self] in self?.onDeleteBackward?() },
performSystemDelete: { super.deleteBackward() }
)
}

func reportEmptyReplacement() {
deletionRouter.emptyReplacement { [weak self] in self?.onDeleteBackward?() }
}

@objc
Expand Down Expand Up @@ -410,6 +417,11 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
return false
}

// Some software keyboards report deletion here without calling deleteBackward.
// The field suppresses a duplicate if UIKit also calls this delegate from super.deleteBackward.
if textField === inputField {
inputField.reportEmptyReplacement()
}
return false
}

Expand Down
17 changes: 17 additions & 0 deletions apps/mobile/modules/t3-terminal/ios/TerminalDeletionRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// Sends one terminal DEL for either UITextField deletion callback path.
/// `super.deleteBackward()` may synchronously call the editing delegate.
final class TerminalDeletionRouter {
private var insideDeleteBackward = false

func deleteBackward(emit: () -> Void, performSystemDelete: () -> Void) {
insideDeleteBackward = true
defer { insideDeleteBackward = false }
emit()
performSystemDelete()
}

func emptyReplacement(emit: () -> Void) {
guard !insideDeleteBackward else { return }
emit()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

@main
struct TerminalDeletionTest {
static func main() {
let router = TerminalDeletionRouter()
var sent = 0
let emit = { sent += 1 }

// Software keyboards may only send an empty replacement to the delegate.
router.emptyReplacement(emit: emit)
precondition(sent == 1)

// UIKit may also invoke that delegate from the subclass's super call.
router.deleteBackward(emit: emit, performSystemDelete: {
router.emptyReplacement(emit: emit)
})
precondition(sent == 2)

// A hardware-key callback can arrive without the editing delegate.
router.deleteBackward(emit: emit, performSystemDelete: {})
precondition(sent == 3)

// Ownership resets after the call, so later software-keyboard deletes work.
router.emptyReplacement(emit: emit)
precondition(sent == 4)
}
}
Loading