Skip to content

Commit

Permalink
initial interactive fuzzy-search mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ranyitz committed Mar 22, 2018
1 parent d5c979a commit 3b088d2
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 16 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@
"archy": "~1.0.0",
"chalk": "~2.3.2",
"commander": "~2.15.0",
"figures": "~2.0.0",
"fuzzysort": "~1.1.1",
"js-levenshtein": "~1.1.3",
"lodash": "~4.17.5",
"log-update": "~2.3.0",
"pkg-dir": "~2.0.0",
"tabtab": "~2.2.2"
},
Expand Down
79 changes: 79 additions & 0 deletions src/actions/interactive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const Input = require('../interactive/input');
const logUpdate = require('log-update');
const sortBySimilarity = require('../interactive/sort-by-similarity');
const getAction = require('./get');
const chalk = require('chalk');
const figures = require('figures');

module.exports = workspace => {
let results = [];
let currentResult = 0;
let currentInputValue = '';

const modulesNames = workspace.getModulesNames();

const renderResults = () => {
return results
.map((result, i) => {
if (i === results.length - currentResult - 1) {
return `${chalk.red(figures.pointer)} ${chalk.bold(
result.highlight,
)}`;
}
return ` ${result.highlight}`;
})
.join('\n');
};

const renderInputValue = () => {
return `${chalk.cyan(figures.pointer)} ${currentInputValue}`;
};

const renderAmountOfResults = () => {
return chalk.dim.yellow(`${results.length}/${modulesNames.length}`);
};

const renderUi = () => {
logUpdate(
`${renderResults()}\n${renderAmountOfResults()}\n${renderInputValue()}`,
);
};

results = sortBySimilarity(modulesNames, '');
renderUi();

const input = new Input({
stdin: process.stdin,
});

input.on('change', obj => {
currentInputValue = obj.valueWithCursor;
results = sortBySimilarity(modulesNames, obj.value);
renderUi();
});

input.on('up', () => {
if (currentResult < results.length - 1) {
currentResult++;
}
renderUi();
});

input.on('down', () => {
if (currentResult > 0) {
currentResult--;
}
renderUi();
});

input.on('choose', () => {
if (results.length === 0) {
return;
}
input.end();
logUpdate('');
const chosen = results[results.length - 1 - currentResult].value;
console.log(getAction(workspace, chosen));
process.exit(0);
});
};
32 changes: 16 additions & 16 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const setupCompletions = require('./completions/setup-completions');
const matchAction = require('./actions/match');
const getAction = require('./actions/get');
const listAction = require('./actions/list');
const interactiveAction = require('./actions/interactive');

const handleError = require('./handler-error');

Expand All @@ -31,26 +32,25 @@ try {

program.parse(process.argv);

// if no arguments specified, show help
if (program.args.length === 0) {
program.help();
}

const preDefinedCommands = program.commands.map(c => c._name);
setupCompletions(preDefinedCommands);

const firstArg = program.rawArgs[2];
const workspace = Workspace.loadSync();

if (!preDefinedCommands.includes(firstArg) && firstArg !== 'completion') {
const arg = program.args[0];
const { match, why } = program;

const workspace = Workspace.loadSync();

if (match) {
console.log(matchAction(workspace, arg));
} else {
console.log(getAction(workspace, arg, { why }));
if (program.args.length === 0) {
interactiveAction(workspace);
} else {
const firstArg = program.rawArgs[2];

if (!preDefinedCommands.includes(firstArg) && firstArg !== 'completion') {
const arg = program.args[0];
const { match, why } = program;

if (match) {
console.log(matchAction(workspace, arg));
} else {
console.log(getAction(workspace, arg, { why }));
}
}
}
} catch (error) {
Expand Down
108 changes: 108 additions & 0 deletions src/interactive/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const EventEmitter = require('events');
const chalk = require('chalk');
const {
ctrlC,
ctrlD,
esc,
left,
right,
up,
down,
enter,
del,
backspace,
} = require('./raw-key-codes');

module.exports = class Input extends EventEmitter {
constructor({ stdin }) {
super();

this._value = [];
this.cursorPos = 0;

stdin.setRawMode(true);
stdin.setEncoding('utf8');

stdin.on('error', e => {
this.end();
console.error(e);
});

stdin.on('data', this.onKeyPress.bind(this));
}

get value() {
return this._value.join('');
}

get valueWithCursor() {
if (this.cursorPos === this.value.length) {
return this.value + chalk.inverse(' ');
}

const firstChunk = this.value.slice(0, this.cursorPos);
const corsurChar = this.value.slice(this.cursorPos, this.cursorPos + 1);
const secondChunk = this.value.slice(this.cursorPos + 1);

return firstChunk + chalk.inverse(corsurChar) + secondChunk;
}

onKeyPress(key) {
let changed = false;

switch (key) {
case ctrlC:
case ctrlD:
case esc:
process.emit('SIGINT');
process.exit(1);
break;
case left:
this.cursorPos = Math.max(0, this.cursorPos - 1);
changed = true;
break;
case right:
this.cursorPos = Math.min(this._value.length, this.cursorPos + 1);
changed = true;
break;
case backspace:
if (this.cursorPos !== 0) {
this._value.splice(this.cursorPos - 1, 1);
this.cursorPos = Math.max(0, this.cursorPos - 1);
changed = true;
}
break;
case del:
if (this._value.length > this.cursorPos) {
this._value.splice(this.cursorPos, 1);
changed = true;
}
break;
case up:
this.emit('up');
break;
case down:
this.emit('down');
break;
case enter:
this.emit('choose');
break;
default:
this.insertChar(key);
changed = true;
}

if (changed) {
this.emit('change', this);
}
}

insertChar(char) {
this._value.splice(this.cursorPos, 0, char);
this.cursorPos++;
}

end() {
this.removeAllListeners();
}
};
10 changes: 10 additions & 0 deletions src/interactive/raw-key-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports.ctrlC = '\u0003';
module.exports.ctrlD = '\u0004';
module.exports.esc = '\u001b';
module.exports.left = '\u001b\u005b\u0044';
module.exports.right = '\u001b\u005b\u0043';
module.exports.up = '\u001b\u005b\u0041';
module.exports.down = '\u001b\u005b\u0042';
module.exports.enter = '\u000d';
module.exports.del = '\u001b\u005b\u0033\u007e';
module.exports.backspace = '\u007f';
18 changes: 18 additions & 0 deletions src/interactive/sort-by-similarity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fuzzysort = require('fuzzysort');

module.exports = (list, string) => {
return fuzzysort
.go(string, list, {
threshold: -20, // Don't return matches worse than this (higher is faster)
limit: Infinity, // Don't return more results than this (lower is faster)
allowTypo: true, // Allwos a snigle transpoes (false is faster)
})
.sort((a, b) => a.score > b.score)
.map(result => {
return {
value: result.target,
score: result.score,
highlight: fuzzysort.highlight(result, '\u001b[32m', '\u001b[39m') // eslint-disable-line
};
});
};

0 comments on commit 3b088d2

Please sign in to comment.