/* Copyright 2015, 2016 OpenMarket Ltd 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. */ var Entry = require("./TabCompleteEntries").Entry; const DELAY_TIME_MS = 1000; const KEY_TAB = 9; const KEY_SHIFT = 16; const KEY_WINDOWS = 91; // NB: DO NOT USE \b its "words" are roman alphabet only! // // Capturing group containing the start // of line or a whitespace char // \_______________ __________Capturing group of 1 or more non-whitespace chars // _|__ _|_ followed by the end of line // / \/ \ const MATCH_REGEX = /(^|\s)(\S+)$/; class TabComplete { constructor(opts) { opts.allowLooping = opts.allowLooping || false; opts.autoEnterTabComplete = opts.autoEnterTabComplete || false; opts.onClickCompletes = opts.onClickCompletes || false; this.opts = opts; this.completing = false; this.list = []; // full set of tab-completable things this.matchedList = []; // subset of completable things to loop over this.currentIndex = 0; // index in matchedList currently this.originalText = null; // original input text when tab was first hit this.textArea = opts.textArea; // DOMElement this.isFirstWord = false; // true if you tab-complete on the first word this.enterTabCompleteTimerId = null; this.inPassiveMode = false; } /** * @param {Entry[]} completeList */ setCompletionList(completeList) { this.list = completeList; if (this.opts.onClickCompletes) { // assign onClick listeners for each entry to complete the text this.list.forEach((l) => { l.onClick = () => { this.completeTo(l); } }); } } /** * @param {DOMElement} */ setTextArea(textArea) { this.textArea = textArea; } /** * @return {Boolean} */ isTabCompleting() { // actually have things to tab over return this.completing && this.matchedList.length > 1; } stopTabCompleting() { this.completing = false; this.currentIndex = 0; this._notifyStateChange(); } startTabCompleting() { this.completing = true; this.currentIndex = 0; this._calculateCompletions(); } /** * Do an auto-complete with the given word. This terminates the tab-complete. * @param {Entry} entry The tab-complete entry to complete to. */ completeTo(entry) { this.textArea.value = this._replaceWith( entry.getFillText(), true, entry.getSuffix(this.isFirstWord) ); this.stopTabCompleting(); // keep focus on the text area this.textArea.focus(); } /** * @param {Number} numAheadToPeek Return *up to* this many elements. * @return {Entry[]} */ peek(numAheadToPeek) { if (this.matchedList.length === 0) { return []; } var peekList = []; // return the current match item and then one with an index higher, and // so on until we've reached the requested limit. If we hit the end of // the list of options we're done. for (var i = 0; i < numAheadToPeek; i++) { var nextIndex; if (this.opts.allowLooping) { nextIndex = (this.currentIndex + i) % this.matchedList.length; } else { nextIndex = this.currentIndex + i; if (nextIndex === this.matchedList.length) { break; } } peekList.push(this.matchedList[nextIndex]); } // console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList)); return peekList; } handleTabPress(passive, shiftKey) { var wasInPassiveMode = this.inPassiveMode && !passive; this.inPassiveMode = passive; if (!this.completing) { this.startTabCompleting(); } if (shiftKey) { this.nextMatchedEntry(-1); } else { // if we were in passive mode we got out of sync by incrementing the // index to show the peek view but not set the text area. Therefore, // we want to set the *current* index rather than the *next* index. this.nextMatchedEntry(wasInPassiveMode ? 0 : 1); } this._notifyStateChange(); } /** * @param {DOMEvent} e */ onKeyDown(ev) { if (!this.textArea) { console.error("onKeyDown called before a