2021-02-28 01:04:35 +01:00
|
|
|
import Handlebars from "handlebars/runtime";
|
2024-03-09 10:40:28 +01:00
|
|
|
import assert from "minimalistic-assert";
|
|
|
|
|
2021-02-28 01:04:35 +01:00
|
|
|
import * as common from "./common";
|
|
|
|
import {Filter} from "./filter";
|
|
|
|
import * as huddle_data from "./huddle_data";
|
|
|
|
import * as narrow_state from "./narrow_state";
|
2021-03-25 22:35:45 +01:00
|
|
|
import {page_params} from "./page_params";
|
2021-02-28 01:04:35 +01:00
|
|
|
import * as people from "./people";
|
2024-03-09 10:40:28 +01:00
|
|
|
import type {User} from "./people";
|
|
|
|
import type {NarrowTerm} from "./state_data";
|
2021-02-28 01:04:35 +01:00
|
|
|
import * as stream_data from "./stream_data";
|
|
|
|
import * as stream_topic_history from "./stream_topic_history";
|
2021-04-20 00:11:23 +02:00
|
|
|
import * as stream_topic_history_util from "./stream_topic_history_util";
|
2021-02-28 01:04:35 +01:00
|
|
|
import * as typeahead_helper from "./typeahead_helper";
|
2020-07-29 01:37:13 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
type UserPillItem = {
|
|
|
|
id: number;
|
|
|
|
display_value: Handlebars.SafeString;
|
|
|
|
has_image: boolean;
|
|
|
|
img_src: string;
|
|
|
|
should_add_guest_user_indicator: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
type TermPattern = Omit<NarrowTerm, "operand"> & Partial<Pick<NarrowTerm, "operand">>;
|
|
|
|
|
|
|
|
type Suggestion = {
|
|
|
|
description_html: string;
|
|
|
|
search_string: string;
|
|
|
|
is_person?: boolean;
|
|
|
|
user_pill_context?: UserPillItem;
|
|
|
|
};
|
|
|
|
|
2021-02-28 01:04:35 +01:00
|
|
|
export const max_num_of_search_results = 12;
|
2019-12-25 16:58:11 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function stream_matches_query(stream_name: string, q: string): boolean {
|
2018-06-25 17:14:45 +02:00
|
|
|
return common.phrase_match(q, stream_name);
|
2013-07-30 23:02:10 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function make_person_highlighter(query: string): (person: User) => string {
|
2022-06-19 08:34:42 +02:00
|
|
|
const highlight_query = typeahead_helper.make_query_highlighter(query);
|
2019-12-24 15:31:52 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
return function (person: User): string {
|
2022-06-19 08:34:42 +02:00
|
|
|
return highlight_query(person.full_name);
|
2019-12-24 15:31:52 +01:00
|
|
|
};
|
2013-07-30 23:02:10 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function highlight_person(person: User, highlighter: (person: User) => string): UserPillItem {
|
2021-12-06 23:44:26 +01:00
|
|
|
const avatar_url = people.small_avatar_url_for_person(person);
|
|
|
|
const highlighted_name = highlighter(person);
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: person.user_id,
|
|
|
|
display_value: new Handlebars.SafeString(highlighted_name),
|
|
|
|
has_image: true,
|
|
|
|
img_src: avatar_url,
|
2023-09-13 19:30:52 +02:00
|
|
|
should_add_guest_user_indicator: people.should_add_guest_user_indicator(person.user_id),
|
2021-12-06 23:44:26 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function match_criteria(terms: NarrowTerm[], criteria: TermPattern[]): boolean {
|
2023-12-22 00:26:14 +01:00
|
|
|
const filter = new Filter(terms);
|
2020-07-02 01:39:34 +02:00
|
|
|
return criteria.some((cr) => {
|
2024-03-09 10:40:28 +01:00
|
|
|
if (cr.operand !== undefined) {
|
2017-06-03 04:32:25 +02:00
|
|
|
return filter.has_operand(cr.operator, cr.operand);
|
|
|
|
}
|
|
|
|
return filter.has_operator(cr.operator);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function check_validity(
|
|
|
|
last: NarrowTerm,
|
|
|
|
terms: NarrowTerm[],
|
|
|
|
valid: string[],
|
|
|
|
incompatible_patterns: TermPattern[],
|
|
|
|
): boolean {
|
2018-06-01 15:00:00 +02:00
|
|
|
// valid: list of strings valid for the last operator
|
2024-03-15 10:41:52 +01:00
|
|
|
// incompatible_patterns: list of terms incompatible for any previous terms except last.
|
js: Convert a.indexOf(…) !== -1 to a.includes(…).
Babel polyfills this for us for Internet Explorer.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
recast.visit(ast, {
visitBinaryExpression(path) {
const { operator, left, right } = path.node;
if (
n.CallExpression.check(left) &&
n.MemberExpression.check(left.callee) &&
!left.callee.computed &&
n.Identifier.check(left.callee.property) &&
left.callee.property.name === "indexOf" &&
left.arguments.length === 1 &&
checkExpression(left.arguments[0]) &&
((["===", "!==", "==", "!=", ">", "<="].includes(operator) &&
n.UnaryExpression.check(right) &&
right.operator == "-" &&
n.Literal.check(right.argument) &&
right.argument.value === 1) ||
([">=", "<"].includes(operator) &&
n.Literal.check(right) &&
right.value === 0))
) {
const test = b.callExpression(
b.memberExpression(left.callee.object, b.identifier("includes")),
[left.arguments[0]]
);
path.replace(
["!==", "!=", ">", ">="].includes(operator)
? test
: b.unaryExpression("!", test)
);
changed = true;
}
this.traverse(path);
},
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 04:55:06 +01:00
|
|
|
if (!valid.includes(last.operator)) {
|
2018-06-01 15:00:00 +02:00
|
|
|
return false;
|
|
|
|
}
|
2024-03-15 10:41:52 +01:00
|
|
|
if (match_criteria(terms, incompatible_patterns)) {
|
2018-06-01 15:00:00 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function format_as_suggestion(terms: NarrowTerm[]): Suggestion {
|
2018-06-01 15:59:17 +02:00
|
|
|
return {
|
2023-06-26 20:11:17 +02:00
|
|
|
description_html: Filter.search_description_as_html(terms),
|
2018-06-01 15:59:17 +02:00
|
|
|
search_string: Filter.unparse(terms),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function compare_by_huddle(huddle_emails: string[]): (person1: User, person2: User) => number {
|
|
|
|
const user_ids = huddle_emails.slice(0, -1).flatMap((person) => {
|
|
|
|
const user = people.get_by_email(person);
|
|
|
|
return user?.user_id ?? [];
|
2017-07-05 02:58:05 +02:00
|
|
|
});
|
docs: Add missing space to compound verbs “back up”, “log in”, etc.
Noun: backup, login, logout, lookup, setup.
Verb: back up, log in, log out, look up, set up.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-07 20:41:10 +01:00
|
|
|
// Construct dict for all huddles, so we can look up each's recency
|
2020-05-26 14:25:21 +02:00
|
|
|
const huddles = huddle_data.get_huddles();
|
2024-03-09 10:40:28 +01:00
|
|
|
const huddle_dict = new Map<string, number>();
|
2020-02-12 07:05:22 +01:00
|
|
|
for (const [i, huddle] of huddles.entries()) {
|
|
|
|
huddle_dict.set(huddle, i + 1);
|
2017-07-05 02:58:05 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
return function (person1: User, person2: User): number {
|
|
|
|
const huddle1 = people.concat_huddle(user_ids, person1.user_id);
|
|
|
|
const huddle2 = people.concat_huddle(user_ids, person2.user_id);
|
2017-07-05 02:58:05 +02:00
|
|
|
// If not in the dict, assign an arbitrarily high index
|
2024-03-09 10:40:28 +01:00
|
|
|
const score1 = huddle_dict.get(huddle1) ?? huddles.length + 1;
|
|
|
|
const score2 = huddle_dict.get(huddle2) ?? huddles.length + 1;
|
2019-11-02 00:06:25 +01:00
|
|
|
const diff = score1 - score2;
|
2017-07-05 02:58:05 +02:00
|
|
|
|
|
|
|
if (diff !== 0) {
|
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
return typeahead_helper.compare_by_pms(person1, person2);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_stream_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2024-04-05 19:41:15 +02:00
|
|
|
const valid = ["channel", "search", ""];
|
2024-03-15 10:41:52 +01:00
|
|
|
const incompatible_patterns = [
|
2024-04-05 19:41:15 +02:00
|
|
|
{operator: "channel"},
|
2024-04-12 13:45:46 +02:00
|
|
|
{operator: "channels"},
|
2023-04-07 14:03:34 +02:00
|
|
|
{operator: "is", operand: "dm"},
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2023-04-19 17:35:32 +02:00
|
|
|
{operator: "dm-including"},
|
2017-06-03 07:10:13 +02:00
|
|
|
];
|
2024-03-15 10:41:52 +01:00
|
|
|
if (!check_validity(last, terms, valid, incompatible_patterns)) {
|
2013-07-30 23:02:10 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const query = last.operand;
|
|
|
|
let streams = stream_data.subscribed_streams();
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
streams = streams.filter((stream) => stream_matches_query(stream, query));
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2022-12-28 08:28:52 +01:00
|
|
|
streams = typeahead_helper.sorter(query, streams, (x) => x);
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2019-12-25 14:43:48 +01:00
|
|
|
const regex = typeahead_helper.build_highlight_regex(query);
|
2022-06-19 08:34:42 +02:00
|
|
|
const highlight_query = typeahead_helper.highlight_with_escaping_and_regex;
|
2019-12-25 14:43:48 +01:00
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
const objs = streams.map((stream) => {
|
2020-07-15 01:29:15 +02:00
|
|
|
const prefix = "stream";
|
2022-06-19 08:34:42 +02:00
|
|
|
const highlighted_stream = highlight_query(regex, stream);
|
2020-07-15 01:29:15 +02:00
|
|
|
const verb = last.negated ? "exclude " : "";
|
2022-08-18 14:54:00 +02:00
|
|
|
const description_html = verb + prefix + " " + highlighted_stream;
|
2019-11-02 00:06:25 +01:00
|
|
|
const term = {
|
2024-04-05 19:41:15 +02:00
|
|
|
operator: "channel",
|
2017-01-12 00:17:43 +01:00
|
|
|
operand: stream,
|
2018-05-21 06:23:08 +02:00
|
|
|
negated: last.negated,
|
2014-02-06 16:35:46 +01:00
|
|
|
};
|
2019-11-02 00:06:25 +01:00
|
|
|
const search_string = Filter.unparse([term]);
|
2022-06-19 07:17:16 +02:00
|
|
|
return {description_html, search_string};
|
2013-07-30 23:02:10 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return objs;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2023-04-11 21:04:33 +02:00
|
|
|
// For users with "pm-with" in their muscle memory, still
|
|
|
|
// have group direct message suggestions with "dm:" operator.
|
2024-04-05 19:41:15 +02:00
|
|
|
if (!check_validity(last, terms, ["dm", "pm-with"], [{operator: "channel"}])) {
|
2017-02-05 22:11:18 +01:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const operand = last.operand;
|
|
|
|
const negated = last.negated;
|
2017-02-05 22:11:18 +01:00
|
|
|
|
|
|
|
// The operand has the form "part1,part2,pa", where all but the last part
|
|
|
|
// are emails, and the last part is an arbitrary query.
|
|
|
|
//
|
|
|
|
// We only generate group suggestions when there's more than one part, and
|
|
|
|
// we only use the last part to generate suggestions.
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const last_comma_index = operand.lastIndexOf(",");
|
2017-02-05 22:11:18 +01:00
|
|
|
if (last_comma_index < 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Neither all_but_last_part nor last_part include the final comma.
|
2019-10-26 00:11:05 +02:00
|
|
|
const all_but_last_part = operand.slice(0, last_comma_index);
|
|
|
|
const last_part = operand.slice(last_comma_index + 1);
|
2017-02-05 22:11:18 +01:00
|
|
|
|
|
|
|
// We don't suggest a person if their email is already present in the
|
|
|
|
// operand (not including the last part).
|
2023-03-02 01:58:25 +01:00
|
|
|
const parts = [...all_but_last_part.split(","), people.my_current_email()];
|
2019-12-23 22:42:45 +01:00
|
|
|
|
|
|
|
const person_matcher = people.build_person_matcher(last_part);
|
2020-07-02 01:45:54 +02:00
|
|
|
let persons = people.filter_all_persons((person) => {
|
2020-02-08 04:04:36 +01:00
|
|
|
if (parts.includes(person.email)) {
|
2017-02-05 22:11:18 +01:00
|
|
|
return false;
|
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
return last_part === "" || person_matcher(person);
|
2017-02-05 22:11:18 +01:00
|
|
|
});
|
|
|
|
|
2017-07-05 02:58:05 +02:00
|
|
|
persons.sort(compare_by_huddle(parts));
|
2017-02-05 22:11:18 +01:00
|
|
|
|
2017-06-23 08:39:37 +02:00
|
|
|
// Take top 15 persons, since they're ordered by pm_recipient_count.
|
|
|
|
persons = persons.slice(0, 15);
|
2017-02-05 22:11:18 +01:00
|
|
|
|
2023-04-11 21:04:33 +02:00
|
|
|
const prefix = Filter.operator_to_prefix("dm", negated);
|
2017-02-05 22:11:18 +01:00
|
|
|
|
2022-06-19 07:24:38 +02:00
|
|
|
const person_highlighter = make_person_highlighter(last_part);
|
2019-12-24 15:31:52 +01:00
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
const suggestions = persons.map((person) => {
|
2019-11-02 00:06:25 +01:00
|
|
|
const term = {
|
2023-04-11 21:04:33 +02:00
|
|
|
operator: "dm",
|
2020-07-15 01:29:15 +02:00
|
|
|
operand: all_but_last_part + "," + person.email,
|
2020-07-20 22:18:43 +02:00
|
|
|
negated,
|
2017-02-05 22:11:18 +01:00
|
|
|
};
|
2021-12-06 23:44:26 +01:00
|
|
|
|
|
|
|
// Note that description_html won't contain the user's
|
|
|
|
// identity; that instead will be rendered in the separate
|
|
|
|
// user pill.
|
2022-06-19 07:17:16 +02:00
|
|
|
const description_html =
|
2021-12-06 23:44:26 +01:00
|
|
|
prefix + Handlebars.Utils.escapeExpression(" " + all_but_last_part + ",");
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
let terms: NarrowTerm[] = [term];
|
2017-02-05 22:11:18 +01:00
|
|
|
if (negated) {
|
2023-04-07 14:03:34 +02:00
|
|
|
terms = [{operator: "is", operand: "dm"}, term];
|
2017-02-05 22:11:18 +01:00
|
|
|
}
|
2021-12-06 23:44:26 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
description_html,
|
|
|
|
search_string: Filter.unparse(terms),
|
|
|
|
is_person: true,
|
|
|
|
user_pill_context: highlight_person(person, person_highlighter),
|
|
|
|
};
|
2017-02-05 22:11:18 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return suggestions;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function make_people_getter(last: NarrowTerm): () => User[] {
|
|
|
|
let persons: User[];
|
2020-01-02 12:53:11 +01:00
|
|
|
|
|
|
|
/* The next function will be called between 0 and 4
|
|
|
|
times for each keystroke in a search, but we will
|
|
|
|
only do real work one time.
|
|
|
|
*/
|
2024-03-09 10:40:28 +01:00
|
|
|
return function (): User[] {
|
2020-01-02 12:53:11 +01:00
|
|
|
if (persons !== undefined) {
|
|
|
|
return persons;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
let query: string;
|
2020-01-02 12:53:11 +01:00
|
|
|
|
2023-04-07 14:03:34 +02:00
|
|
|
// This next block is designed to match the behavior
|
|
|
|
// of the "is:dm" block in get_person_suggestions.
|
|
|
|
if (last.operator === "is" && last.operand === "dm") {
|
2020-07-15 01:29:15 +02:00
|
|
|
query = "";
|
2020-01-02 12:53:11 +01:00
|
|
|
} else {
|
|
|
|
query = last.operand;
|
|
|
|
}
|
|
|
|
|
|
|
|
persons = people.get_people_for_search_bar(query);
|
|
|
|
persons.sort(typeahead_helper.compare_by_pms);
|
|
|
|
return persons;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-19 20:25:55 +02:00
|
|
|
// Possible args for autocomplete_operator: dm, pm-with, sender, from, dm-including
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_person_suggestions(
|
|
|
|
people_getter: () => User[],
|
|
|
|
last: NarrowTerm,
|
|
|
|
terms: NarrowTerm[],
|
|
|
|
autocomplete_operator: string,
|
|
|
|
): Suggestion[] {
|
2023-04-11 21:04:33 +02:00
|
|
|
if ((last.operator === "is" && last.operand === "dm") || last.operator === "pm-with") {
|
|
|
|
// Interpret "is:dm" or "pm-with:" operator as equivalent to "dm:".
|
|
|
|
last = {operator: "dm", operand: "", negated: false};
|
2017-06-03 09:19:57 +02:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const query = last.operand;
|
2017-06-03 09:19:57 +02:00
|
|
|
|
|
|
|
// Be especially strict about the less common "from" operator.
|
2020-07-15 01:29:15 +02:00
|
|
|
if (autocomplete_operator === "from" && last.operator !== "from") {
|
2017-06-03 09:19:57 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const valid = ["search", autocomplete_operator];
|
2024-03-09 10:40:28 +01:00
|
|
|
let incompatible_patterns: TermPattern[] = [];
|
2023-04-12 18:51:51 +02:00
|
|
|
|
|
|
|
switch (autocomplete_operator) {
|
2023-04-19 17:35:32 +02:00
|
|
|
case "dm-including":
|
2024-04-05 19:41:15 +02:00
|
|
|
incompatible_patterns = [{operator: "channel"}, {operator: "is", operand: "resolved"}];
|
2023-04-12 18:51:51 +02:00
|
|
|
break;
|
2023-04-11 21:04:33 +02:00
|
|
|
case "dm":
|
2023-04-12 18:51:51 +02:00
|
|
|
case "pm-with":
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns = [
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2023-04-12 18:51:51 +02:00
|
|
|
{operator: "pm-with"},
|
2024-04-05 19:41:15 +02:00
|
|
|
{operator: "channel"},
|
2023-04-12 18:51:51 +02:00
|
|
|
{operator: "is", operand: "resolved"},
|
|
|
|
];
|
|
|
|
break;
|
|
|
|
case "sender":
|
|
|
|
case "from":
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns = [{operator: "sender"}, {operator: "from"}];
|
2023-04-12 18:51:51 +02:00
|
|
|
break;
|
2017-06-03 09:19:57 +02:00
|
|
|
}
|
|
|
|
|
2024-03-15 10:41:52 +01:00
|
|
|
if (!check_validity(last, terms, valid, incompatible_patterns)) {
|
2013-07-30 23:02:10 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-01-02 12:53:11 +01:00
|
|
|
const persons = people_getter();
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const prefix = Filter.operator_to_prefix(autocomplete_operator, last.negated);
|
2013-10-07 20:48:25 +02:00
|
|
|
|
2022-06-19 07:24:38 +02:00
|
|
|
const person_highlighter = make_person_highlighter(query);
|
2019-12-24 15:31:52 +01:00
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
const objs = persons.map((person) => {
|
2024-03-09 10:40:28 +01:00
|
|
|
const terms: NarrowTerm[] = [
|
2020-07-15 00:34:28 +02:00
|
|
|
{
|
|
|
|
operator: autocomplete_operator,
|
|
|
|
operand: person.email,
|
|
|
|
negated: last.negated,
|
|
|
|
},
|
|
|
|
];
|
2023-04-11 21:04:33 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
last.negated &&
|
|
|
|
(autocomplete_operator === "dm" || autocomplete_operator === "pm-with")
|
|
|
|
) {
|
|
|
|
// In the special case of "-dm" or "-pm-with", add "is:dm" before
|
|
|
|
// it because we assume the user still wants to narrow to direct
|
2023-04-07 14:03:34 +02:00
|
|
|
// messages.
|
|
|
|
terms.unshift({operator: "is", operand: "dm"});
|
2017-06-03 09:19:57 +02:00
|
|
|
}
|
2021-12-06 23:44:26 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
description_html: prefix,
|
|
|
|
search_string: Filter.unparse(terms),
|
|
|
|
is_person: true,
|
|
|
|
user_pill_context: highlight_person(person, person_highlighter),
|
|
|
|
};
|
2013-07-30 23:02:10 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return objs;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_default_suggestion(terms: NarrowTerm[]): Suggestion {
|
2020-06-13 16:34:23 +02:00
|
|
|
// Here we return the canonical suggestion for the query that the
|
2023-12-22 00:26:14 +01:00
|
|
|
// user typed. (The caller passes us the parsed query as "terms".)
|
|
|
|
if (terms.length === 0) {
|
2022-06-19 07:17:16 +02:00
|
|
|
return {description_html: "", search_string: ""};
|
2018-07-14 13:08:34 +02:00
|
|
|
}
|
2023-12-22 00:26:14 +01:00
|
|
|
return format_as_suggestion(terms);
|
2018-07-14 13:08:34 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
export function get_topic_suggestions_from_candidates({
|
|
|
|
candidate_topics,
|
|
|
|
guess,
|
|
|
|
}: {
|
|
|
|
candidate_topics: string[];
|
|
|
|
guess: string;
|
|
|
|
}): string[] {
|
2022-01-27 14:39:19 +01:00
|
|
|
// This function is exported for unit testing purposes.
|
|
|
|
const max_num_topics = 10;
|
|
|
|
|
|
|
|
if (guess === "") {
|
|
|
|
// In the search UI, once you autocomplete the stream,
|
|
|
|
// we just show you the most recent topics before you even
|
|
|
|
// need to start typing any characters.
|
|
|
|
return candidate_topics.slice(0, max_num_topics);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Once the user starts typing characters for a topic name,
|
|
|
|
// it is pretty likely they want to get suggestions for
|
|
|
|
// topics that may be fairly low in our list of candidates,
|
|
|
|
// so we do an aggressive search here.
|
|
|
|
//
|
|
|
|
// The following loop can be expensive if you have lots
|
|
|
|
// of topics in a stream, so we try to exit the loop as
|
|
|
|
// soon as we find enough matches.
|
2024-03-09 10:40:28 +01:00
|
|
|
const topics: string[] = [];
|
2022-01-27 14:39:19 +01:00
|
|
|
for (const topic of candidate_topics) {
|
|
|
|
if (common.phrase_match(guess, topic)) {
|
|
|
|
topics.push(topic);
|
|
|
|
if (topics.length >= max_num_topics) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return topics;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_topic_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2024-03-15 10:41:52 +01:00
|
|
|
const incompatible_patterns = [
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2023-04-07 14:03:34 +02:00
|
|
|
{operator: "is", operand: "dm"},
|
2023-04-19 17:35:32 +02:00
|
|
|
{operator: "dm-including"},
|
2020-07-15 01:29:15 +02:00
|
|
|
{operator: "topic"},
|
2017-06-03 22:54:35 +02:00
|
|
|
];
|
2024-04-05 19:41:15 +02:00
|
|
|
if (!check_validity(last, terms, ["channel", "topic", "search"], incompatible_patterns)) {
|
2017-06-03 22:54:35 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const operator = Filter.canonicalize_operator(last.operator);
|
|
|
|
const operand = last.operand;
|
2020-07-15 01:29:15 +02:00
|
|
|
const negated = operator === "topic" && last.negated;
|
2024-03-09 10:40:28 +01:00
|
|
|
let stream: string | undefined;
|
|
|
|
let guess: string | undefined;
|
2023-12-22 00:26:14 +01:00
|
|
|
const filter = new Filter(terms);
|
2024-03-09 10:40:28 +01:00
|
|
|
const suggest_terms: NarrowTerm[] = [];
|
2013-07-30 23:02:10 +02:00
|
|
|
|
|
|
|
// stream:Rome -> show all Rome topics
|
|
|
|
// stream:Rome topic: -> show all Rome topics
|
|
|
|
// stream:Rome f -> show all Rome topics with a word starting in f
|
|
|
|
// stream:Rome topic:f -> show all Rome topics with a word starting in f
|
|
|
|
// stream:Rome topic:f -> show all Rome topics with a word starting in f
|
|
|
|
|
|
|
|
// When narrowed to a stream:
|
|
|
|
// topic: -> show all topics in current stream
|
|
|
|
// foo -> show all topics in current stream with words starting with foo
|
|
|
|
|
|
|
|
// If somebody explicitly types search:, then we might
|
|
|
|
// not want to suggest topics, but I feel this is a very
|
2013-08-22 01:29:28 +02:00
|
|
|
// minor issue, and Filter.parse() is currently lossy
|
2013-07-30 23:02:10 +02:00
|
|
|
// in terms of telling us whether they provided the operator,
|
2014-02-10 20:53:38 +01:00
|
|
|
// i.e. "foo" and "search:foo" both become [{operator: 'search', operand: 'foo'}].
|
2013-07-30 23:02:10 +02:00
|
|
|
switch (operator) {
|
2024-04-05 19:41:15 +02:00
|
|
|
case "channel":
|
2020-07-15 02:14:03 +02:00
|
|
|
guess = "";
|
|
|
|
stream = operand;
|
2023-12-22 00:26:14 +01:00
|
|
|
suggest_terms.push(last);
|
2020-07-15 02:14:03 +02:00
|
|
|
break;
|
|
|
|
case "topic":
|
|
|
|
case "search":
|
|
|
|
guess = operand;
|
2024-04-05 19:41:15 +02:00
|
|
|
if (filter.has_operator("channel")) {
|
|
|
|
stream = filter.operands("channel")[0];
|
2020-07-15 02:14:03 +02:00
|
|
|
} else {
|
2023-06-26 18:52:26 +02:00
|
|
|
stream = narrow_state.stream_name();
|
2024-03-09 10:40:28 +01:00
|
|
|
if (stream) {
|
2024-04-05 19:41:15 +02:00
|
|
|
suggest_terms.push({operator: "channel", operand: stream});
|
2024-03-09 10:40:28 +01:00
|
|
|
}
|
2020-07-15 02:14:03 +02:00
|
|
|
}
|
|
|
|
break;
|
2013-07-30 23:02:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!stream) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2022-10-18 19:01:58 +02:00
|
|
|
const stream_sub = stream_data.get_sub(stream);
|
|
|
|
if (!stream_sub) {
|
2017-07-24 15:15:28 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2022-10-18 19:01:58 +02:00
|
|
|
if (stream_data.can_access_topic_history(stream_sub)) {
|
2024-03-09 10:40:28 +01:00
|
|
|
stream_topic_history_util.get_server_history(stream_sub.stream_id, () => {
|
|
|
|
// Fetch topic history from the server, in case we will need it.
|
|
|
|
// Note that we won't actually use the results from the server here
|
|
|
|
// for this particular keystroke from the user, because we want to
|
|
|
|
// show results immediately. Assuming the server responds quickly,
|
|
|
|
// as the user makes their search more specific, subsequent calls to
|
|
|
|
// this function will get more candidates from calling
|
|
|
|
// stream_topic_history.get_recent_topic_names.
|
|
|
|
});
|
2022-10-18 19:01:58 +02:00
|
|
|
}
|
2022-01-27 14:56:51 +01:00
|
|
|
|
2022-10-18 19:01:58 +02:00
|
|
|
const candidate_topics = stream_topic_history.get_recent_topic_names(stream_sub.stream_id);
|
2013-08-20 16:47:27 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
if (!candidate_topics?.length) {
|
2013-07-30 23:02:10 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
assert(guess !== undefined);
|
2022-01-27 14:39:19 +01:00
|
|
|
const topics = get_topic_suggestions_from_candidates({candidate_topics, guess});
|
2013-11-25 20:26:46 +01:00
|
|
|
|
2013-07-30 23:02:10 +02:00
|
|
|
// Just use alphabetical order. While recency and read/unreadness of
|
2016-10-28 19:07:25 +02:00
|
|
|
// topics do matter in some contexts, you can get that from the left sidebar,
|
2013-07-30 23:02:10 +02:00
|
|
|
// and I'm leaning toward high scannability for autocompletion. I also don't
|
|
|
|
// care about case.
|
|
|
|
topics.sort();
|
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
return topics.map((topic) => {
|
2020-07-20 22:18:43 +02:00
|
|
|
const topic_term = {operator: "topic", operand: topic, negated};
|
2023-12-22 00:26:14 +01:00
|
|
|
const terms = [...suggest_terms, topic_term];
|
|
|
|
return format_as_suggestion(terms);
|
2013-07-30 23:02:10 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_term_subset_suggestions(terms: NarrowTerm[]): Suggestion[] {
|
2013-07-30 23:02:10 +02:00
|
|
|
// For stream:a topic:b search:c, suggest:
|
|
|
|
// stream:a topic:b
|
|
|
|
// stream:a
|
2023-12-22 00:26:14 +01:00
|
|
|
if (terms.length < 1) {
|
2013-07-30 23:02:10 +02:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
const suggestions: Suggestion[] = [];
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
for (let i = terms.length - 1; i >= 1; i -= 1) {
|
2023-12-22 00:26:14 +01:00
|
|
|
const subset = terms.slice(0, i);
|
2018-06-01 15:59:17 +02:00
|
|
|
suggestions.push(format_as_suggestion(subset));
|
2013-07-30 23:02:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return suggestions;
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_special_filter_suggestions(
|
|
|
|
last: NarrowTerm,
|
|
|
|
terms: NarrowTerm[],
|
|
|
|
suggestions: (Suggestion & {incompatible_patterns: TermPattern[]})[],
|
|
|
|
): Suggestion[] {
|
|
|
|
const is_search_operand_negated = last.operator === "search" && last.operand.startsWith("-");
|
2018-05-20 16:30:06 +02:00
|
|
|
// Negating suggestions on is_search_operand_negated is required for
|
2023-12-22 00:26:14 +01:00
|
|
|
// suggesting negated terms.
|
2024-03-09 10:40:28 +01:00
|
|
|
if (last.negated === true || is_search_operand_negated) {
|
2020-07-02 01:39:34 +02:00
|
|
|
suggestions = suggestions.map((suggestion) => ({
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "-" + suggestion.search_string,
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "exclude " + suggestion.description_html,
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: suggestion.incompatible_patterns,
|
js: Convert _.map(a, …) to a.map(…).
And convert the corresponding function expressions to arrow style
while we’re here.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
recast.visit(ast, {
visitCallExpression(path) {
const { callee, arguments: args } = path.node;
if (
n.MemberExpression.check(callee) &&
!callee.computed &&
n.Identifier.check(callee.object) &&
callee.object.name === "_" &&
n.Identifier.check(callee.property) &&
callee.property.name === "map" &&
args.length === 2 &&
checkExpression(args[0]) &&
checkExpression(args[1])
) {
const [arr, fn] = args;
path.replace(
b.callExpression(b.memberExpression(arr, b.identifier("map")), [
n.FunctionExpression.check(fn) ||
n.ArrowFunctionExpression.check(fn)
? b.arrowFunctionExpression(
fn.params,
n.BlockStatement.check(fn.body) &&
fn.body.body.length === 1 &&
n.ReturnStatement.check(fn.body.body[0])
? fn.body.body[0].argument || b.identifier("undefined")
: fn.body
)
: fn,
])
);
changed = true;
}
this.traverse(path);
},
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 02:43:49 +01:00
|
|
|
}));
|
2018-05-20 16:30:06 +02:00
|
|
|
}
|
2018-05-18 10:10:49 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_string = Filter.unparse([last]).toLowerCase();
|
2020-07-02 01:39:34 +02:00
|
|
|
suggestions = suggestions.filter((s) => {
|
2024-03-15 10:41:52 +01:00
|
|
|
if (match_criteria(terms, s.incompatible_patterns)) {
|
2018-05-18 10:10:49 +02:00
|
|
|
return false;
|
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
if (last_string === "") {
|
2018-05-18 10:10:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
2018-05-20 16:30:06 +02:00
|
|
|
|
|
|
|
// returns the substring after the ":" symbol.
|
2020-10-07 09:41:22 +02:00
|
|
|
const suggestion_operand = s.search_string.slice(s.search_string.indexOf(":") + 1);
|
2018-05-20 16:30:06 +02:00
|
|
|
// e.g for `att` search query, `has:attachment` should be suggested.
|
2020-07-15 00:34:28 +02:00
|
|
|
const show_operator_suggestions =
|
|
|
|
last.operator === "search" && suggestion_operand.toLowerCase().startsWith(last_string);
|
|
|
|
return (
|
|
|
|
s.search_string.toLowerCase().startsWith(last_string) ||
|
|
|
|
show_operator_suggestions ||
|
2022-06-19 07:17:16 +02:00
|
|
|
s.description_html.toLowerCase().startsWith(last_string)
|
2020-07-15 00:34:28 +02:00
|
|
|
);
|
2018-05-18 10:10:49 +02:00
|
|
|
});
|
2024-03-09 10:40:28 +01:00
|
|
|
const filtered_suggestions = suggestions.map(({incompatible_patterns, ...s}) => s);
|
|
|
|
|
|
|
|
return filtered_suggestions;
|
2018-05-18 10:10:49 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_streams_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2019-11-02 00:06:25 +01:00
|
|
|
const suggestions = [
|
2019-08-13 20:20:36 +02:00
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "streams:public",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "All public streams in organization",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [
|
2023-04-07 14:03:34 +02:00
|
|
|
{operator: "is", operand: "dm"},
|
2024-04-05 19:41:15 +02:00
|
|
|
{operator: "channel"},
|
2023-04-19 17:35:32 +02:00
|
|
|
{operator: "dm-including"},
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2020-07-15 01:29:15 +02:00
|
|
|
{operator: "in"},
|
2024-04-12 13:45:46 +02:00
|
|
|
{operator: "channels"},
|
2019-08-13 20:20:36 +02:00
|
|
|
],
|
|
|
|
},
|
|
|
|
];
|
2023-12-22 00:26:14 +01:00
|
|
|
return get_special_filter_suggestions(last, terms, suggestions);
|
2019-08-13 20:20:36 +02:00
|
|
|
}
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2019-11-02 00:06:25 +01:00
|
|
|
const suggestions = [
|
2013-07-30 23:02:10 +02:00
|
|
|
{
|
2023-04-07 14:03:34 +02:00
|
|
|
search_string: "is:dm",
|
2023-01-24 19:49:56 +01:00
|
|
|
description_html: "direct messages",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [
|
2023-04-07 14:03:34 +02:00
|
|
|
{operator: "is", operand: "dm"},
|
2023-04-12 18:51:51 +02:00
|
|
|
{operator: "is", operand: "resolved"},
|
2024-04-05 19:41:15 +02:00
|
|
|
{operator: "channel"},
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2020-07-15 01:29:15 +02:00
|
|
|
{operator: "in"},
|
2017-06-03 04:32:25 +02:00
|
|
|
],
|
2013-07-30 23:02:10 +02:00
|
|
|
},
|
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "is:starred",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "starred messages",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "is", operand: "starred"}],
|
2013-07-30 23:02:10 +02:00
|
|
|
},
|
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "is:mentioned",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "@-mentions",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "is", operand: "mentioned"}],
|
2013-07-30 23:02:10 +02:00
|
|
|
},
|
2013-08-30 21:15:01 +02:00
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "is:alerted",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "alerted messages",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "is", operand: "alerted"}],
|
2013-08-30 21:15:01 +02:00
|
|
|
},
|
2017-06-19 00:33:24 +02:00
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "is:unread",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "unread messages",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "is", operand: "unread"}],
|
2017-06-19 00:33:24 +02:00
|
|
|
},
|
2021-07-13 10:03:26 +02:00
|
|
|
{
|
|
|
|
search_string: "is:resolved",
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html: "topics marked as resolved",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [
|
2023-04-12 18:51:51 +02:00
|
|
|
{operator: "is", operand: "resolved"},
|
2023-04-07 14:03:34 +02:00
|
|
|
{operator: "is", operand: "dm"},
|
2023-04-11 21:04:33 +02:00
|
|
|
{operator: "dm"},
|
2023-04-19 17:35:32 +02:00
|
|
|
{operator: "dm-including"},
|
2023-04-12 18:51:51 +02:00
|
|
|
],
|
2021-07-13 10:03:26 +02:00
|
|
|
},
|
2013-07-30 23:02:10 +02:00
|
|
|
];
|
2023-12-22 00:26:14 +01:00
|
|
|
return get_special_filter_suggestions(last, terms, suggestions);
|
2018-05-18 10:10:49 +02:00
|
|
|
}
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_has_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2019-11-02 00:06:25 +01:00
|
|
|
const suggestions = [
|
2018-05-18 10:10:49 +02:00
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "has:link",
|
2023-06-27 21:41:19 +02:00
|
|
|
description_html: "messages that contain links",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "has", operand: "link"}],
|
2018-05-18 10:10:49 +02:00
|
|
|
},
|
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "has:image",
|
2023-06-27 21:41:19 +02:00
|
|
|
description_html: "messages that contain images",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "has", operand: "image"}],
|
2018-05-18 10:10:49 +02:00
|
|
|
},
|
|
|
|
{
|
2020-07-15 01:29:15 +02:00
|
|
|
search_string: "has:attachment",
|
2023-06-27 21:41:19 +02:00
|
|
|
description_html: "messages that contain attachments",
|
2024-03-15 10:41:52 +01:00
|
|
|
incompatible_patterns: [{operator: "has", operand: "attachment"}],
|
2018-05-18 10:10:49 +02:00
|
|
|
},
|
|
|
|
];
|
2023-12-22 00:26:14 +01:00
|
|
|
return get_special_filter_suggestions(last, terms, suggestions);
|
2013-07-30 23:02:10 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_sent_by_me_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_string = Filter.unparse([last]).toLowerCase();
|
2024-03-09 10:40:28 +01:00
|
|
|
const negated =
|
|
|
|
last.negated === true || (last.operator === "search" && last.operand.startsWith("-"));
|
2020-07-15 01:29:15 +02:00
|
|
|
const negated_symbol = negated ? "-" : "";
|
|
|
|
const verb = negated ? "exclude " : "";
|
2019-11-02 00:06:25 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const sender_query = negated_symbol + "sender:" + people.my_current_email();
|
|
|
|
const from_query = negated_symbol + "from:" + people.my_current_email();
|
|
|
|
const sender_me_query = negated_symbol + "sender:me";
|
|
|
|
const from_me_query = negated_symbol + "from:me";
|
|
|
|
const sent_string = negated_symbol + "sent";
|
2022-06-19 07:17:16 +02:00
|
|
|
const description_html = verb + "sent by me";
|
2019-11-02 00:06:25 +01:00
|
|
|
|
2024-03-15 10:41:52 +01:00
|
|
|
const incompatible_patterns = [{operator: "sender"}, {operator: "from"}];
|
2017-01-11 19:20:31 +01:00
|
|
|
|
2024-03-15 10:41:52 +01:00
|
|
|
if (match_criteria(terms, incompatible_patterns)) {
|
2017-01-11 19:20:31 +01:00
|
|
|
return [];
|
2017-06-03 05:33:02 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
last.operator === "" ||
|
2020-01-28 15:26:02 +01:00
|
|
|
sender_query.startsWith(last_string) ||
|
|
|
|
sender_me_query.startsWith(last_string) ||
|
|
|
|
last_string === sent_string
|
|
|
|
) {
|
2017-01-11 19:20:31 +01:00
|
|
|
return [
|
|
|
|
{
|
|
|
|
search_string: sender_query,
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html,
|
2017-01-11 19:20:31 +01:00
|
|
|
},
|
|
|
|
];
|
2020-07-15 00:34:28 +02:00
|
|
|
} else if (from_query.startsWith(last_string) || from_me_query.startsWith(last_string)) {
|
2017-01-11 19:20:31 +01:00
|
|
|
return [
|
|
|
|
{
|
|
|
|
search_string: from_query,
|
2022-06-19 07:17:16 +02:00
|
|
|
description_html,
|
2017-01-11 19:20:31 +01:00
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_operator_suggestions(last: NarrowTerm): Suggestion[] {
|
2023-04-07 14:03:34 +02:00
|
|
|
// Suggest "is:dm" to anyone with "is:private" in their muscle memory
|
|
|
|
if (last.operator === "is" && common.phrase_match(last.operand, "private")) {
|
|
|
|
const is_dm = format_as_suggestion([
|
|
|
|
{operator: last.operator, operand: "dm", negated: last.negated},
|
|
|
|
]);
|
|
|
|
return [is_dm];
|
|
|
|
}
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
if (!(last.operator === "search")) {
|
2018-04-06 20:43:34 +02:00
|
|
|
return [];
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
let last_operand = last.operand;
|
2017-06-15 09:07:12 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let negated = false;
|
2020-01-28 15:26:02 +01:00
|
|
|
if (last_operand.startsWith("-")) {
|
2018-04-06 20:43:34 +02:00
|
|
|
negated = true;
|
2018-05-20 16:30:06 +02:00
|
|
|
last_operand = last_operand.slice(1);
|
2018-04-06 20:43:34 +02:00
|
|
|
}
|
2017-06-15 09:07:12 +02:00
|
|
|
|
2023-04-19 20:25:55 +02:00
|
|
|
let choices = ["stream", "topic", "dm", "dm-including", "sender", "near", "from", "pm-with"];
|
2020-07-02 01:39:34 +02:00
|
|
|
choices = choices.filter((choice) => common.phrase_match(last_operand, choice));
|
2017-06-15 09:07:12 +02:00
|
|
|
|
2020-07-02 01:39:34 +02:00
|
|
|
return choices.map((choice) => {
|
2023-04-11 21:04:33 +02:00
|
|
|
// Map results for "dm:" operator for users
|
|
|
|
// who have "pm-with" in their muscle memory.
|
|
|
|
if (choice === "pm-with") {
|
|
|
|
choice = "dm";
|
|
|
|
}
|
2020-07-20 22:18:43 +02:00
|
|
|
const op = [{operator: choice, operand: "", negated}];
|
2018-06-01 15:59:17 +02:00
|
|
|
return format_as_suggestion(op);
|
2018-04-06 20:43:34 +02:00
|
|
|
});
|
2017-06-15 09:07:12 +02:00
|
|
|
}
|
|
|
|
|
2020-07-23 02:09:54 +02:00
|
|
|
class Attacher {
|
2024-03-09 10:40:28 +01:00
|
|
|
result: Suggestion[] = [];
|
|
|
|
prev = new Set<string>();
|
|
|
|
base: Suggestion;
|
2020-07-23 02:09:54 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
constructor(base: Suggestion) {
|
2020-07-23 02:09:54 +02:00
|
|
|
this.base = base;
|
2019-12-25 18:36:16 +01:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
prepend_base(suggestion: Suggestion): void {
|
2022-06-19 07:17:16 +02:00
|
|
|
if (this.base && this.base.description_html.length > 0) {
|
2020-07-23 02:09:54 +02:00
|
|
|
suggestion.search_string = this.base.search_string + " " + suggestion.search_string;
|
2022-06-19 07:17:16 +02:00
|
|
|
suggestion.description_html =
|
|
|
|
this.base.description_html + ", " + suggestion.description_html;
|
2019-12-25 18:36:16 +01:00
|
|
|
}
|
2020-07-23 02:09:54 +02:00
|
|
|
}
|
2019-12-25 18:36:16 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
push(suggestion: Suggestion): void {
|
2020-07-23 02:09:54 +02:00
|
|
|
if (!this.prev.has(suggestion.search_string)) {
|
|
|
|
this.prev.add(suggestion.search_string);
|
|
|
|
this.result.push(suggestion);
|
|
|
|
}
|
|
|
|
}
|
2019-12-25 18:36:16 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
push_many(suggestions: Suggestion[]): void {
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const suggestion of suggestions) {
|
2020-07-23 02:09:54 +02:00
|
|
|
this.push(suggestion);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2020-07-23 02:09:54 +02:00
|
|
|
}
|
2019-12-25 18:36:16 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
attach_many(suggestions: Suggestion[]): void {
|
2020-07-23 02:09:54 +02:00
|
|
|
for (const suggestion of suggestions) {
|
|
|
|
this.prepend_base(suggestion);
|
|
|
|
this.push(suggestion);
|
|
|
|
}
|
|
|
|
}
|
2017-06-03 04:32:25 +02:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
export function get_search_result(query: string): Suggestion[] {
|
|
|
|
let suggestion: Suggestion;
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2023-12-22 00:26:14 +01:00
|
|
|
// search_terms correspond to the terms for the query in the input.
|
2023-05-19 00:55:49 +02:00
|
|
|
// This includes the entire query entered in the searchbox.
|
2023-12-22 00:26:14 +01:00
|
|
|
// terms correspond to the terms for the entire query entered in the searchbox.
|
|
|
|
const search_terms = Filter.parse(query);
|
2024-03-09 10:40:28 +01:00
|
|
|
|
|
|
|
let last: NarrowTerm = {operator: "", operand: "", negated: false};
|
2023-12-22 00:26:14 +01:00
|
|
|
if (search_terms.length > 0) {
|
2024-03-09 10:40:28 +01:00
|
|
|
last = search_terms.at(-1)!;
|
2017-06-03 04:32:25 +02:00
|
|
|
}
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2023-04-19 20:25:55 +02:00
|
|
|
const person_suggestion_ops = ["sender", "dm", "dm-including", "from", "pm-with"];
|
2018-05-25 17:03:13 +02:00
|
|
|
|
2020-06-01 12:44:23 +02:00
|
|
|
// Handle spaces in person name in new suggestions only. Checks if the last operator is 'search'
|
2023-12-22 00:26:14 +01:00
|
|
|
// and the second last operator in search_terms is one out of person_suggestion_ops.
|
2018-05-25 17:03:13 +02:00
|
|
|
// e.g for `sender:Ted sm`, initially last = {operator: 'search', operand: 'sm'....}
|
|
|
|
// and second last is {operator: 'sender', operand: 'sm'....}. If the second last operand
|
2023-12-22 00:26:14 +01:00
|
|
|
// is an email of a user, both of these terms remain unchanged. Otherwise search operator
|
2018-05-25 17:03:13 +02:00
|
|
|
// will be deleted and new last will become {operator:'sender', operand: 'Ted sm`....}.
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
2023-12-22 00:26:14 +01:00
|
|
|
search_terms.length > 1 &&
|
2020-07-15 01:29:15 +02:00
|
|
|
last.operator === "search" &&
|
2024-03-09 10:40:28 +01:00
|
|
|
person_suggestion_ops.includes(search_terms.at(-2)!.operator)
|
2020-07-15 00:34:28 +02:00
|
|
|
) {
|
2024-03-09 10:40:28 +01:00
|
|
|
const person_op = search_terms.at(-2)!;
|
2019-06-27 19:16:55 +02:00
|
|
|
if (!people.reply_to_to_user_ids_string(person_op.operand)) {
|
2018-05-25 17:03:13 +02:00
|
|
|
last = {
|
|
|
|
operator: person_op.operator,
|
2020-07-15 01:29:15 +02:00
|
|
|
operand: person_op.operand + " " + last.operand,
|
2018-05-25 17:03:13 +02:00
|
|
|
negated: person_op.negated,
|
|
|
|
};
|
2023-12-22 00:26:14 +01:00
|
|
|
search_terms.splice(-2);
|
|
|
|
search_terms.push(last);
|
2018-05-25 17:03:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-22 00:26:14 +01:00
|
|
|
const base = get_default_suggestion(search_terms.slice(0, -1));
|
2020-07-23 02:09:54 +02:00
|
|
|
const attacher = new Attacher(base);
|
2019-12-25 18:36:16 +01:00
|
|
|
|
2017-06-03 04:32:25 +02:00
|
|
|
// Display the default first
|
2018-05-22 14:37:51 +02:00
|
|
|
// `has` and `is` operators work only on predefined categories. Default suggestion
|
2023-06-27 21:41:19 +02:00
|
|
|
// is not displayed in that case. e.g. `messages that contain abc` as
|
2023-12-14 08:28:24 +01:00
|
|
|
// a suggestion for `has:abc` does not make sense.
|
2023-06-30 01:47:15 +02:00
|
|
|
if (last.operator === "search") {
|
|
|
|
suggestion = {
|
|
|
|
search_string: last.operand,
|
|
|
|
description_html: `search for <strong>${Handlebars.Utils.escapeExpression(
|
|
|
|
last.operand,
|
|
|
|
)}</strong>`,
|
|
|
|
};
|
|
|
|
attacher.prepend_base(suggestion);
|
|
|
|
attacher.push(suggestion);
|
|
|
|
} else if (last.operator !== "" && last.operator !== "has" && last.operator !== "is") {
|
2023-12-22 00:26:14 +01:00
|
|
|
suggestion = get_default_suggestion(search_terms);
|
2020-06-13 16:34:23 +02:00
|
|
|
attacher.push(suggestion);
|
2017-06-03 04:32:25 +02:00
|
|
|
}
|
|
|
|
|
2020-01-02 12:53:11 +01:00
|
|
|
// only make one people_getter to avoid duplicate work
|
|
|
|
const people_getter = make_people_getter(last);
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
function get_people(
|
|
|
|
flavor: string,
|
|
|
|
): (last: NarrowTerm, base_terms: NarrowTerm[]) => Suggestion[] {
|
|
|
|
return function (last: NarrowTerm, base_terms: NarrowTerm[]): Suggestion[] {
|
2023-12-22 00:26:14 +01:00
|
|
|
return get_person_suggestions(people_getter, last, base_terms, flavor);
|
2020-01-07 14:21:35 +01:00
|
|
|
};
|
|
|
|
}
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2022-02-10 11:55:53 +01:00
|
|
|
// Remember to update the spectator list when changing this.
|
|
|
|
let filterers = [
|
2020-01-07 14:21:35 +01:00
|
|
|
get_streams_filter_suggestions,
|
|
|
|
get_is_filter_suggestions,
|
|
|
|
get_sent_by_me_suggestions,
|
|
|
|
get_stream_suggestions,
|
2020-07-15 01:29:15 +02:00
|
|
|
get_people("sender"),
|
2023-04-11 21:04:33 +02:00
|
|
|
get_people("dm"),
|
2023-04-19 17:35:32 +02:00
|
|
|
get_people("dm-including"),
|
2020-07-15 01:29:15 +02:00
|
|
|
get_people("from"),
|
2020-01-07 14:21:35 +01:00
|
|
|
get_group_suggestions,
|
|
|
|
get_topic_suggestions,
|
|
|
|
get_operator_suggestions,
|
|
|
|
get_has_filter_suggestions,
|
|
|
|
];
|
2013-07-30 23:02:10 +02:00
|
|
|
|
2022-02-10 11:55:53 +01:00
|
|
|
if (page_params.is_spectator) {
|
|
|
|
filterers = [
|
|
|
|
get_stream_suggestions,
|
|
|
|
get_people("sender"),
|
|
|
|
get_people("from"),
|
|
|
|
get_topic_suggestions,
|
|
|
|
get_operator_suggestions,
|
|
|
|
get_has_filter_suggestions,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2023-12-22 00:26:14 +01:00
|
|
|
const base_terms = search_terms.slice(0, -1);
|
2021-02-28 01:04:35 +01:00
|
|
|
const max_items = max_num_of_search_results;
|
2017-06-15 09:07:12 +02:00
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const filterer of filterers) {
|
2020-01-07 14:21:35 +01:00
|
|
|
if (attacher.result.length < max_items) {
|
2023-12-22 00:26:14 +01:00
|
|
|
const suggestions = filterer(last, base_terms);
|
2020-01-07 14:21:35 +01:00
|
|
|
attacher.attach_many(suggestions);
|
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2017-06-05 09:54:56 +02:00
|
|
|
|
2023-05-19 00:55:49 +02:00
|
|
|
if (attacher.result.length < max_items) {
|
2023-12-22 00:26:14 +01:00
|
|
|
const subset_suggestions = get_term_subset_suggestions(search_terms);
|
2023-03-02 02:17:23 +01:00
|
|
|
attacher.push_many(subset_suggestions);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2018-07-14 13:01:21 +02:00
|
|
|
|
2019-12-25 16:58:11 +01:00
|
|
|
return attacher.result.slice(0, max_items);
|
2021-02-28 01:04:35 +01:00
|
|
|
}
|
2019-12-25 15:36:51 +01:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
export function get_suggestions(query: string): {
|
|
|
|
strings: string[];
|
|
|
|
lookup_table: Map<string, Suggestion>;
|
|
|
|
} {
|
2023-06-29 21:36:02 +02:00
|
|
|
const result = get_search_result(query);
|
2021-02-28 01:04:35 +01:00
|
|
|
return finalize_search_result(result);
|
|
|
|
}
|
2018-07-14 13:01:21 +02:00
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
export function finalize_search_result(result: Suggestion[]): {
|
|
|
|
strings: string[];
|
|
|
|
lookup_table: Map<string, Suggestion>;
|
|
|
|
} {
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const sug of result) {
|
2022-06-19 07:17:16 +02:00
|
|
|
const first = sug.description_html.charAt(0).toUpperCase();
|
|
|
|
sug.description_html = first + sug.description_html.slice(1);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2018-07-14 13:01:21 +02:00
|
|
|
|
2019-12-25 18:36:16 +01:00
|
|
|
// Typeahead expects us to give it strings, not objects,
|
|
|
|
// so we maintain our own hash back to our objects
|
2024-03-09 10:40:28 +01:00
|
|
|
const lookup_table = new Map<string, Suggestion>();
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const obj of result) {
|
2020-02-12 06:58:20 +01:00
|
|
|
lookup_table.set(obj.search_string, obj);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
|
|
|
|
2024-03-09 10:40:28 +01:00
|
|
|
const strings = result.map((obj: Suggestion) => obj.search_string);
|
2018-07-14 13:01:21 +02:00
|
|
|
return {
|
2020-07-20 22:18:43 +02:00
|
|
|
strings,
|
|
|
|
lookup_table,
|
2018-07-14 13:01:21 +02:00
|
|
|
};
|
2021-02-28 01:04:35 +01:00
|
|
|
}
|