fix: constrain omnibox ghost completion to domain for noisy URLs
- commit
- 59a422a34efd997fea32881a276915ef346002cf
- author
- brice · · 2026-02-07 05:50 UTC
- parent
- bf5b0f6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +83 -1
M internal/ui/component/omnibox.go +48 -1
| @@ -3,6 +3,7 @@ package component | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | stdurl "net/url" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "sync" | |
| 8 | 9 | "time" | |
| @@ -1066,10 +1067,56 @@ func (o *Omnibox) resolveGhostCompletion(entryText string, mode ViewMode, sugges | |||
| 1066 | 1067 | o.clearGhostTextIfInput(entryText) | |
| 1067 | 1068 | return | |
| 1068 | 1069 | } | |
| 1069 | - | o.setGhostText(entryText, suggestion.Suffix, suggestion.FullText) | |
| 1070 | + | fullText, suffix := normalizeGhostSuggestion(entryText, suggestion.FullText, suggestion.Suffix) | |
| 1071 | + | if fullText == "" || suffix == "" { | |
| 1072 | + | o.clearGhostTextIfInput(entryText) | |
| 1073 | + | return | |
| 1074 | + | } | |
| 1075 | + | o.setGhostText(entryText, suffix, fullText) | |
| 1070 | 1076 | }() | |
| 1071 | 1077 | } | |
| 1072 | 1078 | ||
| 1079 | + | // normalizeGhostSuggestion trims noisy URL completions to a domain completion when input | |
| 1080 | + | // appears to be host-like (e.g. "google" -> "google.com" instead of full redirect URL). | |
| 1081 | + | func normalizeGhostSuggestion(queryText, fullText, fallbackSuffix string) (normalizedFullText, suffix string) { | |
| 1082 | + | if queryText == "" || fullText == "" { | |
| 1083 | + | return "", "" | |
| 1084 | + | } | |
| 1085 | + | ||
| 1086 | + | // If input already contains path/query-ish delimiters, keep original completion behavior. | |
| 1087 | + | if strings.ContainsAny(queryText, "/?#=& ") { | |
| 1088 | + | return fullText, fallbackSuffix | |
| 1089 | + | } | |
| 1090 | + | ||
| 1091 | + | hostOnly := extractHostForCompletion(fullText) | |
| 1092 | + | if hostOnly != "" { | |
| 1093 | + | for _, candidate := range []string{hostOnly, strings.TrimPrefix(hostOnly, "www.")} { | |
| 1094 | + | if completionSuffix, ok := autocomplete.ComputeCompletionSuffix(queryText, candidate); ok { | |
| 1095 | + | return candidate, completionSuffix | |
| 1096 | + | } | |
| 1097 | + | } | |
| 1098 | + | } | |
| 1099 | + | ||
| 1100 | + | return fullText, fallbackSuffix | |
| 1101 | + | } | |
| 1102 | + | ||
| 1103 | + | func extractHostForCompletion(raw string) string { | |
| 1104 | + | if raw == "" { | |
| 1105 | + | return "" | |
| 1106 | + | } | |
| 1107 | + | if parsed, err := stdurl.Parse(raw); err == nil && parsed.Host != "" { | |
| 1108 | + | return strings.ToLower(parsed.Hostname()) | |
| 1109 | + | } | |
| 1110 | + | trimmed := autocomplete.StripProtocol(raw) | |
| 1111 | + | trimmed = strings.ToLower(trimmed) | |
| 1112 | + | for _, sep := range []string{"/", "?", "#"} { | |
| 1113 | + | if idx := strings.Index(trimmed, sep); idx >= 0 { | |
| 1114 | + | trimmed = trimmed[:idx] | |
| 1115 | + | } | |
| 1116 | + | } | |
| 1117 | + | return strings.TrimSpace(trimmed) | |
| 1118 | + | } | |
| 1119 | + | ||
| 1073 | 1120 | func (o *Omnibox) isGhostTokenCurrent(searchToken, ghostToken uint64, query string) bool { | |
| 1074 | 1121 | o.mu.RLock() | |
| 1075 | 1122 | defer o.mu.RUnlock() | |
| @@ -3,6 +3,7 @@ package component | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | stdurl "net/url" | |||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "sync" | 8 | "sync" | ||
| 8 | "time" | 9 | "time" | ||
| @@ -1066,10 +1067,56 @@ func (o *Omnibox) resolveGhostCompletion(entryText string, mode ViewMode, sugges | |||||
| 1066 | o.clearGhostTextIfInput(entryText) | 1067 | o.clearGhostTextIfInput(entryText) | ||
| 1067 | return | 1068 | return | ||
| 1068 | } | 1069 | } | ||
| 1069 | - | o.setGhostText(entryText, suggestion.Suffix, suggestion.FullText) | 1070 | + | fullText, suffix := normalizeGhostSuggestion(entryText, suggestion.FullText, suggestion.Suffix) |
| 1071 | + | if fullText == "" || suffix == "" { | |||
| 1072 | + | o.clearGhostTextIfInput(entryText) | |||
| 1073 | + | return | |||
| 1074 | + | } | |||
| 1075 | + | o.setGhostText(entryText, suffix, fullText) | |||
| 1070 | }() | 1076 | }() | ||
| 1071 | } | 1077 | } | ||
| 1072 | 1078 | ||||
| 1079 | + | // normalizeGhostSuggestion trims noisy URL completions to a domain completion when input | |||
| 1080 | + | // appears to be host-like (e.g. "google" -> "google.com" instead of full redirect URL). | |||
| 1081 | + | func normalizeGhostSuggestion(queryText, fullText, fallbackSuffix string) (normalizedFullText, suffix string) { | |||
| 1082 | + | if queryText == "" || fullText == "" { | |||
| 1083 | + | return "", "" | |||
| 1084 | + | } | |||
| 1085 | + | ||||
| 1086 | + | // If input already contains path/query-ish delimiters, keep original completion behavior. | |||
| 1087 | + | if strings.ContainsAny(queryText, "/?#=& ") { | |||
| 1088 | + | return fullText, fallbackSuffix | |||
| 1089 | + | } | |||
| 1090 | + | ||||
| 1091 | + | hostOnly := extractHostForCompletion(fullText) | |||
| 1092 | + | if hostOnly != "" { | |||
| 1093 | + | for _, candidate := range []string{hostOnly, strings.TrimPrefix(hostOnly, "www.")} { | |||
| 1094 | + | if completionSuffix, ok := autocomplete.ComputeCompletionSuffix(queryText, candidate); ok { | |||
| 1095 | + | return candidate, completionSuffix | |||
| 1096 | + | } | |||
| 1097 | + | } | |||
| 1098 | + | } | |||
| 1099 | + | ||||
| 1100 | + | return fullText, fallbackSuffix | |||
| 1101 | + | } | |||
| 1102 | + | ||||
| 1103 | + | func extractHostForCompletion(raw string) string { | |||
| 1104 | + | if raw == "" { | |||
| 1105 | + | return "" | |||
| 1106 | + | } | |||
| 1107 | + | if parsed, err := stdurl.Parse(raw); err == nil && parsed.Host != "" { | |||
| 1108 | + | return strings.ToLower(parsed.Hostname()) | |||
| 1109 | + | } | |||
| 1110 | + | trimmed := autocomplete.StripProtocol(raw) | |||
| 1111 | + | trimmed = strings.ToLower(trimmed) | |||
| 1112 | + | for _, sep := range []string{"/", "?", "#"} { | |||
| 1113 | + | if idx := strings.Index(trimmed, sep); idx >= 0 { | |||
| 1114 | + | trimmed = trimmed[:idx] | |||
| 1115 | + | } | |||
| 1116 | + | } | |||
| 1117 | + | return strings.TrimSpace(trimmed) | |||
| 1118 | + | } | |||
| 1119 | + | ||||
| 1073 | func (o *Omnibox) isGhostTokenCurrent(searchToken, ghostToken uint64, query string) bool { | 1120 | func (o *Omnibox) isGhostTokenCurrent(searchToken, ghostToken uint64, query string) bool { | ||
| 1074 | o.mu.RLock() | 1121 | o.mu.RLock() | ||
| 1075 | defer o.mu.RUnlock() | 1122 | defer o.mu.RUnlock() | ||
A internal/ui/component/omnibox_ghost_normalize_test.go +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | package component | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestNormalizeGhostSuggestion_PrefersHostOnlyForRedirectURL(t *testing.T) { | |
| 6 | + | full, suffix := normalizeGhostSuggestion( | |
| 7 | + | "google", | |
| 8 | + | "google.com/url?q=https://dashboard.stripe.com/auth_challenge/email_link/abc", | |
| 9 | + | ".com/url?q=https://dashboard.stripe.com/auth_challenge/email_link/abc", | |
| 10 | + | ) | |
| 11 | + | if full != "google.com" { | |
| 12 | + | t.Fatalf("expected host-only completion, got %q", full) | |
| 13 | + | } | |
| 14 | + | if suffix != ".com" { | |
| 15 | + | t.Fatalf("expected .com suffix, got %q", suffix) | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func TestNormalizeGhostSuggestion_KeepsFallbackWhenInputLooksLikePath(t *testing.T) { | |
| 20 | + | fallback := "/url?q=https://x" | |
| 21 | + | full, suffix := normalizeGhostSuggestion("google.com/u", "google.com/url?q=https://x", fallback) | |
| 22 | + | if full != "google.com/url?q=https://x" { | |
| 23 | + | t.Fatalf("expected full URL fallback, got %q", full) | |
| 24 | + | } | |
| 25 | + | if suffix != fallback { | |
| 26 | + | t.Fatalf("expected fallback suffix, got %q", suffix) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func TestExtractHostForCompletion(t *testing.T) { | |
| 31 | + | got := extractHostForCompletion("https://WWW.Google.com/url?q=abc") | |
| 32 | + | if got != "www.google.com" { | |
| 33 | + | t.Fatalf("expected normalized host, got %q", got) | |
| 34 | + | } | |
| 35 | + | } | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | package component | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestNormalizeGhostSuggestion_PrefersHostOnlyForRedirectURL(t *testing.T) { | |||
| 6 | + | full, suffix := normalizeGhostSuggestion( | |||
| 7 | + | "google", | |||
| 8 | + | "google.com/url?q=https://dashboard.stripe.com/auth_challenge/email_link/abc", | |||
| 9 | + | ".com/url?q=https://dashboard.stripe.com/auth_challenge/email_link/abc", | |||
| 10 | + | ) | |||
| 11 | + | if full != "google.com" { | |||
| 12 | + | t.Fatalf("expected host-only completion, got %q", full) | |||
| 13 | + | } | |||
| 14 | + | if suffix != ".com" { | |||
| 15 | + | t.Fatalf("expected .com suffix, got %q", suffix) | |||
| 16 | + | } | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func TestNormalizeGhostSuggestion_KeepsFallbackWhenInputLooksLikePath(t *testing.T) { | |||
| 20 | + | fallback := "/url?q=https://x" | |||
| 21 | + | full, suffix := normalizeGhostSuggestion("google.com/u", "google.com/url?q=https://x", fallback) | |||
| 22 | + | if full != "google.com/url?q=https://x" { | |||
| 23 | + | t.Fatalf("expected full URL fallback, got %q", full) | |||
| 24 | + | } | |||
| 25 | + | if suffix != fallback { | |||
| 26 | + | t.Fatalf("expected fallback suffix, got %q", suffix) | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func TestExtractHostForCompletion(t *testing.T) { | |||
| 31 | + | got := extractHostForCompletion("https://WWW.Google.com/url?q=abc") | |||
| 32 | + | if got != "www.google.com" { | |||
| 33 | + | t.Fatalf("expected normalized host, got %q", got) | |||
| 34 | + | } | |||
| 35 | + | } | |||