<commit_before>/**
 * @license
 * Copyright 2016 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debounce from 'lodash/debounce';
import {CancellationToken, CancellationTokenSource} from 'neuroglancer/util/cancellation';
import {BasicCompletionResult, Completion, CompletionWithDescription} from 'neuroglancer/util/completion';
import {RefCounted} from 'neuroglancer/util/disposable';
import {removeChildren, removeFromParent} from 'neuroglancer/util/dom';
import {positionDropdown} from 'neuroglancer/util/dropdown';
import {EventActionMap, KeyboardEventBinder, registerActionListener} from 'neuroglancer/util/keyboard_bindings';
import {longestCommonPrefix} from 'neuroglancer/util/longest_common_prefix';
import {scrollIntoViewIfNeeded} from 'neuroglancer/util/scroll_into_view';
import {Signal} from 'neuroglancer/util/signal';
import {associateLabelWithElement} from 'neuroglancer/widget/associate_label';

export {Completion, CompletionWithDescription} from 'neuroglancer/util/completion';

require('./autocomplete.css');

const ACTIVE_COMPLETION_CLASS_NAME = 'autocomplete-completion-active';

const AUTOCOMPLETE_INDEX_SYMBOL = Symbol('autocompleteIndex');

export interface CompletionResult extends BasicCompletionResult {
  showSingleResult?: boolean;
  selectSingleResult?: boolean;
  makeElement?: (completion: Completion) => HTMLElement;
}

export function makeDefaultCompletionElement(completion: Completion) {
  let element = document.createElement('div');
  element.textContent = completion.value;
  return element;
}

export function makeCompletionElementWithDescription(completion: CompletionWithDescription) {
  let element = document.createElement('div');
  element.className = 'autocomplete-completion-with-description';
  element.textContent = completion.value;
  let descriptionElement = document.createElement('div');
  descriptionElement.className = 'autocomplete-completion-description';
  descriptionElement.textContent = completion.description || '';
  element.appendChild(descriptionElement);
  return element;
}

const keyMap = EventActionMap.fromObject({
  'arrowdown': {action: 'cycle-next-active-completion'},
  'arrowup': {action: 'cycle-prev-active-completion'},
  'tab': {action: 'choose-active-completion-or-prefix', preventDefault: false},
  'enter': {action: 'choose-active-completion', preventDefault: false},
  'escape': {action: 'cancel', preventDefault: false, stopPropagation: false},
});


export type Completer = (value: string, cancellationToken: CancellationToken) =>
    Promise<CompletionResult>| null;

const DEFAULT_COMPLETION_DELAY = 200;  // milliseconds

export class AutocompleteTextInput extends RefCounted {
  element: HTMLDivElement;
  promptElement: HTMLLabelElement;
  inputWrapperElement: HTMLDivElement;
  inputElement: HTMLInputElement;
  hintElement: HTMLInputElement;
  dropdownElement: HTMLDivElement;
  inputChanged = new Signal<(value: string) => void>();
  private prevInputValue = '';
  private completionsVisible = false;
  private activeCompletionPromise: Promise<CompletionResult>|null = null;
  private activeCompletionCancellationToken: CancellationTokenSource|undefined = undefined;
  private hasFocus = false;
  private completionResult: CompletionResult|null = null;
  private dropdownContentsStale = true;
  private updateHintScrollPositionTimer: number|null = null;
  private completionElements: HTMLElement[]|null = null;
  private hasResultForDropdown = false;
  private commonPrefix = '';

  /**
   * Index of the active completion.  The active completion is displayed as the hint text and is
   * highlighted in the dropdown.
   */
  private activeIndex = -1;

  private dropdownStyleStale = true;

  private scheduleUpdateCompletions: () => void;
  completer: Completer;

  constructor(options: {completer: Completer, delay?: number}) {
    super();
    this.completer = options.completer;
    let {delay = DEFAULT_COMPLETION_DELAY} = options;

    let debouncedCompleter = this.scheduleUpdateCompletions = debounce(() => {
      const cancellationToken = this.activeCompletionCancellationToken =
          new CancellationTokenSource();
      let activeCompletionPromise = this.activeCompletionPromise =
          this.completer(this.value, cancellationToken);
      if (activeCompletionPromise !== null) {
        activeCompletionPromise.then(completionResult => {
          if (this.activeCompletionPromise === activeCompletionPromise) {
            this.setCompletions(completionResult);
            this.activeCompletionPromise = null;
          }
        });
      }
    }, delay);
    this.registerDisposer(() => {
      debouncedCompleter.cancel();
    });

    let element = this.element = document.createElement('div');
    element.className = 'autocomplete';

    let dropdownAndInputWrapper = document.createElement('div');
    dropdownAndInputWrapper.className = 'autocomplete-dropdown-wrapper';

    let dropdownElement = this.dropdownElement = document.createElement('div');
    dropdownElement.className = 'autocomplete-dropdown';

    let promptElement = this.promptElement = document.createElement('label');
    promptElement.className = 'autocomplete-prompt';

    let inputWrapperElement = this.inputWrapperElement = document.createElement('div');
    inputWrapperElement.className = 'autocomplete-input-wrapper';

    element.appendChild(promptElement);

    let inputElement = this.inputElement = document.createElement('input');
    inputElement.type = 'text';
    inputElement.autocomplete = 'off';
    inputElement.spellcheck = false;
    inputElement.className = 'autocomplete-input';
    associateLabelWithElement(promptElement, inputElement);

    let hintElement = this.hintElement = document.createElement('input');
    hintElement.type = 'text';
    hintElement.spellcheck = false;
    hintElement.className = 'autocomplete-hint';
    hintElement.disabled = true;
    inputWrapperElement.appendChild(hintElement);
    inputWrapperElement.appendChild(inputElement);

    dropdownAndInputWrapper.appendChild(inputWrapperElement);
    dropdownAndInputWrapper.appendChild(dropdownElement);
    element.appendChild(dropdownAndInputWrapper);

    this.registerInputHandler();
    this.handleInputChanged('');

    this.registerEventListener(this.inputElement, 'focus', () => {
      if (!this.hasFocus) {
        this.hasFocus = true;
        this.dropdownStyleStale = true;
        this.updateDropdown();
      }
    });
    this.registerEventListener(this.inputElement, 'blur', () => {
      if (this.hasFocus) {
        this.hasFocus = false;
        this.updateDropdown();
      }
    });
    this.registerEventListener(element.ownerDocument!.defaultView!, 'resize', () => {
      this.dropdownStyleStale = true;
    });

    this.registerEventListener(element.ownerDocument!.defaultView!, 'scroll', () => {
      this.dropdownStyleStale = true;
    });

    this.registerEventListener(
        this.dropdownElement, 'mousedown', this.handleDropdownMousedown.bind(this));

    this.registerEventListener(this.inputElement, 'keydown', () => {
      // User may have used a keyboard shortcut to scroll the input.
      this.hintScrollPositionMayBeStale();
    });

    this.registerEventListener(this.inputElement, 'mousemove', (event: MouseEvent) => {
      if (event.buttons !== 0) {
        // May be dragging the text, which could cause scrolling.  This is not perfect, because we
        // don't detect mouse movements outside of the input box.
        this.hintScrollPositionMayBeStale();
      }
    });

    const keyboardHandler = this.registerDisposer(new KeyboardEventBinder(inputElement, keyMap));
    keyboardHandler.allShortcutsAreGlobal = true;

    registerActionListener(inputElement, 'cycle-next-active-completion', () => {
      this.cycleActiveCompletion(+1);
    });

    registerActionListener(inputElement, 'cycle-prev-active-completion', () => {
      this.cycleActiveCompletion(-1);
    });

    registerActionListener(
        inputElement, 'choose-active-completion-or-prefix', (event: CustomEvent) => {
          if (this.selectActiveCompletion(/*allowPrefix=*/true)) {
            event.preventDefault();
          }
        });
    registerActionListener(inputElement, 'choose-active-completion', (event: CustomEvent) => {
      if (this.selectActiveCompletion(/*allowPrefix=*/false)) {
        event.preventDefault();
      }
    });
    registerActionListener(inputElement, 'cancel', (event: CustomEvent) => {
      event.stopPropagation();
      if (this.cancel()) {
        event.detail.preventDefault();
        event.detail.stopPropagation();
      }
    });
  }

  private hintScrollPositionMayBeStale() {
    if (this.hintElement.value !== '') {
      this.scheduleUpdateHintScrollPosition();
    }
  }

  get disabled() {
    return this.inputElement.disabled;
  }

  set disabled(value: boolean) {
    this.inputElement.disabled = value;
  }

  private handleDropdownMousedown(event: MouseEvent) {
    this.inputElement.focus();<commit_msg>fix: allow copy pasting layer location in Firefox<commit_after>/**
 * @license
 * Copyright 2016 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debounce from 'lodash/debounce';
import {CancellationToken, CancellationTokenSource} from 'neuroglancer/util/cancellation';
import {BasicCompletionResult, Completion, CompletionWithDescription} from 'neuroglancer/util/completion';
import {RefCounted} from 'neuroglancer/util/disposable';
import {removeChildren, removeFromParent} from 'neuroglancer/util/dom';
import {positionDropdown} from 'neuroglancer/util/dropdown';
import {EventActionMap, KeyboardEventBinder, registerActionListener} from 'neuroglancer/util/keyboard_bindings';
import {longestCommonPrefix} from 'neuroglancer/util/longest_common_prefix';
import {scrollIntoViewIfNeeded} from 'neuroglancer/util/scroll_into_view';
import {Signal} from 'neuroglancer/util/signal';
import {associateLabelWithElement} from 'neuroglancer/widget/associate_label';

export {Completion, CompletionWithDescription} from 'neuroglancer/util/completion';

require('./autocomplete.css');

const ACTIVE_COMPLETION_CLASS_NAME = 'autocomplete-completion-active';

const AUTOCOMPLETE_INDEX_SYMBOL = Symbol('autocompleteIndex');

export interface CompletionResult extends BasicCompletionResult {
  showSingleResult?: boolean;
  selectSingleResult?: boolean;
  makeElement?: (completion: Completion) => HTMLElement;
}

export function makeDefaultCompletionElement(completion: Completion) {
  let element = document.createElement('div');
  element.textContent = completion.value;
  return element;
}

export function makeCompletionElementWithDescription(completion: CompletionWithDescription) {
  let element = document.createElement('div');
  element.className = 'autocomplete-completion-with-description';
  element.textContent = completion.value;
  let descriptionElement = document.createElement('div');
  descriptionElement.className = 'autocomplete-completion-description';
  descriptionElement.textContent = completion.description || '';
  element.appendChild(descriptionElement);
  return element;
}

const keyMap = EventActionMap.fromObject({
  'arrowdown': {action: 'cycle-next-active-completion'},
  'arrowup': {action: 'cycle-prev-active-completion'},
  'tab': {action: 'choose-active-completion-or-prefix', preventDefault: false},
  'enter': {action: 'choose-active-completion', preventDefault: false},
  'escape': {action: 'cancel', preventDefault: false, stopPropagation: false},
});


export type Completer = (value: string, cancellationToken: CancellationToken) =>
    Promise<CompletionResult>| null;

const DEFAULT_COMPLETION_DELAY = 200;  // milliseconds

export class AutocompleteTextInput extends RefCounted {
  element: HTMLDivElement;
  promptElement: HTMLLabelElement;
  inputWrapperElement: HTMLDivElement;
  inputElement: HTMLInputElement;
  hintElement: HTMLInputElement;
  dropdownElement: HTMLDivElement;
  inputChanged = new Signal<(value: string) => void>();
  private prevInputValue = '';
  private completionsVisible = false;
  private activeCompletionPromise: Promise<CompletionResult>|null = null;
  private activeCompletionCancellationToken: CancellationTokenSource|undefined = undefined;
  private hasFocus = false;
  private completionResult: CompletionResult|null = null;
  private dropdownContentsStale = true;
  private updateHintScrollPositionTimer: number|null = null;
  private completionElements: HTMLElement[]|null = null;
  private hasResultForDropdown = false;
  private commonPrefix = '';

  /**
   * Index of the active completion.  The active completion is displayed as the hint text and is
   * highlighted in the dropdown.
   */
  private activeIndex = -1;

  private dropdownStyleStale = true;

  private scheduleUpdateCompletions: () => void;
  completer: Completer;

  constructor(options: {completer: Completer, delay?: number}) {
    super();
    this.completer = options.completer;
    let {delay = DEFAULT_COMPLETION_DELAY} = options;

    let debouncedCompleter = this.scheduleUpdateCompletions = debounce(() => {
      const cancellationToken = this.activeCompletionCancellationToken =
          new CancellationTokenSource();
      let activeCompletionPromise = this.activeCompletionPromise =
          this.completer(this.value, cancellationToken);
      if (activeCompletionPromise !== null) {
        activeCompletionPromise.then(completionResult => {
          if (this.activeCompletionPromise === activeCompletionPromise) {
            this.setCompletions(completionResult);
            this.activeCompletionPromise = null;
          }
        });
      }
    }, delay);
    this.registerDisposer(() => {
      debouncedCompleter.cancel();
    });

    let element = this.element = document.createElement('div');
    element.className = 'autocomplete';

    let dropdownAndInputWrapper = document.createElement('div');
    dropdownAndInputWrapper.className = 'autocomplete-dropdown-wrapper';

    let dropdownElement = this.dropdownElement = document.createElement('div');
    dropdownElement.className = 'autocomplete-dropdown';

    let promptElement = this.promptElement = document.createElement('label');
    promptElement.className = 'autocomplete-prompt';

    let inputWrapperElement = this.inputWrapperElement = document.createElement('div');
    inputWrapperElement.className = 'autocomplete-input-wrapper';

    element.appendChild(promptElement);

    let inputElement = this.inputElement = document.createElement('input');
    inputElement.type = 'text';
    inputElement.autocomplete = 'off';
    inputElement.spellcheck = false;
    inputElement.className = 'autocomplete-input';
    associateLabelWithElement(promptElement, inputElement);

    let hintElement = this.hintElement = document.createElement('input');
    hintElement.type = 'text';
    hintElement.spellcheck = false;
    hintElement.className = 'autocomplete-hint';
    hintElement.disabled = true;
    inputWrapperElement.appendChild(hintElement);
    inputWrapperElement.appendChild(inputElement);

    dropdownAndInputWrapper.appendChild(inputWrapperElement);
    dropdownAndInputWrapper.appendChild(dropdownElement);
    element.appendChild(dropdownAndInputWrapper);

    this.registerInputHandler();
    this.handleInputChanged('');

    this.registerEventListener(this.inputElement, 'focus', () => {
      if (!this.hasFocus) {
        this.hasFocus = true;
        this.dropdownStyleStale = true;
        this.updateDropdown();
      }
    });
    this.registerEventListener(this.inputElement, 'blur', () => {
      if (this.hasFocus) {
        this.hasFocus = false;
        this.updateDropdown();
      }
    });
    this.registerEventListener(element.ownerDocument!.defaultView!, 'resize', () => {
      this.dropdownStyleStale = true;
    });

    this.registerEventListener(element.ownerDocument!.defaultView!, 'scroll', () => {
      this.dropdownStyleStale = true;
    });

    this.registerEventListener(
        this.dropdownElement, 'mousedown', this.handleDropdownMousedown.bind(this));

    this.registerEventListener(this.inputElement, 'keydown', () => {
      // User may have used a keyboard shortcut to scroll the input.
      this.hintScrollPositionMayBeStale();
    });

    this.registerEventListener(this.inputElement, 'mousemove', (event: MouseEvent) => {
      if (event.buttons !== 0) {
        // May be dragging the text, which could cause scrolling.  This is not perfect, because we
        // don't detect mouse movements outside of the input box.
        this.hintScrollPositionMayBeStale();
      }
    });

    const keyboardHandler = this.registerDisposer(new KeyboardEventBinder(inputElement, keyMap));
    keyboardHandler.allShortcutsAreGlobal = true;

    registerActionListener(inputElement, 'cycle-next-active-completion', () => {
      this.cycleActiveCompletion(+1);
    });

    registerActionListener(inputElement, 'cycle-prev-active-completion', () => {
      this.cycleActiveCompletion(-1);
    });

    registerActionListener(
        inputElement, 'choose-active-completion-or-prefix', (event: CustomEvent) => {
          if (this.selectActiveCompletion(/*allowPrefix=*/true)) {
            event.preventDefault();
          }
        });
    registerActionListener(inputElement, 'choose-active-completion', (event: CustomEvent) => {
      if (this.selectActiveCompletion(/*allowPrefix=*/false)) {
        event.preventDefault();
      }
    });
    registerActionListener(inputElement, 'cancel', (event: CustomEvent) => {
      event.stopPropagation();
      if (this.cancel()) {
        event.detail.preventDefault();
        event.detail.stopPropagation();
      }
    });
  }

  private hintScrollPositionMayBeStale() {
    if (this.hintElement.value !== '') {
      this.scheduleUpdateHintScrollPosition();
    }
  }

  get disabled() {
    return this.inputElement.readOnly;
  }

  set disabled(value: boolean) {
    this.inputElement.readOnly = value;
  }

  private handleDropdownMousedown(event: MouseEvent) {
    this.inputElement.focus();
{'fraction_non_alphanumeric': 0.1094519543120612, 'fraction_numerical': 0.0015718327569946558, 'mean_word_length': 4.428612059158135, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, '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': '2258669', 'n_tokens_mistral': 5133, 'n_tokens_neox': 4975, 'n_words': 1106}