Merge pull request #153 from bnema/fix/truncate-space-before-url
fix(omnibox): strip leading spaces before URLs on input
3 files · +93 -1
M internal/domain/url/normalize.go +12 -0
| @@ -169,3 +169,15 @@ func sanitizeDomain(domain string) string { | |||
| 169 | 169 | func SanitizeDomainForPNGSized(domain string, size int) string { | |
| 170 | 170 | return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size) | |
| 171 | 171 | } | |
| 172 | + | ||
| 173 | + | // TrimLeadingSpacesIfURL removes leading whitespace from input if the trimmed | |
| 174 | + | // result looks like a URL. Returns the original input unchanged if it doesn't | |
| 175 | + | // contain leading spaces or if the trimmed result is not a URL. | |
| 176 | + | // This handles cases like pasting " https://example.com" from clipboard. | |
| 177 | + | func TrimLeadingSpacesIfURL(input string) string { | |
| 178 | + | trimmed := strings.TrimLeft(input, " \t") | |
| 179 | + | if trimmed != input && LooksLikeURL(trimmed) { | |
| 180 | + | return trimmed | |
| 181 | + | } | |
| 182 | + | return input | |
| 183 | + | } | |
| @@ -169,3 +169,15 @@ func sanitizeDomain(domain string) string { | |||||
| 169 | func SanitizeDomainForPNGSized(domain string, size int) string { | 169 | func SanitizeDomainForPNGSized(domain string, size int) string { | ||
| 170 | return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size) | 170 | return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size) | ||
| 171 | } | 171 | } | ||
| 172 | + | ||||
| 173 | + | // TrimLeadingSpacesIfURL removes leading whitespace from input if the trimmed | |||
| 174 | + | // result looks like a URL. Returns the original input unchanged if it doesn't | |||
| 175 | + | // contain leading spaces or if the trimmed result is not a URL. | |||
| 176 | + | // This handles cases like pasting " https://example.com" from clipboard. | |||
| 177 | + | func TrimLeadingSpacesIfURL(input string) string { | |||
| 178 | + | trimmed := strings.TrimLeft(input, " \t") | |||
| 179 | + | if trimmed != input && LooksLikeURL(trimmed) { | |||
| 180 | + | return trimmed | |||
| 181 | + | } | |||
| 182 | + | return input | |||
| 183 | + | } | |||
M internal/domain/url/normalize_test.go +73 -0
| @@ -231,6 +231,79 @@ func TestExpandHome(t *testing.T) { | |||
| 231 | 231 | } | |
| 232 | 232 | } | |
| 233 | 233 | ||
| 234 | + | func TestTrimLeadingSpacesIfURL(t *testing.T) { | |
| 235 | + | tests := []struct { | |
| 236 | + | name string | |
| 237 | + | input string | |
| 238 | + | want string | |
| 239 | + | }{ | |
| 240 | + | { | |
| 241 | + | name: "single space before https URL", | |
| 242 | + | input: " https://example.com", | |
| 243 | + | want: "https://example.com", | |
| 244 | + | }, | |
| 245 | + | { | |
| 246 | + | name: "multiple spaces before https URL", | |
| 247 | + | input: " https://example.com", | |
| 248 | + | want: "https://example.com", | |
| 249 | + | }, | |
| 250 | + | { | |
| 251 | + | name: "tab before URL", | |
| 252 | + | input: "\thttps://example.com", | |
| 253 | + | want: "https://example.com", | |
| 254 | + | }, | |
| 255 | + | { | |
| 256 | + | name: "mixed whitespace before URL", | |
| 257 | + | input: " \t https://example.com", | |
| 258 | + | want: "https://example.com", | |
| 259 | + | }, | |
| 260 | + | { | |
| 261 | + | name: "space before domain", | |
| 262 | + | input: " example.com", | |
| 263 | + | want: "example.com", | |
| 264 | + | }, | |
| 265 | + | { | |
| 266 | + | name: "spaces before localhost", | |
| 267 | + | input: " localhost:8080", | |
| 268 | + | want: "localhost:8080", | |
| 269 | + | }, | |
| 270 | + | { | |
| 271 | + | name: "no leading space URL unchanged", | |
| 272 | + | input: "https://example.com", | |
| 273 | + | want: "https://example.com", | |
| 274 | + | }, | |
| 275 | + | { | |
| 276 | + | name: "search query with leading space unchanged", | |
| 277 | + | input: " hello world", | |
| 278 | + | want: " hello world", | |
| 279 | + | }, | |
| 280 | + | { | |
| 281 | + | name: "single word with leading space unchanged", | |
| 282 | + | input: " hello", | |
| 283 | + | want: " hello", | |
| 284 | + | }, | |
| 285 | + | { | |
| 286 | + | name: "empty string unchanged", | |
| 287 | + | input: "", | |
| 288 | + | want: "", | |
| 289 | + | }, | |
| 290 | + | { | |
| 291 | + | name: "only spaces unchanged", | |
| 292 | + | input: " ", | |
| 293 | + | want: " ", | |
| 294 | + | }, | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | for _, tt := range tests { | |
| 298 | + | t.Run(tt.name, func(t *testing.T) { | |
| 299 | + | got := TrimLeadingSpacesIfURL(tt.input) | |
| 300 | + | if got != tt.want { | |
| 301 | + | t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want) | |
| 302 | + | } | |
| 303 | + | }) | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | ||
| 234 | 307 | func TestLooksLikeURL(t *testing.T) { | |
| 235 | 308 | tests := []struct { | |
| 236 | 309 | name string | |
| @@ -231,6 +231,79 @@ func TestExpandHome(t *testing.T) { | |||||
| 231 | } | 231 | } | ||
| 232 | } | 232 | } | ||
| 233 | 233 | ||||
| 234 | + | func TestTrimLeadingSpacesIfURL(t *testing.T) { | |||
| 235 | + | tests := []struct { | |||
| 236 | + | name string | |||
| 237 | + | input string | |||
| 238 | + | want string | |||
| 239 | + | }{ | |||
| 240 | + | { | |||
| 241 | + | name: "single space before https URL", | |||
| 242 | + | input: " https://example.com", | |||
| 243 | + | want: "https://example.com", | |||
| 244 | + | }, | |||
| 245 | + | { | |||
| 246 | + | name: "multiple spaces before https URL", | |||
| 247 | + | input: " https://example.com", | |||
| 248 | + | want: "https://example.com", | |||
| 249 | + | }, | |||
| 250 | + | { | |||
| 251 | + | name: "tab before URL", | |||
| 252 | + | input: "\thttps://example.com", | |||
| 253 | + | want: "https://example.com", | |||
| 254 | + | }, | |||
| 255 | + | { | |||
| 256 | + | name: "mixed whitespace before URL", | |||
| 257 | + | input: " \t https://example.com", | |||
| 258 | + | want: "https://example.com", | |||
| 259 | + | }, | |||
| 260 | + | { | |||
| 261 | + | name: "space before domain", | |||
| 262 | + | input: " example.com", | |||
| 263 | + | want: "example.com", | |||
| 264 | + | }, | |||
| 265 | + | { | |||
| 266 | + | name: "spaces before localhost", | |||
| 267 | + | input: " localhost:8080", | |||
| 268 | + | want: "localhost:8080", | |||
| 269 | + | }, | |||
| 270 | + | { | |||
| 271 | + | name: "no leading space URL unchanged", | |||
| 272 | + | input: "https://example.com", | |||
| 273 | + | want: "https://example.com", | |||
| 274 | + | }, | |||
| 275 | + | { | |||
| 276 | + | name: "search query with leading space unchanged", | |||
| 277 | + | input: " hello world", | |||
| 278 | + | want: " hello world", | |||
| 279 | + | }, | |||
| 280 | + | { | |||
| 281 | + | name: "single word with leading space unchanged", | |||
| 282 | + | input: " hello", | |||
| 283 | + | want: " hello", | |||
| 284 | + | }, | |||
| 285 | + | { | |||
| 286 | + | name: "empty string unchanged", | |||
| 287 | + | input: "", | |||
| 288 | + | want: "", | |||
| 289 | + | }, | |||
| 290 | + | { | |||
| 291 | + | name: "only spaces unchanged", | |||
| 292 | + | input: " ", | |||
| 293 | + | want: " ", | |||
| 294 | + | }, | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | for _, tt := range tests { | |||
| 298 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 299 | + | got := TrimLeadingSpacesIfURL(tt.input) | |||
| 300 | + | if got != tt.want { | |||
| 301 | + | t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want) | |||
| 302 | + | } | |||
| 303 | + | }) | |||
| 304 | + | } | |||
| 305 | + | } | |||
| 306 | + | ||||
| 234 | func TestLooksLikeURL(t *testing.T) { | 307 | func TestLooksLikeURL(t *testing.T) { | ||
| 235 | tests := []struct { | 308 | tests := []struct { | ||
| 236 | name string | 309 | name string | ||
M internal/ui/component/omnibox.go +8 -1
| @@ -801,11 +801,18 @@ func (o *Omnibox) onEntryChanged() { | |||
| 801 | 801 | return | |
| 802 | 802 | } | |
| 803 | 803 | ||
| 804 | + | entryText := o.entry.GetText() | |
| 805 | + | if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText { | |
| 806 | + | o.entry.SetText(trimmed) | |
| 807 | + | o.entry.SetPosition(-1) | |
| 808 | + | return | |
| 809 | + | } | |
| 810 | + | ||
| 804 | 811 | // Reset navigation state when user types - space should type, not toggle favorite | |
| 805 | 812 | o.mu.Lock() | |
| 806 | 813 | o.hasNavigated = false | |
| 807 | 814 | // Update realInput with what user actually typed | |
| 808 | - | o.realInput = o.entry.GetText() | |
| 815 | + | o.realInput = entryText | |
| 809 | 816 | o.mu.Unlock() | |
| 810 | 817 | ||
| 811 | 818 | // Clear ghost text when user types (it will be recalculated after search) | |
| @@ -801,11 +801,18 @@ func (o *Omnibox) onEntryChanged() { | |||||
| 801 | return | 801 | return | ||
| 802 | } | 802 | } | ||
| 803 | 803 | ||||
| 804 | + | entryText := o.entry.GetText() | |||
| 805 | + | if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText { | |||
| 806 | + | o.entry.SetText(trimmed) | |||
| 807 | + | o.entry.SetPosition(-1) | |||
| 808 | + | return | |||
| 809 | + | } | |||
| 810 | + | ||||
| 804 | // Reset navigation state when user types - space should type, not toggle favorite | 811 | // Reset navigation state when user types - space should type, not toggle favorite | ||
| 805 | o.mu.Lock() | 812 | o.mu.Lock() | ||
| 806 | o.hasNavigated = false | 813 | o.hasNavigated = false | ||
| 807 | // Update realInput with what user actually typed | 814 | // Update realInput with what user actually typed | ||
| 808 | - | o.realInput = o.entry.GetText() | 815 | + | o.realInput = entryText |
| 809 | o.mu.Unlock() | 816 | o.mu.Unlock() | ||
| 810 | 817 | ||||
| 811 | // Clear ghost text when user types (it will be recalculated after search) | 818 | // Clear ghost text when user types (it will be recalculated after search) | ||