<commit_before>import { extend } from 'flarum/common/extend';
import TextEditor from 'flarum/common/components/TextEditor';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
import avatar from 'flarum/common/helpers/avatar';
import usernameHelper from 'flarum/common/helpers/username';
import highlight from 'flarum/common/helpers/highlight';
import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable';
import { truncate } from 'flarum/common/utils/string';
import { throttle } from 'flarum/common/utils/throttleDebounce';

import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getMentionText from './utils/getMentionText';

const throttledSearch = throttle(
  250, // 250ms timeout
  function (typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions) {
    const typedLower = typed.toLowerCase();
    if (!searched.includes(typedLower)) {
      app.store.find('users', { filter: { q: typed }, page: { limit: 5 } }).then((results) => {
        results.forEach((u) => {
          if (!returnedUserIds.has(u.id())) {
            returnedUserIds.add(u.id());
            returnedUsers.push(u);
          }
        });
        
        buildSuggestions();
      });
      searched.push(typedLower);
    }
  }
);

export default function addComposerAutocomplete() {
  const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
  const dropdown = new AutocompleteDropdown();

  extend(TextEditor.prototype, 'oncreate', function () {
    const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');

    this.navigator = new KeyboardNavigatable();
    this.navigator
      .when(() => dropdown.active)
      .onUp(() => dropdown.navigate(-1))
      .onDown(() => dropdown.navigate(1))
      .onSelect(dropdown.complete.bind(dropdown))
      .onCancel(dropdown.hide.bind(dropdown))
      .bindTo($editor);

    $editor.after($container);
  });

  extend(TextEditor.prototype, 'buildEditorParams', function (params) {
    const searched = [];
    let relMentionStart;
    let absMentionStart;
    let typed;
    let matchTyped;

    // We store users returned from an API here to preserve order in which they are returned
    // This prevents the user list jumping around while users are returned.
    // We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
    const returnedUsers = Array.from(app.store.all('users'));
    const returnedUserIds = new Set(returnedUsers.map((u) => u.id()));

    const applySuggestion = (replacement) => {
      app.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');

      dropdown.hide();
    };

    params.inputListeners.push(function () {
      const selection = app.composer.editor.getSelectionRange();

      const cursor = selection[0];

      if (selection[1] - cursor > 0) return;

      // Search backwards from the cursor for an '@' symbol. If we find one,
      // we will want to show the autocomplete dropdown!
      const lastChunk = app.composer.editor.getLastNChars(30);
      absMentionStart = 0;
      for (let i = lastChunk.length - 1; i >= 0; i--) {
        const character = lastChunk.substr(i, 1);
        if (character === '@' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
          relMentionStart = i + 1;
          absMentionStart = cursor - lastChunk.length + i + 1;
          break;
        }
      }

      dropdown.hide();
      dropdown.active = false;<commit_msg>fix: autocomplete errors with TextEditor re-use (#77)<commit_after>import { extend } from 'flarum/common/extend';
import TextEditor from 'flarum/common/components/TextEditor';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
import avatar from 'flarum/common/helpers/avatar';
import usernameHelper from 'flarum/common/helpers/username';
import highlight from 'flarum/common/helpers/highlight';
import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable';
import { truncate } from 'flarum/common/utils/string';
import { throttle } from 'flarum/common/utils/throttleDebounce';

import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getMentionText from './utils/getMentionText';

const throttledSearch = throttle(
  250, // 250ms timeout
  function (typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions) {
    const typedLower = typed.toLowerCase();
    if (!searched.includes(typedLower)) {
      app.store.find('users', { filter: { q: typed }, page: { limit: 5 } }).then((results) => {
        results.forEach((u) => {
          if (!returnedUserIds.has(u.id())) {
            returnedUserIds.add(u.id());
            returnedUsers.push(u);
          }
        });
        
        buildSuggestions();
      });
      searched.push(typedLower);
    }
  }
);

export default function addComposerAutocomplete() {
  const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
  const dropdown = new AutocompleteDropdown();

  extend(TextEditor.prototype, 'oncreate', function () {
    const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');

    this.navigator = new KeyboardNavigatable();
    this.navigator
      .when(() => dropdown.active)
      .onUp(() => dropdown.navigate(-1))
      .onDown(() => dropdown.navigate(1))
      .onSelect(dropdown.complete.bind(dropdown))
      .onCancel(dropdown.hide.bind(dropdown))
      .bindTo($editor);

    $editor.after($container);
  });

  extend(TextEditor.prototype, 'buildEditorParams', function (params) {
    const searched = [];
    let relMentionStart;
    let absMentionStart;
    let typed;
    let matchTyped;

    // We store users returned from an API here to preserve order in which they are returned
    // This prevents the user list jumping around while users are returned.
    // We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
    const returnedUsers = Array.from(app.store.all('users'));
    const returnedUserIds = new Set(returnedUsers.map((u) => u.id()));

    const applySuggestion = (replacement) => {
      this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');

      dropdown.hide();
    };

    params.inputListeners.push(function () {
      const selection = app.composer.editor.getSelectionRange();

      const cursor = selection[0];

      if (selection[1] - cursor > 0) return;

      // Search backwards from the cursor for an '@' symbol. If we find one,
      // we will want to show the autocomplete dropdown!
      const lastChunk = app.composer.editor.getLastNChars(30);
      absMentionStart = 0;
      for (let i = lastChunk.length - 1; i >= 0; i--) {
        const character = lastChunk.substr(i, 1);
        if (character === '@' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
          relMentionStart = i + 1;
          absMentionStart = cursor - lastChunk.length + i + 1;
          break;
        }
      }

      dropdown.hide();
      dropdown.active = false;
{'fraction_non_alphanumeric': 0.13189480782198246, 'fraction_numerical': 0.007012811867835468, 'mean_word_length': 3.354668232530828, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2635357', 'n_tokens_mistral': 2241, 'n_tokens_neox': 2115, 'n_words': 527}