Keep typed text in the connection host field; normalize only the model - #1591
Conversation
✅ Claude PR Review —
|
The address screen re-echoed the normalized model into the Host field on every keystroke. The model strips trailing slashes for assembly, so the `/` a user had just typed was deleted at once — a reverse-proxy subpath could not be typed at all, only pasted. It also bracketed any host containing a colon as an IPv6 literal, so `myserver.com:` on the way to a port and `http:` on the way to a scheme showed up as `[myserver.com:]` / `[http:]`, and typing a URL through was impossible. The field now keeps what was typed. `IntegrationServerAddress.applyHostField` applies the text to the model and returns what the row should display: the input itself, unless the edit moved something into another field — a full URL whose scheme and port were distributed, or a scheme-less `host:port` whose port was peeled — in which case the remaining host + subpath, so the port is not shown twice. Every other normalization (slashes, encoding, IPv6 brackets) applies to the model only and shows in the footer's assembled URL, as on Android. `normalizedHost` brackets only text that looks like an IPv6 literal (two or more colons, hex digits, dots, an optional zone) instead of anything with a colon, so a hostname with a stray colon stays as typed and `url` stays nil until it resolves. Tests: eight new cases on the typing path (subpath slash kept, colon not bracketed, port peel rewrites the field, scheme typed through, pasted URL, bare IPv6, typed space, bracket rule).
The field was rewritten after every keystroke whose text moved something into another field, so typing `myserver.com:8096` peeled `:8` into the port row at the first digit and sent `096` into the host. A URL typed through broke the same way. The model still takes every edit, so the port row and the footer's URL stay current. The field keeps the typed text and is rewritten only on a paste, which lands several characters at once, or when it loses focus. The port row now follows a port that arrives through the host field from the host field's handler.
7df48df to
3953fd9
Compare
| if address.port != port { | ||
| portText = address.port.map(String.init) ?? "" | ||
| } | ||
| if display != newValue, newValue.count > oldValue.count + 1 { |
There was a problem hiding this comment.
🔵 INFO — The paste heuristic newValue.count > oldValue.count + 1 won't recognize a paste that replaces a selection with shorter text (e.g. select-all over a long value, paste host:8096), so the host row keeps showing host:8096 until focus leaves even though the port was already peeled into address.port/portText. This is only cosmetic — the onChange(of: isHostFocused) handler normalizes it on focus loss, and the comment already acknowledges "typed text when the field loses focus." No change required; flagging only so the heuristic's limitation is understood.
What this is
The connection flow's address screen (#1577) mirrors the address model back into the Host field after every keystroke, so the field shows the normalized model rather than what was typed. Two of the model's normalizations make that destructive while typing:
normalizedPathstrips trailing slashes for assembly. Typemedia.example.com/and the/is deleted the moment it lands, so a reverse-proxy subpath cannot be typed at all — only pasted. Typingmedia.example.com/abscharacter by character ends up asmedia.example.comabs.normalizedHostbracketed any host containing a colon as an IPv6 literal.myserver.com:on the way to a port became[myserver.com:];http:on the way to a scheme became[http:], after which the/was eaten again — so typing a URL through was impossible too.A third case shows up once those two are fixed: rewriting the field per keystroke peels a port typed inline at its first digit.
myserver.com:8becomes hostmyserver.com+ port 8, and the next digits land in the host —myserver.com:8096ends ashttps://myserver.com096:8. A URL typed through breaks the same way.Found while bringing Android to parity; Android kept the typed text verbatim from the start.
The fix
IntegrationServerAddress.applyHostField(_:)applies the text to the model and returns what the row should display: the input itself, unless the edit moved something into another field — a full URL whose scheme and port go to their controls, or a scheme-lesshost:portwhose port is peeled off — in which case it returns the remaining host + subpath so the port is not shown twice. ThehostFieldsetter delegates to it, so existing callers and tests are unchanged.8 → 80 → 809 → 8096); it is laid out when the field loses focus (@FocusState), somyserver.com:8096then readsmyserver.comwith port 8096..onChange(of: address.host)sync — ordinary port typing still never rewrites the port text.normalizedHostbrackets only IPv6-looking text (two or more colons; hex digits, dots, colons, an optional%zone). A hostname with a stray colon stays as typed andurlstays nil, so Connect is disabled until the text resolves — no more[myserver.com:].Testing
IntegrationServerAddressTests+9: subpath slash kept in the field and dropped only in the model; colon toward a port neither bracketed nor assembled; port peels off a pastedhost:port; scheme typed through; pasted full URL; bare IPv6 as typed but assembled bracketed (incl. zone id); typed space encoded in the URL, not the field; bracket rule (::1yes,myserver.com:/http:/my:host:nameno); and an address typed character by character ends where its paste does (host:port/path, a fullhttp://…:port/path,[::1]:port).Unit Testsplan: 572/572 green (IntegrationServerAddressTests 33/33).media.example.com/abs/stripped →media.example.comabsmyserver.com:8096[myserver.com:]8096, Connect disabledhttps://myserver.com096:8https://myserver.com:8096; field →myserver.comon leaving ithttp://10.0.0.5:13378/abs[http:]10.0.0.5:13378abshttp://1.0.0.53378:1/abshttp://10.0.0.5:13378/abs; field →10.0.0.5/abs, scheme http🤖 Generated with Claude Code