<commit_before>import {
  ComponentFactoryResolver,
  ComponentRef,
  Directive,
  ElementRef,
  EventEmitter,
  HostListener,
  Injector,
  Input,
  OnDestroy,
  Output,
  ViewContainerRef,
} from '@angular/core';
import { takeUntil } from 'rxjs/operators';
import { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
import { Subject } from 'rxjs';
import getCaretCoordinates from 'textarea-caret';
import { getContentEditableCaretCoordinates } from '../../../helpers/contenteditable-caret';

export interface ChoiceSelectedEvent {
  choice: any;
  insertedAt: {
    start: number;
    end: number;
  };
}

@Directive({
  selector:
    'minds-textarea[mTextInputAutocomplete],textarea[mTextInputAutocomplete],input[type="text"][mTextInputAutocomplete]',
})
export class TextInputAutocompleteDirective implements OnDestroy {
  triggerCharacter: string;
  /**
   * The character that will trigger the menu to appear
   */
  @Input() triggerCharacters = ['@'];

  /**
   * The regular expression that will match the search text after the trigger character
   */
  @Input() searchRegexp = /^\w*$/;

  /**
   * The menu component to show with available options.
   * You can extend the built in `TextInputAutocompleteMenuComponent` component to use a custom template
   */
  @Input() menuComponent = TextInputAutocompleteMenuComponent;

  @Input() itemTemplate: any;

  /**
   * Enables adjustments for scroll offset.
   */
  @Input() adjustForScrollOffset: boolean = false;

  /**
   * Called when the options menu is shown
   */
  @Output() menuShown = new EventEmitter();

  /**
   * Called when the options menu is hidden
   */
  @Output() menuHidden = new EventEmitter();

  /**
   * Called when a choice is selected
   */
  @Output() choiceSelected = new EventEmitter<ChoiceSelectedEvent>();

  /**
   * A function that accepts a search string and returns an array of choices. Can also return a promise.
   */
  @Input()
  findChoices: (
    searchText: string,
    triggerCharacter?: string
  ) => any[] | Promise<any[]>;

  /**
   * A function that formats the selected choice once selected.
   */
  @Input()
  getChoiceLabel: (choice: any, triggerCharacter?: any) => string = choice =>
    choice;

  /* tslint:disable member-ordering */
  private menu:
    | {
        component: ComponentRef<TextInputAutocompleteMenuComponent>;
        triggerCharacterPosition: number;
        lastCaretPosition?: number;
      }
    | undefined;

  private menuHidden$ = new Subject();

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private viewContainerRef: ViewContainerRef,
    private injector: Injector,
    private elm: ElementRef
  ) {}

  @HostListener('keypress', ['$event.key'])
  onKeypress(key: string) {
    const index: number = this.triggerCharacters.indexOf(key);
    if (index !== -1) {
      this.triggerCharacter = this.triggerCharacters[index];
      this.showMenu();
    }
  }

  @HostListener('input', ['$event'])
  onChange(event: any) {
    const value: string = event.target.value || event.target.textContent;
    if (this.menu) {
      if (
        this.triggerCharacters.indexOf(
          value[this.menu.triggerCharacterPosition]
        ) === -1
      ) {
        this.hideMenu();
      } else {
        const cursor = this.elm.nativeElement.selectionStart;
        if (cursor < this.menu.triggerCharacterPosition) {
          this.hideMenu();
        } else {
          const searchText = value.slice(
            this.menu.triggerCharacterPosition + 1,
            cursor
          );
          if (!searchText.match(this.searchRegexp)) {
            this.hideMenu();
          } else {
            this.menu.component.instance.searchText = searchText;
            this.menu.component.instance.choices = [];
            this.menu.component.instance.choiceLoadError = undefined;
            this.menu.component.instance.choiceLoading = true;
            this.menu.component.changeDetectorRef.detectChanges();
            Promise.resolve(this.findChoices(searchText, this.triggerCharacter))
              .then(choices => {
                if (this.menu) {
                  this.menu.component.instance.choices = choices;
                  this.menu.component.instance.choiceLoading = false;
                  this.menu.component.changeDetectorRef.detectChanges();
                }
              })
              .catch(err => {
                if (this.menu) {
                  this.menu.component.instance.choiceLoading = false;
                  this.menu.component.instance.choiceLoadError = err;
                  this.menu.component.changeDetectorRef.detectChanges();
                }
              });
          }
        }
      }
    }
  }

  @HostListener('blur')
  onBlur() {
    if (this.menu) {
      this.menu.lastCaretPosition = this.getTriggerCharPosition(
        this.elm.nativeElement
      );
    }
  }

  private getTriggerCharPosition(element) {
    if (
      element instanceof HTMLTextAreaElement ||
      element instanceof HTMLInputElement
    ) {
      return this.elm.nativeElement.selectionStart;
    } else {
      const coordinates = getContentEditableCaretCoordinates(element);
      if (coordinates && coordinates.start) {
        return coordinates.start;
      }
    }
  }

  private showMenu() {
    if (!this.menu) {
      const menuFactory = this.componentFactoryResolver.resolveComponentFactory<
        TextInputAutocompleteMenuComponent
      >(this.menuComponent);
      this.menu = {
        component: this.viewContainerRef.createComponent(
          menuFactory,
          0,
          this.injector
        ),<commit_msg>Fixed issue preventing autocomplete from firing on first character<commit_after>import {
  ComponentFactoryResolver,
  ComponentRef,
  Directive,
  ElementRef,
  EventEmitter,
  HostListener,
  Injector,
  Input,
  OnDestroy,
  Output,
  ViewContainerRef,
} from '@angular/core';
import { takeUntil } from 'rxjs/operators';
import { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
import { Subject } from 'rxjs';
import getCaretCoordinates from 'textarea-caret';
import { getContentEditableCaretCoordinates } from '../../../helpers/contenteditable-caret';

export interface ChoiceSelectedEvent {
  choice: any;
  insertedAt: {
    start: number;
    end: number;
  };
}

@Directive({
  selector:
    'minds-textarea[mTextInputAutocomplete],textarea[mTextInputAutocomplete],input[type="text"][mTextInputAutocomplete]',
})
export class TextInputAutocompleteDirective implements OnDestroy {
  triggerCharacter: string;
  /**
   * The character that will trigger the menu to appear
   */
  @Input() triggerCharacters = ['@'];

  /**
   * The regular expression that will match the search text after the trigger character
   */
  @Input() searchRegexp = /^\w*$/;

  /**
   * The menu component to show with available options.
   * You can extend the built in `TextInputAutocompleteMenuComponent` component to use a custom template
   */
  @Input() menuComponent = TextInputAutocompleteMenuComponent;

  @Input() itemTemplate: any;

  /**
   * Enables adjustments for scroll offset.
   */
  @Input() adjustForScrollOffset: boolean = false;

  /**
   * Called when the options menu is shown
   */
  @Output() menuShown = new EventEmitter();

  /**
   * Called when the options menu is hidden
   */
  @Output() menuHidden = new EventEmitter();

  /**
   * Called when a choice is selected
   */
  @Output() choiceSelected = new EventEmitter<ChoiceSelectedEvent>();

  /**
   * A function that accepts a search string and returns an array of choices. Can also return a promise.
   */
  @Input()
  findChoices: (
    searchText: string,
    triggerCharacter?: string
  ) => any[] | Promise<any[]>;

  /**
   * A function that formats the selected choice once selected.
   */
  @Input()
  getChoiceLabel: (choice: any, triggerCharacter?: any) => string = choice =>
    choice;

  /* tslint:disable member-ordering */
  private menu:
    | {
        component: ComponentRef<TextInputAutocompleteMenuComponent>;
        triggerCharacterPosition: number;
        lastCaretPosition?: number;
      }
    | undefined;

  private menuHidden$ = new Subject();

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private viewContainerRef: ViewContainerRef,
    private injector: Injector,
    private elm: ElementRef
  ) {}

  @HostListener('keypress', ['$event.key'])
  onKeypress(key: string) {
    const index: number = this.triggerCharacters.indexOf(key);
    if (index !== -1) {
      this.triggerCharacter = this.triggerCharacters[index];
      this.showMenu();
    }
  }

  @HostListener('input', ['$event'])
  onChange(event: any) {
    const value: string =
      event.target.value || event.target.innerText || event.target.textContent;

    if (this.menu) {
      if (
        this.triggerCharacters.indexOf(
          value[this.menu.triggerCharacterPosition]
        ) === -1
      ) {
        this.hideMenu();
      } else {
        const cursor = this.elm.nativeElement.selectionStart;
        if (cursor < this.menu.triggerCharacterPosition) {
          this.hideMenu();
        } else {
          const searchText = value.slice(
            this.menu.triggerCharacterPosition + 1,
            cursor
          );
          if (!searchText.match(this.searchRegexp)) {
            this.hideMenu();
          } else {
            this.menu.component.instance.searchText = searchText;
            this.menu.component.instance.choices = [];
            this.menu.component.instance.choiceLoadError = undefined;
            this.menu.component.instance.choiceLoading = true;
            this.menu.component.changeDetectorRef.detectChanges();
            Promise.resolve(this.findChoices(searchText, this.triggerCharacter))
              .then(choices => {
                if (this.menu) {
                  this.menu.component.instance.choices = choices;
                  this.menu.component.instance.choiceLoading = false;
                  this.menu.component.changeDetectorRef.detectChanges();
                }
              })
              .catch(err => {
                if (this.menu) {
                  this.menu.component.instance.choiceLoading = false;
                  this.menu.component.instance.choiceLoadError = err;
                  this.menu.component.changeDetectorRef.detectChanges();
                }
              });
          }
        }
      }
    }
  }

  @HostListener('blur')
  onBlur() {
    if (this.menu) {
      this.menu.lastCaretPosition = this.getTriggerCharPosition(
        this.elm.nativeElement
      );
    }
  }

  private getTriggerCharPosition(element) {
    if (
      element instanceof HTMLTextAreaElement ||
      element instanceof HTMLInputElement
    ) {
      return this.elm.nativeElement.selectionStart;
    } else {
      const coordinates = getContentEditableCaretCoordinates(element);

      if (coordinates && (coordinates.start || coordinates.start === 0)) {
        return coordinates.start;
      }
    }
  }

  private showMenu() {
    if (!this.menu) {
      const menuFactory = this.componentFactoryResolver.resolveComponentFactory<
        TextInputAutocompleteMenuComponent
      >(this.menuComponent);
      this.menu = {
        component: this.viewContainerRef.createComponent(
          menuFactory,
          0,
          this.injector
        ),
{'fraction_non_alphanumeric': 0.10880195599022005, 'fraction_numerical': 0.0007858889276982187, 'mean_word_length': 2.5880325814536342, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 21, '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': '6543532', 'n_tokens_mistral': 3171, 'n_tokens_neox': 3080, 'n_words': 687}