text: var composebox_typeahead = (function () {
  
  //************************************
  // AN IMPORTANT NOTE ABOUT TYPEAHEADS
  //************************************
  // They do not do any HTML escaping, at all.
  // And your input to them is rendered as though it were HTML by
  // the default highlighter.
  //
  // So if you are not using trusted input, you MUST use the a
  // highlighter that escapes (i.e. one that calls
  // typeahead_helper.highlight_with_escaping).
  
  var exports = {};
  
  var seen_topics = new Dict();
  
  exports.add_topic = function (uc_stream, uc_topic) {
      // For Denmark/FooBar, we set
      // seen_topics['denmark']['foobar'] to 'FooBar',
      // where seen_topics is a Dict of Dicts
      var stream = uc_stream.toLowerCase();
      var topic = uc_topic.toLowerCase();
  
      if (! seen_topics.has(stream)) {
          seen_topics.set(stream, new Dict());
      }
      var topic_dict = seen_topics.get(stream);
      if (! topic_dict.has(topic)) {
          topic_dict.set(topic, uc_topic);
      }
  };
  
  exports.topics_seen_for = function (stream) {
      stream = stream.toLowerCase();
      if (seen_topics.has(stream)) {
          return seen_topics.get(stream).values().sort();
      }
      return [];
  };
  
  function query_matches_language(query, lang) {
      query = query.toLowerCase();
      return lang.indexOf(query) !== -1;
  }
  
  // This function attempts to match a query with source's attributes.
  // * query is the user-entered search query
  // * Source is the object we're matching from, e.g. a user object
  // * match_attrs are the values associated with the target object that
  // the entered string might be trying to match, e.g. for a user
  // account, there might be 2 attrs: their full name and their email.
  // * split_char is the separator for this syntax (e.g. ' ').
  function query_matches_source_attrs(query, source, match_attrs, split_char) {
      // If query doesn't contain a separator, we just want an exact
      // match where query is a substring of one of the target characers.
      return _.any(match_attrs, function (attr) {
          var source_str = source[attr].toLowerCase();
          if (query.indexOf(split_char) > 0) {
              // If there's a whitespace character in the query, then we
              // require a perfect prefix match (e.g. for 'ab cd ef',
              // query needs to be e.g. 'ab c', not 'cd ef' or 'b cd
              // ef', etc.).
              var queries = query.split(split_char);
              var sources = source_str.split(split_char);
              var i;
  
              for (i = 0; i < queries.length - 1; i += 1) {
                  if (sources[i] !== queries[i]) {
                      return false;
                  }
              }
  
              // This block is effectively a final iteration of the last
              // loop.  What differs is that for the last word, a
              // partial match at the beginning of the word is OK.
              if (sources[i] === undefined) {
                  return false;
              }
              return sources[i].indexOf(queries[i]) === 0;
          }
  
          // For a single token, the match can be anywhere in the string.
          return source_str.indexOf(query) !== -1;
      });
  }
  
  function query_matches_person(query, person) {
      // Case-insensitive.
      query = query.toLowerCase();
      return query_matches_source_attrs(query, person, ["full_name", "email"], " ");
  }
  
  function query_matches_user_group_or_stream(query, user_group_or_stream) {
      // Case-insensitive.
      query = query.toLowerCase();
      return query_matches_source_attrs(query, user_group_or_stream, ["name", "description"], " ");
  }
  
  // Case-insensitive
  function query_matches_emoji(query, emoji) {
      // replaces spaces with underscores
      query = query.toLowerCase();
      query = query.split(" ").join("_");
      return query_matches_source_attrs(query, emoji, ["emoji_name"], "_");
  }
  
  // nextFocus is set on a keydown event to indicate where we should focus on keyup.
  // We can't focus at the time of keydown because we need to wait for typeahead.
  // And we can't compute where to focus at the time of keyup because only the keydown
  // has reliable information about whether it was a tab or a shift+tab.
  var nextFocus = false;
  
  function handle_keydown(e) {
      var code = e.keyCode || e.which;
  
      if (code === 13 || (code === 9 && !e.shiftKey)) { // Enter key or tab key
          if (e.target.id === "stream" || e.target.id === "subject" || e.target.id === "private_message_recipient") {
              // For enter, prevent the form from submitting
              // For tab, prevent the focus from changing again
              e.preventDefault();
          }
  
          // In the compose_textarea box, preventDefault() for tab but not for enter
          if (e.target.id === 'compose-textarea' && code !== 13) {
              e.preventDefault();
          }
  
          if (e.target.id === "stream") {
              nextFocus = "subject";
          } else if (e.target.id === "subject") {
              if (code === 13) {
                  e.preventDefault();
              }
              nextFocus = 'compose-textarea';
          } else if (e.target.id === "private_message_recipient") {
              nextFocus = 'compose-textarea';
          } else if (e.target.id === 'compose-textarea') {
              if (code === 13) {
                  nextFocus = false;
              } else {
                  nextFocus = "compose-send-button";
              }
          } else {
              nextFocus = false;
          }
  
          // If no typeaheads are shown...
          if (!($("#subject").data().typeahead.shown ||
                $("#stream").data().typeahead.shown ||
                $("#private_message_recipient").data().typeahead.shown ||
                $("#compose-textarea").data().typeahead.shown)) {
  
              // If no typeaheads are shown and the user is tabbing from the message content box,
              // then there's no need to wait and we can change the focus right away.
              // Without this code to change the focus right away, if the user presses enter
              // before they fully release the tab key, the tab will be lost.  Note that we don't
              // want to change focus right away in the private_message_recipient box since it
              // takes the typeaheads a little time to open after the user finishes typing, which
              // can lead to the focus moving without the autocomplete having a chance to happen.
              if (nextFocus) {
                  ui_util.focus_on(nextFocus);
                  nextFocus = false;
              }
  
              if (e.target.id === 'compose-textarea' && code === 13) {
                  var has_non_shift_modifier_key = e.ctrlKey || e.metaKey || e.altKey;
                  var has_modifier_key = e.shiftKey || has_non_shift_modifier_key;
                  var this_enter_sends;
                  if (page_params.enter_sends) {
                      // With the enter_sends setting, we should send
                      // the message unless the user was holding a
                      // modifier key.
                      this_enter_sends = !has_modifier_key;
                  } else {
                      // If enter_sends is not enabled, just hitting
                      // enter should add a newline, but with a
                      // non-shift modifier key held down, we should
                      // send.  With shift, we shouldn't, because
                      // shift+enter to get a newline is a common
                      // keyboard habit for folks for dealing with other
                      // chat products where enter-always-sends.
                      this_enter_sends = has_non_shift_modifier_key;
                  }
                  if (this_enter_sends) {
                      e.preventDefault();
                      if ($("#compose-send-button").attr('disabled') !== "disabled") {
                          $("#compose-send-button").attr('disabled', 'disabled');
                          compose.finish();
                      }
                      return;
                  }
  
                  // Since this enter doesn't send, we just want to do
                  // the browser's default behavior for the "enter" key.
                  // Letting the browser handle it works great if the
                  // key actually pressed was enter or shift-enter.
  
                  // But the default browser behavior for ctrl/alt/meta
                  // + enter is to do nothing, so we need to emulate
                  // the browser behavior for "enter" in those cases.
                  //
                  // We do this using caret and range from jquery-caret.
                  if (has_non_shift_modifier_key) {
                      var textarea = $("#compose-textarea");
  
                      // To properly emulate browser "enter", if the
                      // user had selected something in the compose box,
                      // we need those characters to be cleared.
                      var range = textarea.range();
                      if (range.length > 0) {
                          textarea.range(range.start, range.end).range('');
                      }
  
                      // Now add the newline, remembering to resize the
                      // compose box if needed.
                      textarea.caret("\n");
                      compose_ui.autosize_textarea();
                      e.preventDefault();
                      return;
                  }
  
                  // Fall through to native browser behavior, otherwise.
              }
          }
      }
  }
  
  function handle_keyup(e) {
      var code = e.keyCode || e.which;
      if (code === 13 || (code === 9 && !e.shiftKey)) { // Enter key or tab key
          if (nextFocus) {
              ui_util.focus_on(nextFocus);
              nextFocus = false;
          }
      }
  }
  
  // http://stackoverflow.com/questions/3380458/looking-for-a-better-workaround-to-chrome-select-on-focus-bug
  function select_on_focus(field_id) {
      // A select event appears to trigger a focus event under certain
      // conditions in Chrome so we need to protect against infinite
      // recursion.
      var in_handler = false;
      $("#" + field_id).focus(function () {
          if (in_handler) {
              return;
          }
          in_handler = true;
          $("#" + field_id).select().one('mouseup', function (e) {
              e.preventDefault();
          });
          in_handler = false;
      });
  }
  
  exports.split_at_cursor = function (query, input) {
      var cursor = input.caret();
      return [query.slice(0, cursor), query.slice(cursor)];
  };
  
  exports.tokenize_compose_str = function (s) {
      // This basically finds a token like "@alic" or
      // "#Veron" as close to the end of the string as it
      // can find it.  It wants to find white space or
      // punctuation before the token, unless it's at the
      // beginning of the line.  It doesn't matter what comes
      // after the first character.
      var i = s.length;
  
      var min_i = s.length - 25;
      if (min_i < 0) {
          min_i = 0;
      }
  
      while (i > min_i) {
          i -= 1;
          switch (s[i]) {
              case '`':
              case '~':
                  // Code block must start on a new line
                  if (i === 2) {
                      return s.slice(0);
                  } else if (i > 2 && s[i-3] === "\n") {
                      return s.slice(i-2);
                  }
                  break;
              case '#':
              case '@':
              case ':':
                  if (i === 0) {
                      return s.slice(i);
                  } else if (/[\s(){}\[\]]/.test(s[i-1])) {
                      return s.slice(i);
                  }
          }
      }
  
      return '';
  };
  
  exports.compose_content_begins_typeahead = function (query) {
      var split = exports.split_at_cursor(query, this.$element);
      var current_token = exports.tokenize_compose_str(split[0]);
      if (current_token === '') {
          return false;
      }
      var rest = split[1];
  
      // If the remaining content after the mention isn't a space or
      // punctuation (or end of the message), don't try to typeahead; we
      // probably just have the cursor in the middle of an
      // already-completed object.
  
      // We will likely want to extend this list to be more i18n-friendly.
      var terminal_symbols = ',.;?!()[] "\'\n\t';
      if (rest !== '' && terminal_symbols.indexOf(rest[0]) === -1) {
          return false;
      }
  
      // Start syntax highlighting autocompleter if the first three characters are ```
      var syntax_token = current_token.substring(0,3);
      if (this.options.completions.syntax && (syntax_token === '```' || syntax_token === "~~~")) {
          // Only autocomplete if user starts typing a language after ```
          if (current_token.length === 3) {
              return false;
          }
  
          // If the only input is a space, don't autocomplete
          current_token = current_token.substring(3);
          if (current_token === " ") {
              return false;
          }
  
          // Trim the first whitespace if it is there
          if (current_token[0] === " ") {
              current_token = current_token.substring(1);
          }
          this.completing = 'syntax';
          this.token = current_token;
          return Object.keys(pygments_data.langs);
      }
  
      // Only start the emoji autocompleter if : is directly after one
      // of the whitespace or punctuation chars we split on.
      if (this.options.completions.emoji && current_token[0] === ':') {
          // We don't want to match non-emoji emoticons such
          // as :P or :-p
          // Also, if the user has only typed a colon and nothing after,
          // no need to match yet.
          if (/^:-.?$/.test(current_token) || /^:[^a-z+]?$/.test(current_token)) {
              return false;
          }
          this.completing = 'emoji';
          this.token = current_token.substring(1);
          return emoji.emojis;
      }
  
      if (this.options.completions.mention && current_token[0] === '@') {
          // Don't autocomplete if there is a space following an '@'
          current_token = current_token.substring(1);
          if (current_token.startsWith('**')) {
              current_token = current_token.substring(2);
          } else if (current_token.startsWith('*')) {
              current_token = current_token.substring(1);
          }
          if (current_token.length < 1 || current_token.lastIndexOf('*') !== -1) {
              return false;
          }
  
          if (current_token[0] === " ") {
              return false;
          }
  
          this.completing = 'mention';
          this.token = current_token;
          var all_item = {
              special_item_text: "all (Notify everyone)",
              email: "all",
              // Always sort above, under the assumption that names will
              // be longer and only contain "all" as a substring.
              pm_recipient_count: Infinity,
              full_name: "all",
          };
          var everyone_item = {
              special_item_text: "everyone (Notify everyone)",
              email: "everyone",
              pm_recipient_count: Infinity,
              full_name: "everyone",
          };
          var persons = people.get_realm_persons();
          var groups = user_groups.get_realm_user_groups();
          return [].concat(persons, [all_item, everyone_item], groups);
      }
  
      if (this.options.completions.stream && current_token[0] === '#') {
          if (current_token.length === 1) {
              return false;
          }
  
          current_token = current_token.substring(1);
          if (current_token.startsWith('**')) {
              current_token = current_token.substring(2);
          }
  
          // Don't autocomplete if there is a space following a '#'
          if (current_token[0] === " ") {
              return false;
          }
  
          this.completing = 'stream';
          this.token = current_token;
          return stream_data.get_streams_for_settings_page();
      }
      return false;
  };
  
  exports.content_highlighter = function (item) {
      if (this.completing === 'emoji') {
          return typeahead_helper.render_emoji(item);
      } else if (this.completing === 'mention') {
          var rendered;
          if (user_groups.is_user_group(item)) {
              rendered = typeahead_helper.render_user_group(item);
          } else {
              rendered = typeahead_helper.render_person(item);
          }
          return rendered;
      } else if (this.completing === 'stream') {
          return typeahead_helper.render_stream(item);
      } else if (this.completing === 'syntax') {
          return typeahead_helper.render_typeahead_item({ primary: item });
      }
  };
  
  exports.content_typeahead_selected = function (item) {
      var pieces = exports.split_at_cursor(this.query, this.$element);
      var beginning = pieces[0];
      var rest = pieces[1];
      var textbox = this.$element;
  
      if (this.completing === 'emoji') {
          // leading and trailing spaces are required for emoji,
          // except if it begins a message or a new line.
          if (beginning.lastIndexOf(":") === 0 ||
              beginning.charAt(beginning.lastIndexOf(":") - 1) === " " ||
              beginning.charAt(beginning.lastIndexOf(":") - 1) === "\n") {
              beginning = (beginning.substring(0, beginning.length - this.token.length - 1)+ ":" + item.emoji_name + ": ");
          } else {
              beginning = (beginning.substring(0, beginning.length - this.token.length - 1) + " :" + item.emoji_name + ": ");
          }
      } else if (this.completing === 'mention') {
          beginning = beginning.substring(0, beginning.length - this.token.length - 1);
          if (beginning.endsWith('@*')) {
              beginning = beginning.substring(0, beginning.length - 2);
          } else if (beginning.endsWith('@')) {
              beginning = beginning.substring(0, beginning.length - 1);
          }
          if (user_groups.is_user_group(item)) {
              beginning += '@*' + item.name + '* ';
              $(document).trigger('usermention_completed.zulip', {user_group: item});
          } else {
              beginning += '@**' + item.full_name + '** ';
              $(document).trigger('usermention_completed.zulip', {mentioned: item});
          }
      } else if (this.completing === 'stream') {
          beginning = beginning.substring(0, beginning.length - this.token.length - 1);
          if (beginning.endsWith('#*')) {
              beginning = beginning.substring(0, beginning.length - 2);
          }
          beginning += '#**' + item.name + '** ';
          $(document).trigger('streamname_completed.zulip', {stream: item});
      } else if (this.completing === 'syntax') {
          // Isolate the end index of the triple backticks/tildes, including
          // possibly a space afterward
          var backticks = beginning.length - this.token.length;
          if (rest === '') {
              // If cursor is at end of input ("rest" is empty), then
              // complete the token before the cursor, and add a closing fence
              // after the cursor
              beginning = beginning.substring(0, backticks) + item + '\n';
              rest = "\n" + beginning.substring(backticks - 4, backticks).trim() + rest;
          } else {
              // If more text after the input, then complete the token, but don't touch
              // "rest" (i.e. do not add a closing fence)
              beginning = beginning.substring(0, backticks) + item;
          }
      }
  
      // Keep the cursor after the newly inserted text, as Bootstrap will call textbox.change() to
      // overwrite the text in the textbox.
      setTimeout(function () {
          textbox.caret(beginning.length, beginning.length);
          // Also, trigger autosize to check if compose box needs to be resized.
          compose_ui.autosize_textarea();
      }, 0);
      return beginning + rest;
  };
  
  exports.compose_content_matcher = function (item) {
      if (this.completing === 'emoji') {
          return query_matches_emoji(this.token, item);
      } else if (this.completing === 'mention') {
          var matches;
          if (user_groups.is_user_group(item)) {
              matches = query_matches_user_group_or_stream(this.token, item);
          } else {
              matches = query_matches_person(this.token, item);
          }
          return matches;
      } else if (this.completing === 'stream') {
          return query_matches_user_group_or_stream(this.token, item);
      } else if (this.completing === 'syntax') {
          return query_matches_language(this.token, item);
      }
  };
  
  exports.compose_matches_sorter = function (matches) {
      if (this.completing === 'emoji') {
          return typeahead_helper.sort_emojis(matches, this.token);
      } else if (this.completing === 'mention') {
          var users = [];
          var groups = [];
          _.each(matches, function (match) {
              if (user_groups.is_user_group(match)) {
                  groups.push(match);
              } else {
                  users.push(match);
              }
          });
  
          var recipients = typeahead_helper.sort_recipients(users, this.token,
                                                        compose_state.stream_name(),
                                                        compose_state.subject(), groups);
          return recipients;
      } else if (this.completing === 'stream') {
          return typeahead_helper.sort_streams(matches, this.token);
      } else if (this.completing === 'syntax') {
          return typeahead_helper.sort_languages(matches, this.token);
      }
  };
  
  exports.initialize_compose_typeahead = function (selector) {
      var completions = {
          mention: true,
          emoji: true,
          stream: true,
          syntax: true,
      };
  
      $(selector).typeahead({
          items: 5,
          dropup: true,
          fixed: true,
          source: exports.compose_content_begins_typeahead,
          highlighter: exports.content_highlighter,
          matcher: exports.compose_content_matcher,
          sorter: exports.compose_matches_sorter,
          updater: exports.content_typeahead_selected,
          stopAdvance: true, // Do not advance to the next field on a tab or enter
          completions: completions,
      });
  };
  
  exports.initialize = function () {
      select_on_focus("stream");
      select_on_focus("subject");
      select_on_focus("private_message_recipient");
  
      // These handlers are at the "form" level so that they are called after typeahead
      $("form#send_message_form").keydown(handle_keydown);
      $("form#send_message_form").keyup(handle_keyup);
  
      $("#enter_sends").click(function () {
          var send_button = $("#compose-send-button");
          page_params.enter_sends = $("#enter_sends").is(":checked");
          if (page_params.enter_sends) {
              send_button.fadeOut();
          } else {
              send_button.fadeIn();
          }
  
          // Refocus in the content box so you can continue typing or
          // press Enter to send.
          $("#compose-textarea").focus();
  
          return channel.post({
              url: '/json/users/me/enter-sends',
              idempotent: true,
              data: {enter_sends: page_params.enter_sends},
          });
      });
      $("#enter_sends").prop('checked', page_params.enter_sends);
      if (page_params.enter_sends) {
          $("#compose-send-button").hide();
      }
  
      // limit number of items so the list doesn't fall off the screen
      $("#stream").typeahead({
          source: function () {
              return stream_data.subscribed_streams();
          },
          items: 3,
          fixed: true,
          highlighter: function (item) {
              return typeahead_helper.render_typeahead_item({ primary: item });
          },
          matcher: function (item) {
              // The matcher for "stream" is strictly prefix-based,
              // because we want to avoid mixing up streams.
              var q = this.query.trim().toLowerCase();
              return (item.toLowerCase().indexOf(q) === 0);
          },
      });
  
      $("#subject").typeahead({
          source: function () {
              var stream_name = $("#stream").val();
              return exports.topics_seen_for(stream_name);
          },
          items: 3,
          fixed: true,
          highlighter: function (item) {
              return typeahead_helper.render_typeahead_item({ primary: item });
          },
          sorter: function (items) {
              var sorted = typeahead_helper.sorter(this.query, items, function (x) {return x;});
              if (sorted.length > 0 && sorted.indexOf(this.query) === -1) {
                  sorted.unshift(this.query);
              }
              return sorted;
          },
      });
  
      $("#private_message_recipient").typeahead({
          source: compose_pm_pill.get_typeahead_items,
          items: 5,
          dropup: true,
          fixed: true,
          highlighter: function (item) {
              return typeahead_helper.render_person(item);
          },
          matcher: function (item) {
              return query_matches_person(this.query, item);
          },
          sorter: function (matches) {
              // var current_stream = compose_state.stream_name();
              return typeahead_helper.sort_recipientbox_typeahead(
                  this.query, matches, "");
          },
          updater: function (item) {
              compose_pm_pill.set_from_typeahead(item);
          },
          stopAdvance: true, // Do not advance to the next field on a tab or enter
      });
  
      exports.initialize_compose_typeahead("#compose-textarea");
  
      $("#private_message_recipient").blur(function () {
          var val = $(this).val();
          var recipients = typeahead_helper.get_cleaned_pm_recipients(val);
          $(this).val(recipients.join(", "));
      });
  };
  
  return exports;
  
  }());
  if (typeof module !== 'undefined') {
      module.exports = composebox_typeahead;
  }
  