Commit Graph

99 Commits

Author SHA1 Message Date
Anders Kaseorg 89aa3155a9 node_tests: Don’t read from most deprecated global variables.
We still need to write to these globals with set_global because the
code being tested reads from them, but the tests themselves should
never need to read from them.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-02-10 07:40:22 -08:00
Anders Kaseorg ce42d1194d node_tests: Skip unnecessary explicit uses of global.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 07:14:00 -05:00
Anders Kaseorg 21d432e12c zjsunit: Deglobalize run_test.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 07:14:00 -05:00
Anders Kaseorg 425f1789e2 zjsunit: Deglobalize namespace.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 07:14:00 -05:00
Anders Kaseorg 7b03d48798 zjsunit: Deglobalize assert.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 07:14:00 -05:00
Anders Kaseorg d72423ef21 eslint: Replace empty-returns with consistent-return.
Instead of prohibiting ‘return undefined’ (#8669), we require that a
function must return an explicit value always or never.  This prevents
you from forgetting to return a value in some cases.  It will also be
important for TypeScript, which distinguishes between undefined and
void.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-25 15:17:59 -07:00
Priyank Patel b7998d3160 js: Purge people module from window. 2020-09-01 19:55:58 -07:00
Anders Kaseorg 6ec808b8df js: Add "use strict" directive to CommonJS files.
ES and TypeScript modules are strict by default and don’t need this
directive.  ESLint will remind us to add it to new CommonJS files and
remove it from ES and TypeScript modules.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-31 22:09:46 -07:00
Anders Kaseorg 226598edc4 js: Use Handlebars as a module.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-29 11:17:24 -07:00
Anders Kaseorg 96dcc0ce6e js: Use ES6 object literal shorthand syntax.
Generated by ESLint.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-21 12:42:22 -07:00
Anders Kaseorg b65d2e063d js: Reformat with Prettier.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-17 14:31:25 -07:00
Anders Kaseorg 8046b6477a js: Remove extra consecutive spaces.
Prettier would do this anyway, but it’s separated out for a more
reviewable diff.  Generated by ESLint.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-17 14:31:25 -07:00
Anders Kaseorg f3726db89a js: Normalize strings to double quotes.
Prettier would do this anyway, but it’s separated out for a more
reviewable diff.  Generated by ESLint.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-17 14:31:24 -07:00
Anders Kaseorg 615b7fcc2c eslint: Enable arrow-body-style.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-03 16:55:50 -07:00
Ryan Rehman 21cdc26193 search: Support multiple search pills creation.
This reverts part of b0d632577f.
The problem was that multiple queries were combined as a single
search pill. And since we create the pills then narrow / search,
we added a comma seperator between them for the typeahead lookups
as required by the logic in `input_pill.js`.

This however introduced a new bug where the search suggestions
were incorrect as the typeahead lookup table wasn't updated, so
every time an item from the type ahead was selected it updated
the input string with an invalid operator.

Thus to resolve the first problem, we follow a simpler approach
by extracting all operators from the search string using our
`Filter.parse` logic and next add the pills, one by one.
2020-06-19 16:18:52 -07:00
Ryan Rehman 9c5d9c961c search: Parse search string correctly.
This is the exact same bug as observed in
02ab48a61e.

The bug is in the way we invoke `Filter.parse`.
`Filter.parse` returns a list of operators which
can contain only one 'search' term at max.
All strings with the 'search' operator present
in the query are combined to form this 'search'
term.

However on concatenating two filters we may get
two terms containing the 'search' operator. This
will lead to the search suggestions getting
generated based on only the last 'search' operator
term instead of all the terms having the 'search'
operator.

This is evident from the test change as suggestions
should be based on "s stream:of" but instead they
were based on just the latest query.
2020-06-15 20:16:40 -07:00
Ryan Rehman b0d632577f search pills: Update multiple pills creation event behaviour.
If typeahead is used, this adds comma separated search queries
so that multiple search pills don't get combined as one and the
search behaviour remains same as search_pills_enabled = False case.

If typeahead is not used, this prevent the typing of a single comma
after the pill gets created.
2020-06-05 17:16:20 -07:00
clarammdantas aae7c79c00 people.js: Rename add() to add_active_user(). 2020-05-26 21:41:54 -07:00
Steve Howell e040721090 refactor: Extract huddle_data.js.
This makes it so that search_suggestion.js
does not depend on activity.js.

That dependency hasn't really been "elegant"
for quite some time, but it will become particularly
unnecessary when we go to remove the "Group PMs"
section from the right sidebar.

This commit introduces a temporary wart
where we have these two functions with the
same name in a sort of unnecessarily
complicated code stack:

    activity.process_loaded_messages
    huddle_data.process_loaded_messages

But we will eliminate the former function
very soon, and our message-related codepaths
will just call the `huddle_data` version
directly.

TESTING NOTES:

Now that `huddle_data` is a tiny leaf
module, it's super easy to just use the
real implementation of what was formerly
called `activity.get_huddles()` (and is
now in `huddle_data`).

When I first wrote this commit, introducing
the real implementation of `get_huddles` exposed
some bugs that I fixed in the immediately
prior commits to this.

When the tests were originally written,
I believe `activity.js` had some annoying
`jQuery` dependencies that made it hard
to unit test against.  We've slimmed it over
time to be mostly just a "controller" module.
But even in its current state it would have
been a bit of a bloated dependency.

The other friction for using the actual
version of `get_huddles` was setting up
the message data, but that's pretty minor.
2020-05-26 21:26:11 -07:00
Steve Howell ede709f75c bug fix: Fix sorting for group-pm edge cases.
If you have a group PM where some users have
three-digit user_ids and some with four-digit
user_ids (or similar), a huddle could effectively
be ignored when determining the order of
search search suggestions.

Basically, we need a way to canonically sort
user_ids in "huddle" strings, and it's somewhat
arbitrary whether you sort lexically or sort
numerically, but you do need to be consistent
about it.

And JS is not exactly helpful here:

    > [99, 101].sort()
    [ 101, 99 ]

This is a pretty obscure bug with pretty low
user-facing consequences, and it was never
reported to us as far as I know, but the fix
here is pretty straightforward.

We have had similar bugs of slightly more consequence
in the past.  The reason this bug has shown
up multiple times in our codebase is that every
component that deals with huddles has slightly
different forces that determine how it wants
to serialize the huddle.  It's just one of those
annoying things.  Plus, bugs with group PMs
do tend to escape detection, since most people
spend most of their time either on streams
or in 1:1 PMs.
2020-05-26 21:26:11 -07:00
Steve Howell ea0810e13f search tests: Create users up front.
This creates a little bit of noise in some
tests where we don't care about users, but
it's worth avoiding confusion about which
users exist at which time.  Also the noisy
aspects here may actually catch regressions.

Finally, if the noise gets annoying, we can
do things like rename "Ted" not to collide
with the "Test" stream.
2020-05-26 10:47:59 -04:00
Steve Howell 0cc5a8e185 search tests: Add a "myself" user.
Using "bob" as the current user was a bad
choice, as our convention is to use "me" or
"myself" or "alice" for the current user.

It also particularly complicated the tests
around Group PMs.

Now we have both "bob" and "myself", which
makes the intentions of the tests a little
more clear.
2020-05-26 10:47:59 -04:00
Hashir Sarwar ee0d4541b4 topic_data: Rename `topic_data` module to `stream_topic_history`.
`stream_topic_history` is a more appropriate name as this
module will contain information about last message of a
stream in upcoming commits. Function and variable names
are changed accordingly like:

* topic_history() -> per_stream_history()
* get_recent_names() -> get_recent_topic_names()
* name -> topic_name
2020-04-16 20:11:04 -07:00
Steve Howell 2788ebdde7 minor: Clean up blank lines in tests. 2020-04-03 12:56:49 -04:00
Steve Howell 979dcfe85b refactor: Extract settings_data.py.
This extracts a new module with three
functions, which we will test with 100%
line coverage:

    - show_email
    - email_for_user_settings
    - get_time_preferences

The first two break several dependencies
in the codebase on `settings_org.js`.  The
`get_time_preferences` breaks an annoying
dependency on `page_params` within people.

The module is pretty cohesive, in terms that
all three functions are just light wrappers
around `page_params` and/or `settings_config`.

Now all the modules that want to call show_email()
only have to require `settings_data`, instead of
having a dependency on the much heavier
`settings_org.js` module.

I also make some of the unit tests here be more
full-stack, where instead of stubbing show_email,
I basically just toggle `page_params.is_admin`.
2020-02-28 17:11:24 -08:00
Steve Howell b994889315 node tests: Just set i18n every time.
Explicitly stubbing i18n in 48 different files
is mostly busy work at this point, and it doesn't
provide much signal, since often it's invoked
only to satisfy transitive dependencies.
2020-02-28 17:11:24 -08:00
Steve Howell 9ab07d1038 util.js: Remove util from window.
We now treat util like a leaf module and
use "require" to import it everywhere it's used.

An earlier version of this commit moved
util into our "shared" library, but we
decided to wait on that.  Once we're ready
to do that, we should only need to do a
simple search/replace on various
require/zrequire statements plus a small
tweak to one of the custom linter checks.

It turns out we don't really need util.js
for our most immediate code-sharing goal,
which is to reuse our markdown code on
mobile.  There's a little bit of cleanup
still remaining to break the dependency,
but it's minor.

The util module still calls the global
blueslip module in one place, but that
code is about to be removed in the next
few commits.

I am pretty confident that once we start
sharing things like the typeahead code
more aggressively, we'll start having
dependencies on util.  The module is barely
more than 300 lines long, so we'll probably
just move the whole thing into shared
rather than break it apart.  Also, we
can continue to nibble away at the
cruftier parts of the module.
2020-02-15 12:20:20 -08:00
Anders Kaseorg 7c6ec51e5d search_suggestions: Convert lookup_table from object to Map.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-12 10:39:01 -08:00
Anders Kaseorg 719546641f 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-10 14:08:12 -08:00
Anders Kaseorg 11b5d80800 tests: Fix more undefined mocks.
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2020-02-10 14:08:12 -08:00
Anders Kaseorg 02511bff1c 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-07 14:09:47 -08:00
Ryan Rehman 174b2abcfd settings: Migrate to stream_post_policy structure.
This commit includes a new `stream_post_policy` setting,
by replacing the `is_announcement_only` field from the Stream model,
which is done by mirroring the structure of the existing
`create_stream_policy`.

It includes the necessary schema and database migrations to migrate
the is_announcement_only boolean field to stream_post_policy,
a smallPositiveInteger field similar to many other settings.

This change is done to allow organization administrators to restrict
new members from creating and posting to a stream. However, this does
not affect admins who are new members.

With many tweaks by tabbott to documentation under /help, etc.

Fixes #13616.
2020-02-04 17:08:08 -08:00
Steve Howell 4a03f91daa search: Retrofit recent changes to pills code.
This change makes these two functions more alike:

    - get_search_result
    - get_search_result_legacy

To test the UI modify zerver/views/home.py by
replacing `settings.SEARCH_PILLS_ENABLED` with
`True`.  I only did a quick sanity check, since
any bugs with the new system are more likely due
to bitrot than any changes I have made here.

The history is this:

    Tim cloned the code (before the smaller
    helpers were extracted):

        db4f6e278f

    In 8b153f6452
    Shubham removed get_operator_subset_suggestions but
    accidentally left a `concat` statement in that got
    misapplied to the previous suggestions:

    -    suggestions = get_operator_subset_suggestions(operators);
         result = result.concat(suggestions);

    The error there was carried over in some recent changes,
    but this commit fixes that strangeness.

    In 73e4f3b3fa
    Shubham made this change, which makes sense only for
    pills, and this code remains intact.

        -    if (operators.length > 0) {
        -        last = operators.slice(-1)[0];
        +    if (query_operators.length > 0) {
        +        last = query_operators.slice(-1)[0];
        +    } else {
        +        // If query_operators = [] then last will remain
        +        // {operator: '', operand: '', negated: false}; from above.
        +        // `last` has not yet been added to operators/query_operators.
        +        // The code below adds last to operators/query_operators
        +        operators.push(last);
        +        query_operators.push(last);
             }

    Mohit made a couple changes to both old and new.

    Anders made a couple non-substantive changes related to
    the ES6 migration.

    Steve (me) made several structural changes to the code.  For
    some of them I only changed the legacy code, not the pills
    code.  I didn't fix Shubham's mistake until this change.

Now the two functions should look similar except in the places
where they are intentionally different.  I also added a comment
explaining the get_operator_subset_suggestions difference.

Fixes #13609
2020-01-08 14:02:34 -08:00
Steve Howell 26168eaa98 search: Optimize search bar suggestions for large realms.
We only ever show 3 or 4 people in search suggestions
(possibly w/a couple variations, like pm-with/sender/etc.),
so we can try to search a smaller subset of people
before going through the entire realm.

We use message_store.user_ids() for this, since you
typically want to search messages for people that
have sent messages recently, and we already sort
based on PM conversations.
2020-01-04 12:58:00 -08:00
Steve Howell 32a1ef20d1 minor: Extract helper for search tests. 2019-12-28 05:43:04 -05:00
Anders Kaseorg 28f3dfa284 js: Automatically convert var to let and const in most files.
This commit was originally automatically generated using `tools/lint
--only=eslint --fix`.  It was then modified by tabbott to contain only
changes to a set of files that are unlikely to result in significant
merge conflicts with any open pull request, excluding about 20 files.
His plan is to merge the remaining changes with more precise care,
potentially involving merging parts of conflicting pull requests
before running the `eslint --fix` operation.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2019-11-03 12:42:39 -08:00
Mohit Gupta e5482adec0 search: Add streams:public to search entire history of public streams.
Add ability to search entire message history of all public streams at
once. It includes all subscibed, non subscribed public streams messages
and even historical public stream messages sent before user had joined
an organization or stream.

Fixes #8859.
2019-08-22 13:40:49 -07:00
Anders Kaseorg fb3fac1d96 zjsunit: Add make_handlebars abstraction.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2019-07-12 21:11:14 -07:00
Mohit Gupta 487861554f search: Fix searching and search suggestion in Group PM.
Fix the .get_suggestions and .get_suggestions_legacy
to correctly handle search terms in group PM and treat
it as search term by not concatenating it at end of pm-with
email list operand.
2019-06-28 12:45:37 -07:00
Pragati Agrawal 69ec96b63e search suggestion: Hide email under hidden email-address-visibility cases.
This commit hides the email address from the search bar for the email
hidden cases.
2019-05-30 16:22:54 -07:00
Shubham Padia 73e4f3b3fa search: Do not display `All messages` suggestion if bar not empty.
Previously `All messages` was displayed irrespective of the existing
pills. Now the suggestion is displayed only if no pills are present
2018-07-23 11:29:10 -07:00
Shubham Padia 8b153f6452 search: Validate suggestion against existing operators.
After adding search pills, suggestions were based only on the
current input and no validation against the existing pills was done.
operator_subset_suggestions have been removed. Default suggestions
for base_operators have also been removed.
Handle multiple operators:
if `is:starred stream:Ver` was typed without selecting the typeahead
or pressing enter in between i.e search pill for is:starred has not yet
been added, then the description of `is:starred` will act as a prefix
in every suggestion.
Also makes changes re-enabling person suggestions for names with spaces.
2018-07-23 11:29:10 -07:00
Tim Abbott 5c01ae87a0 search: Duplicate search node tests for pills.
This forks off search_legacy.js and search_suggestion_legacy.js so
that we can continue running automated tests against the legacy search
code while we develop the input pills feature.
2018-07-23 11:29:10 -07:00
Shubham Padia 1364971a17 search: Extract phrase_match to common.js.
Extracts phrase_match to common.js so it can be used by other
components like integrations search.
2018-06-26 20:38:47 -04:00
Shubham Padia 5d750a5d44 search: Respect spaces in person name in new suggestions only.
Fixes #6515.
New suggestions for `sender:King ha` will respect spaces and the new
suggestion will be `Sent by King Hamlet <email>` instead of `Sent by King,
search for ha`. But if first term of sender operand is a valid user email,
tokens will be seperated by spaces. e.g `sender:hamlet@zulip.com abc`
will show `Sent by King Hamlet <email>, search for abc`.
2018-05-25 09:03:27 -07:00
Shubham Padia e5b3fb332b search: Do not show default suggestion for `is` operator.
Fixes #9492.
Default suggestion e.g `abc messages` as a suggestion for `is:abc`
is not shown in a new suggestion. But if the is operator is already
present before any other operator, the default message text will be
used. e.g `is:abc sender:abc@zulipchat.com` will have all the suggestions
with the prefix `abc messages, sent by abc@zulipchat.com`.
2018-05-22 18:16:02 +05:30
Shubham Padia 4a78c00397 search: Rank "messages sent by" above "PMs with".
Fixes #9313.
2018-05-21 22:38:57 -07:00
Shubham Padia 17d4908667 search: Show negated suggestions for sent by me filters. 2018-05-21 22:38:14 -07:00
Shubham Padia d5976e9501 search: Show negated suggestions for stream filters.
Fixes #9461.
Adds negated suggestions for stream filters when the query is negated
which were previously being returned empty.
2018-05-21 22:38:14 -07:00
Shubham Padia 3d545abf9d search: Remove redundant get_containing_suggestions function.
`get_containing_suggestions` was used to get the operand suggestions
for the `has` operator. `get_special_filter_suggestions` is now used
to get both the operand and operator suggestions for `has`.
2018-05-21 22:38:14 -07:00