From dfdc07cbfa3137cd43730386ed4276fbfe6dfdcc Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Fri, 6 Jan 2017 20:10:33 -0800 Subject: [PATCH] Start working on better tab completion Code is not used anywhere yet, but the end goal is: * Replace the bash-style algorithm with a less kludgy one * Add the ability to customize tab completion method (will also include default zsh-style completion) * Abstract tab completion so it can be shared for chat and emote names as available options --- test/www/tabcomplete.js | 73 ++++++++++++++++++++++++++++++++++++ www/js/data.js | 1 + www/js/tabcomplete.js | 82 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 test/www/tabcomplete.js create mode 100644 www/js/tabcomplete.js diff --git a/test/www/tabcomplete.js b/test/www/tabcomplete.js new file mode 100644 index 00000000..81349866 --- /dev/null +++ b/test/www/tabcomplete.js @@ -0,0 +1,73 @@ +const assert = require('assert'); +global.CyTube = {}; +require('../../www/js/tabcomplete'); + +const testcases = [ + { + input: 'and his name is j', + position: 17, + options: ['johncena', 'johnstamos', 'johto'], + output: { + text: 'and his name is joh', + newPosition: 19 + }, + description: 'completes the longest unique substring' + }, + { + input: 'and his name is johnc', + position: 21, + options: ['johncena', 'johnstamos', 'johto'], + output: { + text: 'and his name is johncena ', + newPosition: 25 + }, + description: 'completes a unique match' + }, + { + input: 'and his name is johnc', + position: 21, + options: ['asdf'], + output: { + text: 'and his name is johnc', + newPosition: 21 + }, + description: 'does not complete when there is no match' + }, + { + input: 'and his name is johnc', + position: 21, + options: [], + output: { + text: 'and his name is johnc', + newPosition: 21 + }, + description: 'does not complete when there are no options' + }, + { + input: ' ', + position: 1, + options: ['abc', 'def', 'ghi'], + output: { + text: ' ', + newPosition: 1 + }, + description: 'does not complete when the input is empty' + } +]; + +describe('CyTube.tabCompletionMethods', () => { + describe('#Longest unique prefix', () => { + testcases.forEach(test => { + it(test.description, () => { + assert.deepEqual(test.output, + CyTube.tabCompleteMethods['Longest unique prefix']( + test.input, + test.position, + test.options, + {} + ) + ); + }); + }); + }); +}); diff --git a/www/js/data.js b/www/js/data.js index 75acd277..348a9cb6 100644 --- a/www/js/data.js +++ b/www/js/data.js @@ -68,6 +68,7 @@ var NO_STORAGE = typeof localStorage == "undefined" || localStorage === null; var SOCKETIO_CONNECT_ERROR_COUNT = 0; var HAS_CONNECTED_BEFORE = false; var IMAGE_MATCH = /]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/gi; +var CyTube = {}; function getOpt(k) { var v = NO_STORAGE ? readCookie(k) : localStorage.getItem(k); diff --git a/www/js/tabcomplete.js b/www/js/tabcomplete.js new file mode 100644 index 00000000..902a2ddb --- /dev/null +++ b/www/js/tabcomplete.js @@ -0,0 +1,82 @@ +CyTube.tabCompleteMethods = {}; + +// Bash-style completion +// Only completes as far as it is possible to maintain uniqueness of the completion. +CyTube.tabCompleteMethods['Longest unique prefix'] = function (input, position, options, context) { + var lower = input.toLowerCase(); + // First, backtrack to the nearest whitespace to find the + // incomplete string that should be completed. + var start; + var incomplete = ''; + for (start = position - 1; start >= 0; start--) { + if (/\s/.test(lower[start])) { + start++; + break; + } + + incomplete = lower[start] + incomplete; + } + + // Nothing to complete + if (!incomplete.length) { + return { + text: input, + newPosition: position + }; + } + + var matches = options.filter(function (option) { + return option.toLowerCase().indexOf(incomplete) === 0; + }); + + var completed; + var isFullMatch = false; + if (matches.length === 0) { + return { + text: input, + newPosition: position + }; + } else if (matches.length === 1) { + // Unique match + completed = matches[0]; + isFullMatch = true; + } else { + // There is not a unique match, find the longest possible prefix + // that results in a unique completion + // Do this by comparing each match to the next and trimming to the + // first index where they differ. + var currentPrefix = null; + for (var i = 0; i < matches.length - 1; i++) { + var first = matches[i]; + var second = matches[i+1]; + var nextPrefix = ''; + for (var j = 0; (currentPrefix === null || j < currentPrefix.length) + && j < first.length + && j < second.length; j++) { + if (first[j].toLowerCase() === second[j].toLowerCase()) { + nextPrefix += first[j]; + } else { + break; + } + } + + if (currentPrefix === null || nextPrefix.length < currentPrefix.length) { + currentPrefix = nextPrefix; + } + } + + completed = currentPrefix; + } + + var space = isFullMatch ? ' ' : ''; + return { + text: input.substring(0, start) + completed + space + input.substring(position), + newPosition: start + completed.length + space.length + }; +}; + +// Zsh-style completion. +// Always complete a full option, and cycle through available options on successive tabs +CyTube.tabCompleteMethods['Cycle options'] = function (input, position, options, context) { + +};