Implement tab cycle style completion (not used anywhere yet)
This commit is contained in:
parent
dfdc07cbfa
commit
5321996c64
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue