composebox_typeahead: Filter and sort candidates within get_candidates.

This simplifies some of the logic and will make it easier to
convert to TypeScript.
This commit is contained in:
evykassirer 2024-05-02 21:19:05 -07:00 committed by Tim Abbott
parent 96abc52d97
commit 7f9361a865
2 changed files with 261 additions and 301 deletions

View File

@ -99,14 +99,14 @@ export function topics_seen_for(stream_id) {
return stream_topic_history.get_recent_topic_names(stream_id);
}
function get_language_matcher(query) {
export function get_language_matcher(query) {
query = query.toLowerCase();
return function (lang) {
return lang.includes(query);
};
}
function get_stream_or_user_group_matcher(query) {
export function get_stream_or_user_group_matcher(query) {
// Case-insensitive.
query = typeahead.clean_query_lowercase(query);
@ -115,7 +115,7 @@ function get_stream_or_user_group_matcher(query) {
};
}
function get_slash_matcher(query) {
export function get_slash_matcher(query) {
query = typeahead.clean_query_lowercase(query);
return function (item) {
@ -618,54 +618,6 @@ export function get_stream_topic_data(input_element) {
return opts;
}
export function get_sorted_filtered_items(query, input_element) {
/*
This is just a "glue" function to work
around bootstrap. We want to control these
three steps ourselves:
- get data
- filter data
- sort data
If we do it ourselves, we can convert some
O(N) behavior to just O(1) time.
For example, we want to avoid dispatching
on completing every time through the loop, plus
doing the same token cleanup every time.
It's also a bit easier to debug typeahead when
it's all one step, instead of three callbacks.
(We did the same thing for search suggestions
several years ago.)
*/
const big_results = get_candidates(query, input_element);
if (!big_results) {
return [];
}
// These are sorted separately
if (completing === "mention" || completing === "silent_mention") {
return big_results;
}
return filter_and_sort_candidates(completing, big_results, token);
}
export function filter_and_sort_candidates(completing, candidates, token) {
const matcher = compose_content_matcher(completing, token);
const small_results = candidates.filter((item) => matcher(item));
const sorted_results = sort_results(completing, small_results, token);
return sorted_results;
}
const ALLOWED_MARKDOWN_FEATURES = {
mention: true,
emoji: true,
@ -681,7 +633,7 @@ export function get_candidates(query, input_element) {
const split = split_at_cursor(query, input_element.$element);
let current_token = tokenize_compose_str(split[0]);
if (current_token === "") {
return false;
return [];
}
const rest = split[1];
@ -693,7 +645,7 @@ export function get_candidates(query, input_element) {
// We will likely want to extend this list to be more i18n-friendly.
const terminal_symbols = ",.;?!()[]> \"'\n\t";
if (rest !== "" && !terminal_symbols.includes(rest[0])) {
return false;
return [];
}
// Start syntax highlighting autocompleter if the first three characters are ```
@ -702,14 +654,14 @@ export function get_candidates(query, input_element) {
// Only autocomplete if user starts typing a language after ```
// unless the fence was added via the code formatting button.
if (current_token.length === 3 && !compose_ui.code_formatting_button_triggered) {
return false;
return [];
}
// If the only input is a space, don't autocomplete
current_token = current_token.slice(3);
if (current_token === " ") {
compose_ui.set_code_formatting_button_triggered(false);
return false;
return [];
}
// Trim the first whitespace if it is there
@ -724,7 +676,9 @@ export function get_candidates(query, input_element) {
? ["", ...realm_playground.get_pygments_typeahead_list_for_composebox()]
: realm_playground.get_pygments_typeahead_list_for_composebox();
compose_ui.set_code_formatting_button_triggered(false);
return language_list;
const matcher = get_language_matcher(token);
const matches = language_list.filter((item) => matcher(item));
return typeahead_helper.sort_languages(matches, token);
}
// Only start the emoji autocompleter if : is directly after one
@ -735,15 +689,17 @@ export function get_candidates(query, input_element) {
// Also, if the user has only typed a colon and nothing after,
// no need to match yet.
if (/^:-.?$/.test(current_token) || /^:[^+a-z]?$/.test(current_token)) {
return false;
return [];
}
// Don't autocomplete if there is a space following a ':'
if (current_token[1] === " ") {
return false;
return [];
}
completing = "emoji";
token = current_token.slice(1);
return emoji_collection;
const matcher = typeahead.get_emoji_matcher(token);
const matches = emoji_collection.filter((item) => matcher(item));
return typeahead.sort_emojis(matches, token);
}
if (ALLOWED_MARKDOWN_FEATURES.mention && current_token.startsWith("@")) {
@ -759,7 +715,7 @@ export function get_candidates(query, input_element) {
current_token = filter_mention_name(current_token);
if (current_token === undefined) {
completing = null;
return false;
return [];
}
token = current_token;
const opts = get_stream_topic_data(input_element);
@ -776,12 +732,15 @@ export function get_candidates(query, input_element) {
completing = "slash";
token = current_token;
return get_slash_commands_data();
const slash_commands = get_slash_commands_data();
const matcher = get_slash_matcher(token);
const matches = slash_commands.filter((item) => matcher(item));
return typeahead_helper.sort_slash_commands(matches, token);
}
if (ALLOWED_MARKDOWN_FEATURES.stream && current_token.startsWith("#")) {
if (current_token.length === 1) {
return false;
return [];
}
current_token = current_token.slice(1);
@ -791,12 +750,15 @@ export function get_candidates(query, input_element) {
// Don't autocomplete if there is a space following a '#'
if (current_token.startsWith(" ")) {
return false;
return [];
}
completing = "stream";
token = current_token;
return stream_data.get_unsorted_subs();
const subs = stream_data.get_unsorted_subs();
const matcher = get_stream_or_user_group_matcher(token);
const matches = subs.filter((item) => matcher(item));
return typeahead_helper.sort_streams(matches, token);
}
if (ALLOWED_MARKDOWN_FEATURES.topic) {
@ -823,7 +785,7 @@ export function get_candidates(query, input_element) {
// Don't autocomplete if there is a space following '>'
if (token.startsWith(" ")) {
return false;
return [];
}
const stream_id = stream_data.get_stream_id(stream_name);
@ -831,7 +793,9 @@ export function get_candidates(query, input_element) {
if (should_show_custom_query(token, topic_list)) {
topic_list.push(token);
}
return topic_list;
const matcher = get_topic_matcher(token);
const matches = topic_list.filter((item) => matcher(item));
return typeahead_helper.sorter(token, matches, (x) => x);
}
}
}
@ -842,7 +806,7 @@ export function get_candidates(query, input_element) {
return [$t({defaultMessage: "Mention a time-zone-aware time"})];
}
}
return false;
return [];
}
export function content_highlighter_html(item) {
@ -1039,53 +1003,6 @@ export function content_typeahead_selected(item, query, input_element, event) {
return beginning + rest;
}
export function compose_content_matcher(completing, token) {
switch (completing) {
case "emoji":
return typeahead.get_emoji_matcher(token);
case "slash":
return get_slash_matcher(token);
case "stream":
return get_stream_or_user_group_matcher(token);
case "syntax":
return get_language_matcher(token);
case "topic_list":
return get_topic_matcher(token);
}
return function () {
switch (completing) {
case "topic_jump":
case "time_jump":
// these don't actually have a typeahead popover, so we return quickly here.
return true;
default:
return undefined;
}
};
}
export function sort_results(completing, matches, token) {
switch (completing) {
case "emoji":
return typeahead.sort_emojis(matches, token);
case "slash":
return typeahead_helper.sort_slash_commands(matches, token);
case "stream":
return typeahead_helper.sort_streams(matches, token);
case "syntax":
return typeahead_helper.sort_languages(matches, token);
case "topic_jump":
case "time_jump":
// topic_jump doesn't actually have a typeahead popover, so we return quickly here.
return matches;
case "topic_list":
return typeahead_helper.sorter(token, matches, (x) => x);
default:
return undefined;
}
}
export function compose_automated_selection() {
if (completing === "topic_jump") {
// automatically jump inside stream mention on typing > just after
@ -1166,7 +1083,7 @@ export function initialize_compose_typeahead(selector) {
// matching and sorting inside the `source` field to avoid
// O(n) behavior in the number of users in the organization
// inside the typeahead library.
source: get_sorted_filtered_items,
source: get_candidates,
highlighter_html: content_highlighter_html,
matcher() {
return true;

View File

@ -53,7 +53,6 @@ const compose_pm_pill = zrequire("compose_pm_pill");
const compose_recipient = zrequire("compose_recipient");
const composebox_typeahead = zrequire("composebox_typeahead");
const settings_config = zrequire("settings_config");
const pygments_data = zrequire("pygments_data");
const ct = composebox_typeahead;
@ -286,6 +285,10 @@ for (const [key, val] of emojis_by_name.entries()) {
}
emoji_picker.rebuild_catalog();
const emoji_list = composebox_typeahead.emoji_collection;
const emoji_list_by_name = new Map(emoji_list.map((emoji) => [emoji.emoji_name, emoji]));
function emoji_objects(emoji_names) {
return emoji_names.map((emoji_name) => emoji_list_by_name.get(emoji_name));
}
const ali = user_or_mention_item({
email: "ali@zulip.com",
@ -1068,91 +1071,63 @@ test("initialize", ({override, override_rewire, mock_template}) => {
assert.equal(actual_value, expected_value);
// matching
let matcher = typeahead.get_emoji_matcher("ta");
assert.equal(matcher(make_emoji(emoji_tada), matcher), true);
assert.equal(matcher(make_emoji(emoji_moneybag), matcher), false);
function match(item) {
const token = ct.get_or_set_token_for_testing();
const completing = ct.get_or_set_completing_for_tests();
matcher = ct.get_stream_or_user_group_matcher("swed");
assert.equal(matcher(sweden_stream, matcher), true);
assert.equal(matcher(denmark_stream, matcher), false);
return ct.compose_content_matcher(completing, token)(item);
}
ct.get_or_set_completing_for_tests("emoji");
ct.get_or_set_token_for_testing("ta");
assert.equal(match(make_emoji(emoji_tada)), true);
assert.equal(match(make_emoji(emoji_moneybag)), false);
ct.get_or_set_completing_for_tests("stream");
ct.get_or_set_token_for_testing("swed");
assert.equal(match(sweden_stream), true);
assert.equal(match(denmark_stream), false);
ct.get_or_set_completing_for_tests("syntax");
ct.get_or_set_token_for_testing("py");
assert.equal(match("python"), true);
assert.equal(match("javascript"), false);
ct.get_or_set_completing_for_tests("non-existing-completion");
assert.equal(match(), undefined);
function sort_items(item) {
const token = ct.get_or_set_token_for_testing();
const completing = ct.get_or_set_completing_for_tests();
return ct.sort_results(completing, item, token);
}
matcher = ct.get_language_matcher("py");
assert.equal(matcher("python", matcher), true);
assert.equal(matcher("javascript", matcher), false);
// options.sorter()
ct.get_or_set_completing_for_tests("emoji");
ct.get_or_set_token_for_testing("ta");
actual_value = sort_items([make_emoji(emoji_stadium), make_emoji(emoji_tada)]);
actual_value = typeahead.sort_emojis(
[make_emoji(emoji_stadium), make_emoji(emoji_tada)],
"ta",
);
expected_value = [make_emoji(emoji_tada), make_emoji(emoji_stadium)];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("emoji");
ct.get_or_set_token_for_testing("th");
actual_value = sort_items([
make_emoji(emoji_thermometer),
make_emoji(emoji_thumbs_up),
]);
actual_value = typeahead.sort_emojis(
[make_emoji(emoji_thermometer), make_emoji(emoji_thumbs_up)],
"th",
);
expected_value = [make_emoji(emoji_thumbs_up), make_emoji(emoji_thermometer)];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("emoji");
ct.get_or_set_token_for_testing("he");
actual_value = sort_items([make_emoji(emoji_headphones), make_emoji(emoji_heart)]);
actual_value = typeahead.sort_emojis(
[make_emoji(emoji_headphones), make_emoji(emoji_heart)],
"he",
);
expected_value = [make_emoji(emoji_heart), make_emoji(emoji_headphones)];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("slash");
ct.get_or_set_token_for_testing("m");
actual_value = sort_items([my_slash, me_slash]);
actual_value = typeahead_helper.sort_slash_commands([my_slash, me_slash], "m");
expected_value = [me_slash, my_slash];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("slash");
ct.get_or_set_token_for_testing("da");
actual_value = sort_items([dark_slash, light_slash]);
actual_value = typeahead_helper.sort_slash_commands(
[dark_slash, light_slash],
"da",
);
expected_value = [dark_slash, light_slash];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("stream");
ct.get_or_set_token_for_testing("de");
actual_value = sort_items([sweden_stream, denmark_stream]);
actual_value = typeahead_helper.sort_streams([sweden_stream, denmark_stream], "de");
expected_value = [denmark_stream, sweden_stream];
assert.deepEqual(actual_value, expected_value);
// Matches in the descriptions affect the order as well.
// Testing "co" for "cold", in both streams' description. It's at the
// beginning of Sweden's description, so that one should go first.
ct.get_or_set_completing_for_tests("stream");
ct.get_or_set_token_for_testing("co");
actual_value = sort_items([denmark_stream, sweden_stream]);
actual_value = typeahead_helper.sort_streams([denmark_stream, sweden_stream], "co");
expected_value = [sweden_stream, denmark_stream];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("syntax");
ct.get_or_set_token_for_testing("ap");
actual_value = sort_items(["abap", "applescript"]);
actual_value = typeahead_helper.sort_languages(["abap", "applescript"], "ap");
expected_value = ["applescript", "abap"];
assert.deepEqual(actual_value, expected_value);
@ -1170,9 +1145,7 @@ test("initialize", ({override, override_rewire, mock_template}) => {
);
stream_list_sort.set_filter_out_inactives();
ct.get_or_set_completing_for_tests("stream");
ct.get_or_set_token_for_testing("s");
actual_value = sort_items([sweden_stream, serbia_stream]);
actual_value = typeahead_helper.sort_streams([sweden_stream, serbia_stream], "s");
expected_value = [sweden_stream, serbia_stream];
assert.deepEqual(actual_value, expected_value);
// Subscribed stream is inactive
@ -1183,19 +1156,17 @@ test("initialize", ({override, override_rewire, mock_template}) => {
);
stream_list_sort.set_filter_out_inactives();
actual_value = sort_items([sweden_stream, serbia_stream]);
actual_value = typeahead_helper.sort_streams([sweden_stream, serbia_stream], "s");
expected_value = [sweden_stream, serbia_stream];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("stream");
ct.get_or_set_token_for_testing("ser");
actual_value = sort_items([denmark_stream, serbia_stream]);
actual_value = typeahead_helper.sort_streams(
[denmark_stream, serbia_stream],
"ser",
);
expected_value = [serbia_stream, denmark_stream];
assert.deepEqual(actual_value, expected_value);
ct.get_or_set_completing_for_tests("non-existing-completion");
assert.equal(sort_items(), undefined);
compose_textarea_typeahead_called = true;
break;
@ -1392,19 +1363,42 @@ test("begins_typeahead", ({override, override_rewire}) => {
assert.deepEqual(values, reference);
}
const lang_list = Object.keys(pygments_data.langs);
function assert_typeahead_starts_with(input, rest, reference) {
if (reference === undefined) {
reference = rest;
rest = "";
}
const values = get_values(input, rest);
assert.ok(reference.length > 0);
assert.deepEqual(values.slice(0, reference.length), reference);
}
assert_typeahead_equals("test", false);
assert_typeahead_equals("test one two", false);
assert_typeahead_equals("*", false);
assert_typeahead_equals("* ", false);
assert_typeahead_equals(" *", false);
assert_typeahead_equals("test *", false);
assert_typeahead_equals("test", []);
assert_typeahead_equals("test one two", []);
assert_typeahead_equals("*", []);
assert_typeahead_equals("* ", []);
assert_typeahead_equals(" *", []);
assert_typeahead_equals("test *", []);
// Make sure that the last token is the one we read.
assert_typeahead_equals("~~~ @zulip", []); // zulip isn't set up as a user group
assert_typeahead_equals("@zulip :ta", emoji_list);
assert_typeahead_equals("#foo\n~~~py", lang_list);
assert_typeahead_equals("@zulip :ta", emoji_objects(["tada", "stadium"]));
assert_typeahead_equals("#foo\n~~~py", [
"py",
"py+ul4",
"py2",
"py2tb",
"py3tb",
"pycon",
"pypy",
"pyrex",
"antlr-python",
"bst-pybtex",
"ipython",
"ipython3",
"ipythonconsole",
"numpy",
]);
assert_typeahead_equals(":tada: <time:", ["translated: Mention a time-zone-aware time"]);
const mention_all = user_or_mention_item(ct.broadcast_mentions()[0]);
@ -1434,14 +1428,14 @@ test("begins_typeahead", ({override, override_rewire}) => {
assert_typeahead_equals("test @_*h", [harry, hal, hamlet, hamletcharacters, cordelia, othello]);
assert_typeahead_equals("test @", users_and_all_mention);
assert_typeahead_equals("test @_", users_and_user_groups);
assert_typeahead_equals("test no@o", false);
assert_typeahead_equals("test no@_k", false);
assert_typeahead_equals("@ ", false);
assert_typeahead_equals("@_ ", false);
assert_typeahead_equals("@* ", false);
assert_typeahead_equals("@_* ", false);
assert_typeahead_equals("@** ", false);
assert_typeahead_equals("@_** ", false);
assert_typeahead_equals("test no@o", []);
assert_typeahead_equals("test no@_k", []);
assert_typeahead_equals("@ ", []);
assert_typeahead_equals("@_ ", []);
assert_typeahead_equals("@* ", []);
assert_typeahead_equals("@_* ", []);
assert_typeahead_equals("@** ", []);
assert_typeahead_equals("@_** ", []);
assert_typeahead_equals("test\n@i", [
ali,
alice,
@ -1489,8 +1483,8 @@ test("begins_typeahead", ({override, override_rewire}) => {
]);
assert_typeahead_equals("@zuli", []);
assert_typeahead_equals("@_zuli", []);
assert_typeahead_equals("@ zuli", false);
assert_typeahead_equals("@_ zuli", false);
assert_typeahead_equals("@ zuli", []);
assert_typeahead_equals("@_ zuli", []);
assert_typeahead_equals(" @zuli", []);
assert_typeahead_equals(" @_zuli", []);
assert_typeahead_equals("test @o", [othello, cordelia, mention_everyone]);
@ -1498,95 +1492,149 @@ test("begins_typeahead", ({override, override_rewire}) => {
assert_typeahead_equals("test @z", []);
assert_typeahead_equals("test @_z", []);
assert_typeahead_equals(":", false);
assert_typeahead_equals(": ", false);
assert_typeahead_equals(" :", false);
assert_typeahead_equals(":)", false);
assert_typeahead_equals(":4", false);
assert_typeahead_equals(": la", false);
assert_typeahead_equals("test :-P", false);
assert_typeahead_equals("hi emoji :", false);
assert_typeahead_equals("hi emoj:i", false);
assert_typeahead_equals("hi emoji :D", false);
assert_typeahead_equals("hi emoji : t", false);
assert_typeahead_equals("hi emoji :t", emoji_list);
assert_typeahead_equals("hi emoji :ta", emoji_list);
assert_typeahead_equals("hi emoji :da", emoji_list);
assert_typeahead_equals("hi emoji :da_", emoji_list);
assert_typeahead_equals("hi emoji :da ", emoji_list);
assert_typeahead_equals("hi emoji\n:da", emoji_list);
assert_typeahead_equals("hi emoji\n :ra", emoji_list);
assert_typeahead_equals(":+", emoji_list);
assert_typeahead_equals(":la", emoji_list);
assert_typeahead_equals(" :lee", emoji_list);
assert_typeahead_equals("hi :see no", emoji_list);
assert_typeahead_equals("hi :japanese post of", emoji_list);
assert_typeahead_equals(":", []);
assert_typeahead_equals(": ", []);
assert_typeahead_equals(" :", []);
assert_typeahead_equals(":)", []);
assert_typeahead_equals(":4", []);
assert_typeahead_equals(": la", []);
assert_typeahead_equals("test :-P", []);
assert_typeahead_equals("hi emoji :", []);
assert_typeahead_equals("hi emoj:i", []);
assert_typeahead_equals("hi emoji :D", []);
assert_typeahead_equals("hi emoji : t", []);
assert_typeahead_equals(
"hi emoji :t",
emoji_objects([
"thumbs_up",
"tada",
"thermometer",
"heart",
"stadium",
"japanese_post_office",
]),
);
assert_typeahead_equals("hi emoji :ta", emoji_objects(["tada", "stadium"]));
assert_typeahead_equals("hi emoji :da", emoji_objects(["panda_face", "tada"]));
// We store the emoji panda_face with underscore, but that's not part of the emoji's name
assert_typeahead_equals("hi emoji :da_", emoji_objects([]));
assert_typeahead_equals("hi emoji :da ", emoji_objects([]));
assert_typeahead_equals("hi emoji\n:da", emoji_objects(["panda_face", "tada"]));
assert_typeahead_equals("hi emoji\n :ra", []);
assert_typeahead_equals(":+", []);
assert_typeahead_equals(":la", []);
assert_typeahead_equals(" :lee", []);
assert_typeahead_equals("hi :see no", emoji_objects(["see_no_evil"]));
assert_typeahead_equals("hi :japanese post of", emoji_objects(["japanese_post_office"]));
assert_typeahead_equals("#", false);
assert_typeahead_equals("# ", false);
assert_typeahead_equals(" #", false);
assert_typeahead_equals("# s", false);
assert_typeahead_equals("test #", false);
assert_typeahead_equals("test # a", false);
assert_typeahead_equals("test no#o", false);
assert_typeahead_equals("#", []);
assert_typeahead_equals("# ", []);
assert_typeahead_equals(" #", []);
assert_typeahead_equals("# s", []);
assert_typeahead_equals("test #", []);
assert_typeahead_equals("test # a", []);
assert_typeahead_equals("test no#o", []);
assert_typeahead_equals("/", composebox_typeahead.slash_commands);
assert_typeahead_equals("/m", composebox_typeahead.slash_commands);
assert_typeahead_equals(" /m", false);
assert_typeahead_equals("abc/me", false);
assert_typeahead_equals("hello /me", false);
assert_typeahead_equals("\n/m", false);
assert_typeahead_equals("/poll", composebox_typeahead.slash_commands);
assert_typeahead_equals(" /pol", false);
assert_typeahead_equals("abc/po", false);
assert_typeahead_equals("hello /poll", false);
assert_typeahead_equals("\n/pol", false);
assert_typeahead_equals("/todo", composebox_typeahead.slash_commands);
assert_typeahead_equals("my /todo", false);
assert_typeahead_equals("\n/to", false);
assert_typeahead_equals(" /tod", false);
const me_command = {
text: "translated: /me (Action message)",
name: "me",
aliases: "",
placeholder: "translated: is …",
};
const poll_command = {
text: "translated: /poll (Create a poll)",
name: "poll",
aliases: "",
placeholder: "translated: Question",
};
const todo_command = {
text: "translated: /todo (Create a collaborative to-do list)",
name: "todo",
aliases: "",
placeholder: "translated: Task list",
};
assert_typeahead_equals("x/", false);
assert_typeahead_equals("```", false);
assert_typeahead_equals("``` ", false);
assert_typeahead_equals(" ```", false);
assert_typeahead_equals("test ```", false);
assert_typeahead_equals("test ``` py", false);
assert_typeahead_equals("test ```a", false);
assert_typeahead_equals("test\n```", false);
assert_typeahead_equals("``c", false);
assert_typeahead_equals("```b", lang_list);
assert_typeahead_equals("``` d", lang_list);
assert_typeahead_equals("test\n``` p", lang_list);
assert_typeahead_equals("test\n``` p", lang_list);
assert_typeahead_equals("~~~", false);
assert_typeahead_equals("~~~ ", false);
assert_typeahead_equals(" ~~~", false);
assert_typeahead_equals(" ~~~ g", false);
assert_typeahead_equals("test ~~~", false);
assert_typeahead_equals("test ~~~p", false);
assert_typeahead_equals("test\n~~~", false);
assert_typeahead_equals("~~~e", lang_list);
assert_typeahead_equals("~~~ f", lang_list);
assert_typeahead_equals("test\n~~~ p", lang_list);
assert_typeahead_equals("test\n~~~ p", lang_list);
assert_typeahead_equals("/", [me_command, poll_command, todo_command]);
assert_typeahead_equals("/m", [me_command]);
// Slash commands can only occur at the start of a message
assert_typeahead_equals(" /m", []);
assert_typeahead_equals("abc/me", []);
assert_typeahead_equals("hello /me", []);
assert_typeahead_equals("\n/m", []);
assert_typeahead_equals("/poll", [poll_command]);
assert_typeahead_equals(" /pol", []);
assert_typeahead_equals("abc/po", []);
assert_typeahead_equals("hello /poll", []);
assert_typeahead_equals("\n/pol", []);
assert_typeahead_equals("/todo", [todo_command]);
assert_typeahead_equals("my /todo", []);
assert_typeahead_equals("\n/to", []);
assert_typeahead_equals(" /tod", []);
assert_typeahead_equals("x/", []);
// We don't open the typeahead until there's a letter after ```
assert_typeahead_equals("```", []);
assert_typeahead_equals("``` ", []);
assert_typeahead_equals(" ```", []);
assert_typeahead_equals("test ```", []);
assert_typeahead_equals("test ``` py", []);
assert_typeahead_equals("test ```a", []);
assert_typeahead_equals("test\n```", []);
assert_typeahead_equals("``c", []);
// Languages filtered by a single letter is a very long list.
// The typeahead displays languages sorted by popularity, so to
// avoid typing out all of them here we'll just test that the
// first several match up.
assert_typeahead_starts_with("```b", ["bash", "b3d", "bare", "basemake", "basic", "bat"]);
assert_typeahead_starts_with("``` d", [
"d",
"dart",
"d-objdump",
"dasm16",
"dax",
"debcontrol",
]);
const p_langs = ["python", "powershell", "php", "perl", "pacmanconf", "pan"];
assert_typeahead_starts_with("test\n``` p", p_langs);
// Too many spaces between ``` and the p to
// trigger the typeahead.
assert_typeahead_equals("test\n``` p", []);
assert_typeahead_equals("~~~", []);
assert_typeahead_equals("~~~ ", []);
assert_typeahead_equals(" ~~~", []);
// Only valid when ``` or ~~~ is at the beginning of a line.
assert_typeahead_equals(" ~~~ g", []);
assert_typeahead_equals("test ~~~", []);
assert_typeahead_equals("test ~~~p", []);
assert_typeahead_equals("test\n~~~", []);
assert_typeahead_starts_with("~~~e", [
"earl-grey",
"easytrieve",
"ebnf",
"ec",
"ecl",
"eiffel",
]);
assert_typeahead_starts_with("~~~ f", ["f#", "f90", "factor", "fan", "fancy", "fc"]);
assert_typeahead_starts_with("test\n~~~ p", p_langs);
// Too many spaces before the p
assert_typeahead_equals("test\n~~~ p", []);
// topic_jump
assert_typeahead_equals("@**a person**>", false);
assert_typeahead_equals("@**a person** >", false);
assert_typeahead_equals("@**a person**>", []);
assert_typeahead_equals("@**a person** >", []);
assert_typeahead_equals("#**stream**>", [""]); // this is deliberately a blank choice.
assert_typeahead_equals("#**stream** >", [""]);
assert_typeahead_equals("#**Sweden>some topic** >", false); // Already completed a topic.
assert_typeahead_equals("#**Sweden>some topic** >", []); // Already completed a topic.
// topic_list
// includes "more ice"
assert_typeahead_equals("#**Sweden>more ice", sweden_topics_to_show);
sweden_topics_to_show.push("totally new topic");
assert_typeahead_equals("#**Sweden>totally new topic", sweden_topics_to_show);
assert_typeahead_equals("#**Sweden>more ice", ["more ice", "even more ice"]);
assert_typeahead_equals("#**Sweden>totally new topic", ["totally new topic"]);
// time_jump
assert_typeahead_equals("<tim", false);
assert_typeahead_equals("<timerandom", false);
assert_typeahead_equals("<tim", []);
assert_typeahead_equals("<timerandom", []);
assert_typeahead_equals("<time", ["translated: Mention a time-zone-aware time"]);
assert_typeahead_equals("<time:", ["translated: Mention a time-zone-aware time"]);
assert_typeahead_equals("<time:something", ["translated: Mention a time-zone-aware time"]);
@ -1594,20 +1642,20 @@ test("begins_typeahead", ({override, override_rewire}) => {
"translated: Mention a time-zone-aware time",
]);
assert_typeahead_equals("<time:something>", ["translated: Mention a time-zone-aware time"]);
assert_typeahead_equals("<time:something> ", false); // Already completed the mention
assert_typeahead_equals("<time:something> ", []); // Already completed the mention
// Following tests place the cursor before the second string
assert_typeahead_equals("#test", "ing", false);
assert_typeahead_equals("@test", "ing", false);
assert_typeahead_equals(":test", "ing", false);
assert_typeahead_equals("```test", "ing", false);
assert_typeahead_equals("~~~test", "ing", false);
assert_typeahead_equals("#test", "ing", []);
assert_typeahead_equals("@test", "ing", []);
assert_typeahead_equals(":test", "ing", []);
assert_typeahead_equals("```test", "ing", []);
assert_typeahead_equals("~~~test", "ing", []);
const terminal_symbols = ",.;?!()[]> \"'\n\t";
for (const symbol of terminal_symbols.split()) {
assert_typeahead_equals("@test", symbol, []);
assert_typeahead_equals(":test", symbol, emoji_list);
assert_typeahead_equals("```test", symbol, lang_list);
assert_typeahead_equals("~~~test", symbol, lang_list);
assert_typeahead_equals("@othello", symbol, [othello]);
assert_typeahead_equals(":tada", symbol, emoji_objects(["tada"]));
assert_typeahead_starts_with("```p", symbol, p_langs);
assert_typeahead_starts_with("~~~p", symbol, p_langs);
}
});
@ -1753,12 +1801,9 @@ test("typeahead_results", () => {
mobile_stream,
];
function compose_typeahead_results(completing, items, token) {
return ct.filter_and_sort_candidates(completing, items, token);
}
function assert_emoji_matches(input, expected) {
const returned = compose_typeahead_results("emoji", emoji_list, input);
const matcher = typeahead.get_emoji_matcher(input);
const returned = emoji_list.filter((item) => matcher(item));
assert.deepEqual(returned, expected);
}
function assert_mentions_matches(input, expected) {
@ -1767,16 +1812,14 @@ test("typeahead_results", () => {
assert.deepEqual(returned, expected);
}
function assert_stream_matches(input, expected) {
const returned = compose_typeahead_results("stream", stream_list, input);
const matcher = ct.get_stream_or_user_group_matcher(input);
const returned = stream_list.filter((item) => matcher(item));
assert.deepEqual(returned, expected);
}
function assert_slash_matches(input, expected) {
const returned = compose_typeahead_results(
"slash",
composebox_typeahead.all_slash_commands,
input,
);
const matcher = ct.get_slash_matcher(input);
const returned = composebox_typeahead.all_slash_commands.filter((item) => matcher(item));
assert.deepEqual(returned, expected);
}
assert_emoji_matches("da", [
@ -1898,7 +1941,7 @@ test("typeahead_results", () => {
assert_stream_matches("cold", []);
assert_stream_matches("city", []);
// Always prioritise exact matches, irrespective of activity
assert_stream_matches("Mobile", [mobile_stream, mobile_team_stream]);
assert_stream_matches("Mobile", [mobile_team_stream, mobile_stream]);
});
test("message people", ({override, override_rewire}) => {