"use strict"; const constants = require("./constants"); import Mousetrap from "mousetrap"; import {Textcomplete, Textarea} from "textcomplete"; import fuzzy from "fuzzy"; import emojiMap from "./helpers/simplemap.json"; import store from "./store"; export default enableAutocomplete; const emojiSearchTerms = Object.keys(emojiMap); const emojiStrategy = { id: "emoji", match: /\B:([-+\w:?]{2,}):?$/, search(term, callback) { // Trim colon from the matched term, // as we are unable to get a clean string from match regex (term = term.replace(/:$/, "")), callback(fuzzyGrep(term, emojiSearchTerms)); }, template([string, original]) { return `${emojiMap[original]} ${string}`; }, replace([, original]) { return emojiMap[original]; }, index: 1, }; const nicksStrategy = { id: "nicks", match: /\B(@([a-zA-Z_[\]\\^{}|`@][a-zA-Z0-9_[\]\\^{}|`-]*)?)$/, search(term, callback) { term = term.slice(1); if (term[0] === "@") { callback(completeNicks(term.slice(1), true).map((val) => ["@" + val[0], "@" + val[1]])); } else { callback(completeNicks(term, true)); } }, template([string]) { return string; }, replace([, original], position = 1) { // If no postfix specified, return autocompleted nick as-is if (!store.state.settings.nickPostfix) { return original; } // If there is whitespace in the input already, append space to nick if (position > 0 && /\s/.test(store.state.activeChannel.channel.pendingMessage)) { return original + " "; } // If nick is first in the input, append specified postfix return original + store.state.settings.nickPostfix; }, index: 1, }; const chanStrategy = { id: "chans", match: /\B((#|\+|&|![A-Z0-9]{5})([^\x00\x0A\x0D\x20\x2C\x3A]+(:[^\x00\x0A\x0D\x20\x2C\x3A]*)?)?)$/, search(term, callback, match) { callback(completeChans(match[0])); }, template([string]) { return string; }, replace([, original]) { return original; }, index: 1, }; const commandStrategy = { id: "commands", match: /^\/(\w*)$/, search(term, callback) { callback(completeCommands("/" + term)); }, template([string]) { return string; }, replace([, original]) { return original; }, index: 1, }; const foregroundColorStrategy = { id: "foreground-colors", match: /\x03(\d{0,2}|[A-Za-z ]{0,10})$/, search(term, callback) { term = term.toLowerCase(); const matchingColorCodes = constants.colorCodeMap .filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1])) .map((i) => { if (fuzzy.test(term, i[1])) { return [ i[0], fuzzy.match(term, i[1], { pre: "", post: "", }).rendered, ]; } return i; }); callback(matchingColorCodes); }, template(value) { return `${value[1]}`; }, replace(value) { return "\x03" + value[0]; }, index: 1, }; const backgroundColorStrategy = { id: "background-colors", match: /\x03(\d{2}),(\d{0,2}|[A-Za-z ]{0,10})$/, search(term, callback, match) { term = term.toLowerCase(); const matchingColorCodes = constants.colorCodeMap .filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1])) .map((pair) => { if (fuzzy.test(term, pair[1])) { return [ pair[0], fuzzy.match(term, pair[1], { pre: "", post: "", }).rendered, ]; } return pair; }) .map((pair) => pair.concat(match[1])); // Needed to pass fg color to `template`... callback(matchingColorCodes); }, template(value) { return `${value[1]}`; }, replace(value) { return "\x03$1," + value[0]; }, index: 2, }; function enableAutocomplete(input) { let tabCount = 0; let lastMatch = ""; let currentMatches = []; input.addEventListener("input", (e) => { if (e.detail === "autocomplete") { return; } tabCount = 0; currentMatches = []; lastMatch = ""; }); Mousetrap(input).bind( "tab", (e) => { if (store.state.isAutoCompleting) { return; } e.preventDefault(); const text = input.value; if (tabCount === 0) { lastMatch = text .substring(0, input.selectionStart) .split(/\s/) .pop(); if (lastMatch.length === 0) { return; } currentMatches = completeNicks(lastMatch, false); if (currentMatches.length === 0) { return; } } const position = input.selectionStart - lastMatch.length; const newMatch = nicksStrategy.replace( [0, currentMatches[tabCount % currentMatches.length]], position ); const remainder = text.substr(input.selectionStart); input.value = text.substr(0, position) + newMatch + remainder; input.selectionStart -= remainder.length; input.selectionEnd = input.selectionStart; // Propagate change to Vue model input.dispatchEvent( new CustomEvent("input", { detail: "autocomplete", }) ); lastMatch = newMatch; tabCount++; }, "keydown" ); const editor = new Textarea(input); const textcomplete = new Textcomplete(editor, { dropdown: { className: "textcomplete-menu", placement: "top", }, }); textcomplete.register([ emojiStrategy, nicksStrategy, chanStrategy, commandStrategy, foregroundColorStrategy, backgroundColorStrategy, ]); // Activate the first item by default // https://github.com/yuku-t/textcomplete/issues/93 textcomplete.on("rendered", () => { if (textcomplete.dropdown.items.length > 0) { textcomplete.dropdown.items[0].activate(); } }); textcomplete.on("show", () => { store.commit("isAutoCompleting", true); }); textcomplete.on("hidden", () => { store.commit("isAutoCompleting", false); }); return { hide() { textcomplete.hide(); }, destroy() { textcomplete.destroy(); store.commit("isAutoCompleting", false); }, }; } function fuzzyGrep(term, array) { const results = fuzzy.filter(term, array, { pre: "", post: "", }); return results.map((el) => [el.string, el.original]); } function rawNicks() { if (store.state.activeChannel.channel.users.length > 0) { const users = store.state.activeChannel.channel.users.slice(); return users.sort((a, b) => b.lastMessage - a.lastMessage).map((u) => u.nick); } const me = store.state.activeChannel.network.nick; const otherUser = store.state.activeChannel.channel.name; // If this is a query, add their name to autocomplete if (me !== otherUser && store.state.activeChannel.channel.type === "query") { return [otherUser, me]; } // Return our own name by default for anything that isn't a channel or query return [me]; } function completeNicks(word, isFuzzy) { const users = rawNicks(); word = word.toLowerCase(); if (isFuzzy) { return fuzzyGrep(word, users); } return users.filter((w) => !w.toLowerCase().indexOf(word)); } function completeCommands(word) { const words = constants.commands.slice(); return fuzzyGrep(word, words); } function completeChans(word) { const words = []; for (const channel of store.state.activeChannel.network.channels) { if (channel.type === "channel") { words.push(channel.name); } } return fuzzyGrep(word, words); }