-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add autocomplete #333
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,5 @@ coverage | |
# test temp | ||
test/temp | ||
|
||
autocomplete-hints.json | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#!/usr/bin/env node | ||
|
||
require('module-alias/register'); | ||
const commander = require('commander'); | ||
const { makeAutoCompleteCommander } = require('@src/commands/autocomplete'); | ||
const { makeSmapiCommander } = require('@src/commands/smapi/smapi-commander'); | ||
const ConfigureCommander = require('@src/commands/configure'); | ||
const DeployCommander = require('@src/commands/deploy'); | ||
const DialogCommander = require('@src/commands/dialog'); | ||
const InitCommander = require('@src/commands/init'); | ||
const NewCommander = require('@src/commands/new'); | ||
const UtilCommander = require('@src/commands/util/util-commander'); | ||
|
||
const smapiCommander = makeSmapiCommander(); | ||
const utilCommander = UtilCommander.commander; | ||
const configureCommander = ConfigureCommander.createCommand(commander); | ||
const deployCommander = DeployCommander.createCommand(commander); | ||
const newCommander = NewCommander.createCommand(commander); | ||
const initCommander = InitCommander.createCommand(commander); | ||
const dialogCommander = DialogCommander.createCommand(commander); | ||
const commanders = [smapiCommander, utilCommander, configureCommander, deployCommander, newCommander, initCommander, dialogCommander]; | ||
|
||
const autoCompleteCommander = makeAutoCompleteCommander(commanders); | ||
|
||
if (!process.argv.slice(2).length) { | ||
autoCompleteCommander.outputHelp(); | ||
} else { | ||
autoCompleteCommander.parse(process.argv); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Autocompletion | ||
|
||
## Prerequisites | ||
|
||
Autocompletion currently works for the following shells: bash, zsh and fish. | ||
|
||
### Bash prerequisites setup | ||
|
||
1. Install bash-completion. | ||
|
||
``` | ||
brew install bash-completion | ||
``` | ||
|
||
2. Add bash_completion to ~/.bash_profile or ~/.bashrc: | ||
|
||
``` | ||
echo '[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"' >> ~/.bash_profile | ||
``` | ||
|
||
Similar prerequisites steps can be run for zsh and fish shells. | ||
|
||
## Enable Autocompletion | ||
To setup auto completion, please run the following command and then restart the terminal. | ||
|
||
``` | ||
ask autocomplete setup | ||
``` | ||
|
||
|
||
## Disable Autocompletion | ||
To disable auto completion, please run the following command and then restart the terminal. | ||
|
||
``` | ||
ask autocomplete cleanup | ||
``` | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
const fs = require('fs-extra'); | ||
const path = require('path'); | ||
|
||
module.exports = class Helper { | ||
constructor(omelette, commanders = []) { | ||
this.commanders = commanders; | ||
this.completion = omelette('ask'); | ||
this.autoCompleteHintsFile = path.join(__dirname, 'autocomplete-hints.json'); | ||
} | ||
|
||
_getAutoCompleteOptions() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor, should this func be renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why cmd? it just loads auto complete options tree. |
||
const options = {}; | ||
this.commanders.forEach(com => { | ||
options[com.name()] = com.commands.map(sumCom => sumCom.name()); | ||
}); | ||
|
||
return options; | ||
} | ||
|
||
/** | ||
* Initializes auto complete inside of the program | ||
*/ | ||
initAutoComplete() { | ||
if (fs.existsSync(this.autoCompleteHintsFile)) { | ||
const options = fs.readJsonSync(this.autoCompleteHintsFile); | ||
|
||
this.completion.tree(options); | ||
this.completion.init(); | ||
} | ||
} | ||
|
||
_withProcessExitDisabled(fn) { | ||
RonWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const origExit = process.exit; | ||
process.exit = () => {}; | ||
fn(); | ||
process.exit = origExit; | ||
} | ||
|
||
/** | ||
* Regenerates auto complete hints file | ||
*/ | ||
reloadAutoCompleteHints() { | ||
const options = this._getAutoCompleteOptions(); | ||
fs.writeJSONSync(this.autoCompleteHintsFile, options); | ||
} | ||
|
||
/** | ||
* Sets ups auto complete. For example, adds autocomplete entry to .bash_profile file | ||
*/ | ||
setUpAutoComplete() { | ||
this.reloadAutoCompleteHints(); | ||
this._withProcessExitDisabled(() => this.completion.setupShellInitFile()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. II think we need a try catch to cover this setupShellInitFile function, as suggested by the user https://github.com/f/omelette#automated-install. This step has too many potential problems depending on user's machine. We need to do the cleanup if it fails, and pop up the message for the failure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What type of clean up? How can we know what needs to be cleaned up? If this steps fails it will show user the error message from omelette. What extra can we do in try and catch except of rethrowing the error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When i run in windows here is the result:
Should we pre-check the OS? And give a good message if it falls through? |
||
} | ||
|
||
/** | ||
* Removes auto complete. For example, removes autocomplete entry from .bash_profile file | ||
*/ | ||
cleanUpAutoComplete() { | ||
this._withProcessExitDisabled(() => this.completion.cleanupShellInitFile()); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
const commander = require('commander'); | ||
const omelette = require('omelette'); | ||
const Messenger = require('@src/view/messenger'); | ||
const Helper = require('./helper'); | ||
|
||
/** | ||
* Initializes auto complete inside of the program | ||
*/ | ||
const initAutoComplete = () => { | ||
const helper = new Helper(omelette); | ||
helper.initAutoComplete(); | ||
}; | ||
/** | ||
* Creates auto complete commander | ||
* @param {*} commanders list of commanders used for creating an autocomplete hints file | ||
*/ | ||
const makeAutoCompleteCommander = commanders => { | ||
const program = new commander.Command(); | ||
commanders.push(program); | ||
|
||
const helper = new Helper(omelette, commanders); | ||
|
||
program._name = 'autocomplete'; | ||
program.description('sets up ask cli terminal auto completion'); | ||
|
||
program.command('setup') | ||
.description('set up auto completion') | ||
.action(() => { | ||
helper.setUpAutoComplete(); | ||
Messenger.getInstance().info('Successfully set up auto completion. Please, reload the terminal.'); | ||
}); | ||
|
||
program.command('cleanup') | ||
.description('clean up auto completion') | ||
.action(() => { | ||
helper.cleanUpAutoComplete(); | ||
Messenger.getInstance().info('Successfully removed auto completion. Please, reload the terminal.'); | ||
}); | ||
|
||
program.command('reload') | ||
.description('regenerates hints file') | ||
.action(() => { | ||
helper.reloadAutoCompleteHints(); | ||
Messenger.getInstance().info('Successfully regenerated the hints file.'); | ||
}); | ||
|
||
return program; | ||
}; | ||
|
||
module.exports = { initAutoComplete, makeAutoCompleteCommander }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
const { expect } = require('chai'); | ||
const commander = require('commander'); | ||
const EventEmitter = require('events'); | ||
const fs = require('fs-extra'); | ||
const sinon = require('sinon'); | ||
|
||
const Helper = require('@src/commands/autocomplete/helper'); | ||
|
||
describe('Commands autocomplete - helper test', () => { | ||
let helper; | ||
let setupShellInitFileStub; | ||
let cleanupShellInitFileStub; | ||
let initStub; | ||
let treeStub; | ||
let omeletteStub; | ||
|
||
const testCommander = new commander.Command(); | ||
testCommander._name = 'test'; | ||
testCommander.command('command-one'); | ||
testCommander.command('command-two'); | ||
|
||
const commanders = [testCommander]; | ||
|
||
beforeEach(() => { | ||
setupShellInitFileStub = sinon.stub(); | ||
cleanupShellInitFileStub = sinon.stub(); | ||
initStub = sinon.stub(); | ||
treeStub = sinon.stub(); | ||
|
||
omeletteStub = () => { | ||
class OmeletteStubClass extends EventEmitter { | ||
constructor() { | ||
super(); | ||
this.setupShellInitFile = setupShellInitFileStub; | ||
this.cleanupShellInitFile = cleanupShellInitFileStub; | ||
this.init = initStub; | ||
this.tree = treeStub; | ||
} | ||
} | ||
return new OmeletteStubClass(); | ||
}; | ||
|
||
helper = new Helper(omeletteStub, commanders); | ||
}); | ||
|
||
it('should set up autocomplete', () => { | ||
const writeJSONStub = sinon.stub(fs, 'writeJSONSync'); | ||
helper.setUpAutoComplete(); | ||
|
||
expect(writeJSONStub.callCount).eq(1); | ||
expect(setupShellInitFileStub.callCount).eq(1); | ||
}); | ||
|
||
it('should regenerate autocomplete hints file', () => { | ||
const writeJSONStub = sinon.stub(fs, 'writeJSONSync'); | ||
helper.reloadAutoCompleteHints(); | ||
|
||
expect(writeJSONStub.callCount).eq(1); | ||
}); | ||
|
||
it('should clean up autocomplete', () => { | ||
helper.cleanUpAutoComplete(); | ||
|
||
expect(cleanupShellInitFileStub.callCount).eq(1); | ||
}); | ||
|
||
it('should not initialize autocomplete if hint file is not present', () => { | ||
sinon.stub(fs, 'existsSync').withArgs(helper.autoCompleteHintsFile).returns(false); | ||
helper.initAutoComplete(); | ||
|
||
expect(initStub.callCount).eq(0); | ||
}); | ||
|
||
it('initialize autocomplete if hint file is present', () => { | ||
sinon.stub(fs, 'existsSync').withArgs(helper.autoCompleteHintsFile).returns(true); | ||
sinon.stub(fs, 'readJsonSync').withArgs(helper.autoCompleteHintsFile).returns({}); | ||
helper.initAutoComplete(); | ||
|
||
expect(treeStub.callCount).eq(1); | ||
expect(initStub.callCount).eq(1); | ||
}); | ||
|
||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my biggest question would be can we save this registry step each time a new command is created? I know this is a subcommand, but is it still possible to load from the main commander object? Based on the current cmd structure, we only need it for ask ,ask smapi, ask utils, ask skill. Is it possible to iterate the command list in bin folder, and dynamically load them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot think of a way. Do you know a way?