Skip to content

Commit bf163f9

Browse files
committed
LibWebView: Suggest about: pages in the omnibox
Offer case-insensitive prefix matches for known about pages and support inline completion of those URLs. Keep about queries local instead of sending them to a remote autocomplete provider.
1 parent 6438845 commit bf163f9

4 files changed

Lines changed: 131 additions & 4 deletions

File tree

Libraries/LibWebView/Autocomplete.cpp

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
#include <LibWebView/HistoryDebug.h>
2525
#include <LibWebView/HistoryStore.h>
2626
#include <LibWebView/URL.h>
27+
#include <LibWebView/WebUI.h>
2728

2829
namespace WebView {
2930

3031
static constexpr auto file_url_prefix = "file://"sv;
32+
static constexpr auto about_url_prefix = "about:"sv;
3133

3234
static constexpr auto builtin_autocomplete_engines = to_array<AutocompleteEngine>({
3335
{ "DuckDuckGo"sv, "https://duckduckgo.com/ac/?q={}"sv },
@@ -150,6 +152,37 @@ Vector<String> filter_remote_autocomplete_suggestions(StringView input, Vector<S
150152
return suggestions;
151153
}
152154

155+
Vector<AutocompleteSuggestion> web_ui_autocomplete_suggestions(StringView input)
156+
{
157+
if (!input.starts_with(about_url_prefix, CaseSensitivity::CaseInsensitive))
158+
return {};
159+
160+
Vector<AutocompleteSuggestion> suggestions;
161+
suggestions.ensure_capacity(WebUI::pages().size());
162+
163+
for (auto const& page : WebUI::pages()) {
164+
auto url = MUST(String::formatted("about:{}", page.host));
165+
if (!url.bytes_as_string_view().starts_with(input, CaseSensitivity::CaseInsensitive))
166+
continue;
167+
168+
suggestions.unchecked_append({
169+
.source = AutocompleteSuggestionSource::WebUI,
170+
.text = move(url),
171+
.title = MUST(String::from_utf8(page.title)),
172+
.subtitle = {},
173+
.favicon_base64_png = {},
174+
.highlight_input = {},
175+
.match_class = AutocompleteMatchClass::URLPrefix,
176+
.relevance = 1100,
177+
.is_verbatim = false,
178+
.can_be_automatically_selected = true,
179+
.can_be_inline_completed = true,
180+
});
181+
}
182+
183+
return suggestions;
184+
}
185+
153186
[[maybe_unused]] static ByteString log_autocomplete_suggestions(Vector<AutocompleteSuggestion> const& suggestions)
154187
{
155188
Vector<ByteString> values;
@@ -275,6 +308,13 @@ void Autocomplete::query_autocomplete_engine(AutocompleteQueryID query_id, Strin
275308
return;
276309
}
277310

311+
if (m_trimmed_query.bytes_as_string_view().starts_with(about_url_prefix, CaseSensitivity::CaseInsensitive)) {
312+
m_remote_suggestions.clear();
313+
m_remote_query_complete = true;
314+
dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] Skipping remote autocomplete for about URL query '{}'", m_trimmed_query);
315+
return;
316+
}
317+
278318
auto engine = Application::settings().autocomplete_engine();
279319
if (!engine.has_value()) {
280320
m_remote_suggestions.clear();
@@ -362,13 +402,15 @@ void Autocomplete::deliver_current_result()
362402
if (!m_query_id.has_value())
363403
return;
364404

365-
auto verbatim_suggestion = literal_url_suggestion(m_query);
366-
if (!verbatim_suggestion.has_value())
405+
auto web_ui_suggestions = web_ui_autocomplete_suggestions(m_trimmed_query);
406+
auto verbatim_suggestion = web_ui_suggestions.is_empty() ? literal_url_suggestion(m_query) : Optional<AutocompleteSuggestion> {};
407+
if (web_ui_suggestions.is_empty() && !verbatim_suggestion.has_value())
367408
verbatim_suggestion = search_for_query_suggestion(m_query);
409+
web_ui_suggestions.extend(m_local_suggestions);
368410
auto merged_suggestions = mux_autocomplete_suggestions(
369411
m_trimmed_query,
370412
move(verbatim_suggestion),
371-
m_local_suggestions,
413+
move(web_ui_suggestions),
372414
make_remote_suggestions(m_remote_suggestions),
373415
m_max_suggestions);
374416
for (auto& suggestion : merged_suggestions)

Libraries/LibWebView/Autocomplete.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ static constexpr auto default_autocomplete_suggestion_limit = 8uz;
3838

3939
enum class AutocompleteSuggestionSource {
4040
LiteralURL,
41+
WebUI,
4142
History,
4243
Bookmark,
4344
Adaptive,
@@ -91,6 +92,7 @@ WEBVIEW_API Optional<AutocompleteEngine const&> find_autocomplete_engine_by_name
9192
WEBVIEW_API String autocomplete_suggestion_display_text(AutocompleteSuggestion const&);
9293
WEBVIEW_API Vector<AutocompleteMatchRange> autocomplete_match_ranges(StringView input, StringView text);
9394
WEBVIEW_API Vector<String> filter_remote_autocomplete_suggestions(StringView input, Vector<String> suggestions);
95+
WEBVIEW_API Vector<AutocompleteSuggestion> web_ui_autocomplete_suggestions(StringView input);
9496
WEBVIEW_API bool autocomplete_urls_match(StringView left, StringView right);
9597
WEBVIEW_API bool autocomplete_url_can_complete(StringView query, StringView suggestion);
9698

Tests/LibWebView/TestAutocomplete.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
217217
Core::EventLoop::current().spin_until([&]() { return whitespace_query_completed; });
218218
}
219219

220+
auto about_query_completed = false;
221+
{
222+
WebView::Autocomplete autocomplete { WebView::IsPrivate::No };
223+
autocomplete.on_autocomplete_query_complete = [&](auto, auto const& suggestions, WebView::AutocompleteResultKind kind) {
224+
if (kind != WebView::AutocompleteResultKind::Final)
225+
return;
226+
VERIFY(!suggestions.contains([](auto const& suggestion) {
227+
return suggestion.source == WebView::AutocompleteSuggestionSource::Search;
228+
}));
229+
VERIFY(suggestions.contains([](auto const& suggestion) {
230+
return suggestion.source == WebView::AutocompleteSuggestionSource::WebUI
231+
&& suggestion.text == "about:settings"sv;
232+
}));
233+
autocomplete.cancel_pending_query();
234+
about_query_completed = true;
235+
};
236+
autocomplete.query_autocomplete_engine(100, "about:set"_string);
237+
Core::EventLoop::current().spin_until([&]() { return about_query_completed; });
238+
}
239+
220240
// A closed loopback port makes the request fail fast and deterministically, with no real network.
221241
// Its on_finish synchronously delivers the final result. A data: URL cannot be used because the request
222242
// server's DNS path requires a host.

Tests/LibWebView/TestOmnibox.cpp

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <LibTest/TestCase.h>
88
#include <LibWebView/Omnibox.h>
9+
#include <LibWebView/WebUI.h>
910

1011
namespace {
1112

@@ -23,7 +24,7 @@ AutocompleteSuggestion row(AutocompleteSuggestionSource source, StringView text)
2324
.favicon_base64_png = {},
2425
.highlight_input = {},
2526
.can_be_automatically_selected = true,
26-
.can_be_inline_completed = source == AutocompleteSuggestionSource::History,
27+
.can_be_inline_completed = source == AutocompleteSuggestionSource::History || source == AutocompleteSuggestionSource::WebUI,
2728
};
2829
}
2930

@@ -64,6 +65,11 @@ AutocompleteSuggestion literal_row(StringView url)
6465
return row(AutocompleteSuggestionSource::LiteralURL, url);
6566
}
6667

68+
AutocompleteSuggestion web_ui_row(StringView url)
69+
{
70+
return row(AutocompleteSuggestionSource::WebUI, url);
71+
}
72+
6773
class ScriptedProvider final : public WebView::OmniboxSuggestionProvider {
6874
public:
6975
virtual void query(WebView::AutocompleteQueryID query_id, String query, size_t) override
@@ -188,6 +194,46 @@ struct Harness {
188194

189195
}
190196

197+
TEST_CASE(all_web_ui_pages_are_suggested)
198+
{
199+
static constexpr Array expected_urls {
200+
"about:about"sv,
201+
"about:bookmarks"sv,
202+
"about:downloads"sv,
203+
"about:history"sv,
204+
"about:newtab"sv,
205+
"about:processes"sv,
206+
"about:settings"sv,
207+
"about:version"sv,
208+
};
209+
210+
auto suggestions = WebView::web_ui_autocomplete_suggestions("about:"sv);
211+
212+
EXPECT_EQ(WebView::WebUI::pages().size(), expected_urls.size());
213+
EXPECT_EQ(suggestions.size(), expected_urls.size());
214+
for (size_t index = 0; index < expected_urls.size(); ++index) {
215+
EXPECT_EQ(suggestions[index].source, WebView::AutocompleteSuggestionSource::WebUI);
216+
EXPECT_EQ(suggestions[index].text, expected_urls[index]);
217+
}
218+
}
219+
220+
TEST_CASE(web_ui_pages_are_matched_by_case_insensitive_url_prefix)
221+
{
222+
auto suggestions = WebView::web_ui_autocomplete_suggestions("ABOUT:SET"sv);
223+
224+
EXPECT_EQ(suggestions.size(), 1u);
225+
EXPECT_EQ(suggestions[0].text, "about:settings"sv);
226+
EXPECT_EQ(suggestions[0].title, "Settings"sv);
227+
EXPECT(suggestions[0].can_be_automatically_selected);
228+
EXPECT(suggestions[0].can_be_inline_completed);
229+
}
230+
231+
TEST_CASE(web_ui_pages_are_not_suggested_for_unrelated_input)
232+
{
233+
EXPECT(WebView::web_ui_autocomplete_suggestions("settings"sv).is_empty());
234+
EXPECT(WebView::web_ui_autocomplete_suggestions("about:foo"sv).is_empty());
235+
}
236+
191237
TEST_CASE(fast_typing_publishes_each_local_generation)
192238
{
193239
Harness harness;
@@ -609,6 +655,23 @@ TEST_CASE(a_literal_url_suggestion_never_completes)
609655
EXPECT_EQ(harness.commits.last(), "t.example/path"sv);
610656
}
611657

658+
TEST_CASE(a_web_ui_suggestion_completes_and_commits_as_a_url)
659+
{
660+
Harness harness;
661+
harness.begin_editing();
662+
harness.display_text = "about:set"_string;
663+
harness.omnibox.text_edited(harness.display_text, true);
664+
665+
harness.provider->deliver({ web_ui_row("about:settings"sv) });
666+
EXPECT_EQ(harness.display_text, "about:settings"sv);
667+
EXPECT_EQ(harness.selection_start, 9u);
668+
EXPECT_EQ(harness.omnibox.selected_suggestion(), 0u);
669+
670+
harness.omnibox.return_pressed();
671+
EXPECT_EQ(harness.commits.last(), "about:settings"sv);
672+
EXPECT_EQ(harness.provider->engagements.last().destination_kind, WebView::OmniboxDestinationKind::URL);
673+
}
674+
612675
TEST_CASE(an_automatic_default_does_not_imply_inline_completion)
613676
{
614677
Harness harness;

0 commit comments

Comments
 (0)