diff --git a/test/www/tabcomplete.js b/test/www/tabcomplete.js index 81349866..6985bc78 100644 --- a/test/www/tabcomplete.js +++ b/test/www/tabcomplete.js @@ -2,72 +2,231 @@ 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', () => { + describe('"Longest unique prefix"', () => { + 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 ', + position: 16, + options: ['asdf'], + output: { + text: 'and his name is ', + newPosition: 16 + }, + description: 'does not complete when there is an empty prefix' + }, + { + 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: 0, + options: ['abc', 'def', 'ghi'], + output: { + text: '', + newPosition: 0 + }, + description: 'does not complete when the input is empty' + } + ]; testcases.forEach(test => { it(test.description, () => { - assert.deepEqual(test.output, + assert.deepEqual( CyTube.tabCompleteMethods['Longest unique prefix']( test.input, test.position, test.options, {} - ) + ), + test.output ); }); }); }); + + describe('"Cycle options"', () => { + const testcases = [ + { + input: 'hey c', + position: 5, + options: ['COBOL', 'Carlos', 'carl', 'john', 'joseph', ''], + outputs: [ + { + text: 'hey carl ', + newPosition: 9 + }, + { + text: 'hey Carlos ', + newPosition: 11 + }, + { + text: 'hey COBOL ', + newPosition: 10 + }, + { + text: 'hey carl ', + newPosition: 9 + } + ], + description: 'cycles through options correctly' + }, + { + input: 'hey ', + position: 5, + options: ['COBOL', 'Carlos', 'carl', 'john'], + outputs: [ + { + text: 'hey ', + newPosition: 5 + } + ], + description: 'does not complete when there is an empty prefix' + }, + { + input: 'hey c', + position: 6, + options: [], + outputs: [ + { + text: 'hey c', + newPosition: 6 + } + ], + description: 'does not complete when there are no options' + }, + { + input: '', + position: 0, + options: ['COBOL', 'Carlos', 'carl', 'john'], + outputs: [ + { + text: '', + newPosition: 0 + } + ], + description: 'does not complete when the input is empty' + } + ]; + + const complete = CyTube.tabCompleteMethods['Cycle options']; + testcases.forEach(test => { + it(test.description, () => { + var context = {}; + var currentText = test.input; + var currentPosition = test.position; + for (var i = 0; i < test.outputs.length; i++) { + var output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(output, test.outputs[i]); + currentText = output.text; + currentPosition = output.newPosition; + } + }); + }); + + it('updates the context when the input changes to reduce the # of matches', () => { + var test = testcases[0]; + var context = {}; + var currentText = test.input; + var currentPosition = test.position; + + var output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, { + start: 4, + matches: ['carl', 'Carlos', 'COBOL'], + tabIndex: 0 + }); + currentText = output.text; + currentPosition = output.newPosition; + + output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, { + start: 4, + matches: ['carl', 'Carlos', 'COBOL'], + tabIndex: 1 + }); + currentText = output.text.replace(context.matches[1], 'jo').trim(); + currentPosition = 6; + + output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, { + start: 4, + matches: ['john', 'joseph'], + tabIndex: 0 + }); + assert.deepEqual(output, { + text: 'hey john ', + newPosition: 9 + }); + }); + + it('clears the context when the input changes to a non-match', () => { + var test = testcases[0]; + var context = {}; + var currentText = test.input; + var currentPosition = test.position; + + var output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, { + start: 4, + matches: ['carl', 'Carlos', 'COBOL'], + tabIndex: 0 + }); + currentText = output.text; + currentPosition = output.newPosition; + + output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, { + start: 4, + matches: ['carl', 'Carlos', 'COBOL'], + tabIndex: 1 + }); + currentText = output.text.replace(context.matches[1], 'asdf').trim(); + currentPosition = 8; + + output = complete(currentText, currentPosition, test.options, context); + assert.deepEqual(context, {}); + assert.deepEqual(output, { + text: 'hey asdf', + newPosition: 8 + }); + }); + }); }); diff --git a/www/js/tabcomplete.js b/www/js/tabcomplete.js index 902a2ddb..95323d47 100644 --- a/www/js/tabcomplete.js +++ b/www/js/tabcomplete.js @@ -78,5 +78,62 @@ CyTube.tabCompleteMethods['Longest unique prefix'] = function (input, position, // 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) { + if (typeof context.start !== 'undefined') { + var currentCompletion = input.substring(context.start, position - 1); + if (currentCompletion === context.matches[context.tabIndex]) { + context.tabIndex = (context.tabIndex + 1) % context.matches.length; + var completed = context.matches[context.tabIndex]; + return { + text: input.substring(0, context.start) + completed + ' ' + input.substring(position), + newPosition: context.start + completed.length + 1 + }; + } else { + delete context.matches; + delete context.tabIndex; + delete context.start; + } + } + 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; + }).sort(function (a, b) { + return a.toLowerCase() > b.toLowerCase(); + }); + + if (matches.length === 0) { + return { + text: input, + newPosition: position + }; + } + + context.start = start; + context.matches = matches; + context.tabIndex = 0; + return { + text: input.substring(0, start) + matches[0] + ' ' + input.substring(position), + newPosition: start + matches[0].length + 1 + }; };