Skip to content

feat: add completion command #442

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

Merged
merged 7 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 11 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,55 +120,28 @@ you can set the configuration variable `skipUpdateWhenPageNotFound` to `true` (d

## Command-line Autocompletion

Currently we only support command-line autocompletion for zsh
and bash. Pull requests for other shells are most welcome!
We currently support command-line autocompletion for zsh and bash.
Pull requests for other shells are most welcome!

### zsh

It's easiest for
[oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh)
users, so let's start with that.

```zsh
mkdir -p $ZSH_CUSTOM/plugins/tldr
ln -s bin/completion/zsh/_tldr $ZSH_CUSTOM/plugins/tldr/_tldr
```

Then add tldr to your oh-my-zsh plugins,
usually defined in `~/.zshrc`,
resulting in something looking like this:

```zsh
plugins=(git tmux tldr)
```

Alternatively, using [zplug](https://github.com/zplug/zplug)

```zsh
zplug "tldr-pages/tldr-node-client", use:bin/completion/zsh
```
To enable autocompletion for the tldr command, run:

Fret not regular zsh user!
Copy or symlink `bin/completion/zsh/_tldr` to
`my/completions/_tldr`
(note the filename).
Then add the containing directory to your fpath:
### zsh

```zsh
fpath=(my/completions $fpath)
tldr completion zsh
source ~/.zshrc
```

### Bash
### bash

```bash
ln -s bin/completion/bash/tldr ~/.tldr-completion.bash
tldr completion bash
source ~/.bashrc
```

Now add the following line to our bashrc file:
This command will generate the appropriate completion script and append it to your shell's configuration file (`.zshrc` or `.bashrc`).

```bash
source ~/.tldr-completion.bash
```
If you encounter any issues or need more information about the autocompletion setup, please refer to the [completion.js](https://github.com/tldr-pages/tldr-node-client/blob/master/lib/completion.js) file in the repository.

## FAQ

Expand Down
12 changes: 12 additions & 0 deletions bin/completion/bash/tldr
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
#!/bin/bash

# tldr bash completion

# Check if bash-completion is already sourced
if ! type _completion_loader &>/dev/null; then
# If not, try to load it
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi

BUILTIN_THEMES="single base16 ocean"

PLATFORM_TYPES="android freebsd linux netbsd openbsd osx sunos windows"
Expand Down
24 changes: 22 additions & 2 deletions bin/tldr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const pkg = require('../package');
const Tldr = require('../lib/tldr');
const config = require('../lib/config');
const platforms = require('../lib/platforms');
const Completion = require('../lib/completion');
const { TldrError } = require('../lib/errors');

pkg.version = `v${pkg.version}\nClient Specification: 2.0`;
Expand All @@ -24,7 +25,8 @@ program
.option('-e, --random-example', 'Show a random example')
.option('-f, --render [file]', 'Render a specific markdown [file]')
.option('-m, --markdown', 'Output in markdown format')
.option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`);
.option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`)
.option('completion [shell]', 'Generate and add shell completion script to your shell configuration');

for (const platform of platforms.supportedPlatforms) {
program.option(`--${platform}`, `Override the platform with ${platform}`);
Expand Down Expand Up @@ -58,6 +60,11 @@ const help = `
To render a local file (for testing):

$ tldr --render /path/to/file.md

To add shell completion:

$ tldr completion bash
$ tldr completion zsh
`;

program.on('--help', () => {
Expand Down Expand Up @@ -105,7 +112,20 @@ if (program.list) {
program.args.unshift(program.search);
p = tldr.search(program.args);
} else if (program.args.length >= 1) {
p = tldr.get(program.args, program);
if (program.args[0] === 'completion') {
const shell = program.args[1];
const completion = new Completion(shell);
p = completion.getScript()
.then((script) => {return completion.appendScript(script);})
.then(() => {
if (shell === 'zsh') {
console.log('If completions don\'t work, you may need to rebuild your zcompdump:');
console.log(' rm -f ~/.zcompdump; compinit');
}
});
} else {
p = tldr.get(program.args, program);
}
}

if (p === null) {
Expand Down
82 changes: 82 additions & 0 deletions lib/completion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const { UnsupportedShellError, CompletionScriptError } = require('./errors');

class Completion {
constructor(shell) {
this.supportedShells = ['bash', 'zsh'];
if (!this.supportedShells.includes(shell)) {
throw new UnsupportedShellError(shell, this.supportedShells);
}
this.shell = shell;
this.rcFilename = shell === 'zsh' ? '.zshrc' : '.bashrc';
}

getFilePath() {
const homeDir = os.homedir();
return path.join(homeDir, this.rcFilename);
}

appendScript(script) {
const rcFilePath = this.getFilePath();
return new Promise((resolve, reject) => {
fs.appendFile(rcFilePath, `\n${script}\n`, (err) => {
if (err) {
reject((new CompletionScriptError(`Error appending to ${rcFilePath}: ${err.message}`)));
} else {
console.log(`Completion script added to ${rcFilePath}`);
console.log(`Please restart your shell or run 'source ~/${this.rcFilename}' to enable completions`);
resolve();
}
});
});
}

getScript() {
return new Promise((resolve) => {
if (this.shell === 'zsh') {
resolve(this.getZshScript());
} else if (this.shell === 'bash') {
resolve(this.getBashScript());
}
});
}

getZshScript() {
const completionDir = path.join(__dirname, '..', 'bin', 'completion', 'zsh');
return `
# tldr zsh completion
fpath=(${completionDir} $fpath)

# You might need to force rebuild zcompdump:
# rm -f ~/.zcompdump; compinit

# If you're using oh-my-zsh, you can force reload of completions:
# autoload -U compinit && compinit

# Check if compinit is already loaded, if not, load it
if (( ! $+functions[compinit] )); then
autoload -Uz compinit
compinit -C
fi
`.trim();
}

getBashScript() {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '..', 'bin', 'completion', 'bash', 'tldr');
fs.readFile(scriptPath, 'utf8', (err, data) => {
if (err) {
reject(new CompletionScriptError(`Error reading bash completion script: ${err.message}`));
} else {
resolve(data);
}
});
});
}
}

module.exports = Completion;
22 changes: 21 additions & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,31 @@ class MissingRenderPathError extends TldrError {
}
}

class UnsupportedShellError extends TldrError {
constructor(shell, supportedShells) {
super(`Unsupported shell: ${shell}. Supported shells are: ${supportedShells.join(', ')}`);
this.name = 'UnsupportedShellError';
// eslint-disable-next-line no-magic-numbers
this.code = 5;
}
}

class CompletionScriptError extends TldrError {
constructor(message) {
super(message);
this.name = 'CompletionScriptError';
// eslint-disable-next-line no-magic-numbers
this.code = 6;
}
}

module.exports = {
TldrError,
EmptyCacheError,
MissingPageError,
MissingRenderPathError
MissingRenderPathError,
UnsupportedShellError,
CompletionScriptError
};

function trim(strings, ...values) {
Expand Down
130 changes: 130 additions & 0 deletions test/completion.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

const Completion = require('../lib/completion');
const { UnsupportedShellError, CompletionScriptError } = require('../lib/errors');
const sinon = require('sinon');
const fs = require('fs');
const os = require('os');
const should = require('should');
const path = require('path');

describe('Completion', () => {
const zshrcPath = path.join(os.homedir(), '.zshrc');
const bashrcPath = path.join(os.homedir(), '.bashrc');

describe('constructor()', () => {
it('should construct with supported shell', () => {
const completion = new Completion('zsh');
should.exist(completion);
completion.shell.should.equal('zsh');
completion.rcFilename.should.equal('.zshrc');
});

it('should throw UnsupportedShellError for unsupported shell', () => {
(() => {return new Completion('fish');}).should.throw(UnsupportedShellError);
});
});

describe('getFilePath()', () => {
it('should return .zshrc path for zsh', () => {
const completion = new Completion('zsh');
completion.getFilePath().should.equal(zshrcPath);
});

it('should return .bashrc path for bash', () => {
const completion = new Completion('bash');
completion.getFilePath().should.equal(bashrcPath);
});
});

describe('appendScript()', () => {
let appendFileStub;

beforeEach(() => {
appendFileStub = sinon.stub(fs, 'appendFile').yields(null);
});

afterEach(() => {
appendFileStub.restore();
});

it('should append script to file', () => {
const completion = new Completion('zsh');
return completion.appendScript('test script')
.then(() => {
appendFileStub.calledOnce.should.be.true();
appendFileStub.firstCall.args[0].should.equal(zshrcPath);
appendFileStub.firstCall.args[1].should.equal('\ntest script\n');
});
});

it('should reject with CompletionScriptError on fs error', () => {
const completion = new Completion('zsh');
appendFileStub.yields(new Error('File write error'));
return completion.appendScript('test script')
.should.be.rejectedWith(CompletionScriptError);
});
});

describe('getScript()', () => {
it('should return zsh script for zsh shell', () => {
const completion = new Completion('zsh');
return completion.getScript()
.then((script) => {
script.should.containEql('# tldr zsh completion');
script.should.containEql('fpath=(');
});
});

it('should return bash script for bash shell', () => {
const completion = new Completion('bash');
const readFileStub = sinon.stub(fs, 'readFile').yields(null, '# bash completion script');

return completion.getScript()
.then((script) => {
script.should.equal('# bash completion script');
readFileStub.restore();
});
});
});

describe('getZshScript()', () => {
it('should return zsh completion script', () => {
const completion = new Completion('zsh');
const script = completion.getZshScript();
script.should.containEql('# tldr zsh completion');
script.should.containEql('fpath=(');
script.should.containEql('compinit');
});
});

describe('getBashScript()', () => {
let readFileStub;

beforeEach(() => {
readFileStub = sinon.stub(fs, 'readFile');
});

afterEach(() => {
readFileStub.restore();
});

it('should return bash completion script', () => {
const completion = new Completion('bash');
readFileStub.yields(null, '# bash completion script');

return completion.getBashScript()
.then((script) => {
script.should.equal('# bash completion script');
});
});

it('should reject with CompletionScriptError on fs error', () => {
const completion = new Completion('bash');
readFileStub.yields(new Error('File read error'));

return completion.getBashScript()
.should.be.rejectedWith(CompletionScriptError);
});
});
});
Loading