Implement tab cycle style completion (not used anywhere yet)

This commit is contained in:
Calvin Montgomery 2017-01-07 10:55:59 -08:00
parent dfdc07cbfa
commit 5321996c64
2 changed files with 271 additions and 55 deletions

View file

@ -2,72 +2,231 @@ const assert = require('assert');
global.CyTube = {}; global.CyTube = {};
require('../../www/js/tabcomplete'); 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('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 => { testcases.forEach(test => {
it(test.description, () => { it(test.description, () => {
assert.deepEqual(test.output, assert.deepEqual(
CyTube.tabCompleteMethods['Longest unique prefix']( CyTube.tabCompleteMethods['Longest unique prefix'](
test.input, test.input,
test.position, test.position,
test.options, 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
});
});
});
}); });

View file

@ -78,5 +78,62 @@ CyTube.tabCompleteMethods['Longest unique prefix'] = function (input, position,
// Zsh-style completion. // Zsh-style completion.
// Always complete a full option, and cycle through available options on successive tabs // Always complete a full option, and cycle through available options on successive tabs
CyTube.tabCompleteMethods['Cycle options'] = function (input, position, options, context) { 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
};
}; };