Skip to content

Commit daff603

Browse files
Zamoca42spageektti
andauthored
feat: add completion command (#442)
* feat: add completion command * update: completion from command to option and update help messages * test: Use cross-platform paths in completion tests - Replace hardcoded paths with `os.homedir()` and `path.join()` - Ensure tests run consistently across different operating systems * style: Change completion option argument from <shell> to [shell] for consistency * docs: update README.md for oh-my-zsh users --------- Co-authored-by: spageektti <git@spageektti.cc>
1 parent d3ae722 commit daff603

File tree

6 files changed

+279
-22
lines changed

6 files changed

+279
-22
lines changed

README.md

+12-19
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ you can set the configuration variable `skipUpdateWhenPageNotFound` to `true` (d
120120

121121
## Command-line Autocompletion
122122

123-
Currently we only support command-line autocompletion for zsh
124-
and bash. Pull requests for other shells are most welcome!
123+
We currently support command-line autocompletion for zsh and bash.
124+
Pull requests for other shells are most welcome!
125+
126+
To enable autocompletion for the tldr command, run:
125127

126128
### zsh
127129

@@ -142,33 +144,24 @@ resulting in something looking like this:
142144
plugins=(git tmux tldr)
143145
```
144146

145-
Alternatively, using [zplug](https://github.com/zplug/zplug)
146-
147-
```zsh
148-
zplug "tldr-pages/tldr-node-client", use:bin/completion/zsh
149-
```
150-
151147
Fret not regular zsh user!
152-
Copy or symlink `bin/completion/zsh/_tldr` to
153-
`my/completions/_tldr`
154-
(note the filename).
155-
Then add the containing directory to your fpath:
148+
You can also do this:
156149

157150
```zsh
158-
fpath=(my/completions $fpath)
151+
tldr completion zsh
152+
source ~/.zshrc
159153
```
160154

161-
### Bash
155+
### bash
162156

163157
```bash
164-
ln -s bin/completion/bash/tldr ~/.tldr-completion.bash
158+
tldr completion bash
159+
source ~/.bashrc
165160
```
166161

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

169-
```bash
170-
source ~/.tldr-completion.bash
171-
```
164+
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.
172165

173166
## FAQ
174167

bin/completion/bash/tldr

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
#!/bin/bash
22

3+
# tldr bash completion
4+
5+
# Check if bash-completion is already sourced
6+
if ! type _completion_loader &>/dev/null; then
7+
# If not, try to load it
8+
if [ -f /usr/share/bash-completion/bash_completion ]; then
9+
. /usr/share/bash-completion/bash_completion
10+
elif [ -f /etc/bash_completion ]; then
11+
. /etc/bash_completion
12+
fi
13+
fi
14+
315
BUILTIN_THEMES="single base16 ocean"
416

517
PLATFORM_TYPES="android freebsd linux netbsd openbsd osx sunos windows"

bin/tldr

+22-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const pkg = require('../package');
55
const Tldr = require('../lib/tldr');
66
const config = require('../lib/config');
77
const platforms = require('../lib/platforms');
8+
const Completion = require('../lib/completion');
89
const { TldrError } = require('../lib/errors');
910

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

2931
for (const platform of platforms.supportedPlatforms) {
3032
program.option(`--${platform}`, `Override the platform with ${platform}`);
@@ -58,6 +60,11 @@ const help = `
5860
To render a local file (for testing):
5961
6062
$ tldr --render /path/to/file.md
63+
64+
To add shell completion:
65+
66+
$ tldr completion bash
67+
$ tldr completion zsh
6168
`;
6269

6370
program.on('--help', () => {
@@ -105,7 +112,20 @@ if (program.list) {
105112
program.args.unshift(program.search);
106113
p = tldr.search(program.args);
107114
} else if (program.args.length >= 1) {
108-
p = tldr.get(program.args, program);
115+
if (program.args[0] === 'completion') {
116+
const shell = program.args[1];
117+
const completion = new Completion(shell);
118+
p = completion.getScript()
119+
.then((script) => {return completion.appendScript(script);})
120+
.then(() => {
121+
if (shell === 'zsh') {
122+
console.log('If completions don\'t work, you may need to rebuild your zcompdump:');
123+
console.log(' rm -f ~/.zcompdump; compinit');
124+
}
125+
});
126+
} else {
127+
p = tldr.get(program.args, program);
128+
}
109129
}
110130

111131
if (p === null) {

lib/completion.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const os = require('os');
6+
const { UnsupportedShellError, CompletionScriptError } = require('./errors');
7+
8+
class Completion {
9+
constructor(shell) {
10+
this.supportedShells = ['bash', 'zsh'];
11+
if (!this.supportedShells.includes(shell)) {
12+
throw new UnsupportedShellError(shell, this.supportedShells);
13+
}
14+
this.shell = shell;
15+
this.rcFilename = shell === 'zsh' ? '.zshrc' : '.bashrc';
16+
}
17+
18+
getFilePath() {
19+
const homeDir = os.homedir();
20+
return path.join(homeDir, this.rcFilename);
21+
}
22+
23+
appendScript(script) {
24+
const rcFilePath = this.getFilePath();
25+
return new Promise((resolve, reject) => {
26+
fs.appendFile(rcFilePath, `\n${script}\n`, (err) => {
27+
if (err) {
28+
reject((new CompletionScriptError(`Error appending to ${rcFilePath}: ${err.message}`)));
29+
} else {
30+
console.log(`Completion script added to ${rcFilePath}`);
31+
console.log(`Please restart your shell or run 'source ~/${this.rcFilename}' to enable completions`);
32+
resolve();
33+
}
34+
});
35+
});
36+
}
37+
38+
getScript() {
39+
return new Promise((resolve) => {
40+
if (this.shell === 'zsh') {
41+
resolve(this.getZshScript());
42+
} else if (this.shell === 'bash') {
43+
resolve(this.getBashScript());
44+
}
45+
});
46+
}
47+
48+
getZshScript() {
49+
const completionDir = path.join(__dirname, '..', 'bin', 'completion', 'zsh');
50+
return `
51+
# tldr zsh completion
52+
fpath=(${completionDir} $fpath)
53+
54+
# You might need to force rebuild zcompdump:
55+
# rm -f ~/.zcompdump; compinit
56+
57+
# If you're using oh-my-zsh, you can force reload of completions:
58+
# autoload -U compinit && compinit
59+
60+
# Check if compinit is already loaded, if not, load it
61+
if (( ! $+functions[compinit] )); then
62+
autoload -Uz compinit
63+
compinit -C
64+
fi
65+
`.trim();
66+
}
67+
68+
getBashScript() {
69+
return new Promise((resolve, reject) => {
70+
const scriptPath = path.join(__dirname, '..', 'bin', 'completion', 'bash', 'tldr');
71+
fs.readFile(scriptPath, 'utf8', (err, data) => {
72+
if (err) {
73+
reject(new CompletionScriptError(`Error reading bash completion script: ${err.message}`));
74+
} else {
75+
resolve(data);
76+
}
77+
});
78+
});
79+
}
80+
}
81+
82+
module.exports = Completion;

lib/errors.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,31 @@ class MissingRenderPathError extends TldrError {
4444
}
4545
}
4646

47+
class UnsupportedShellError extends TldrError {
48+
constructor(shell, supportedShells) {
49+
super(`Unsupported shell: ${shell}. Supported shells are: ${supportedShells.join(', ')}`);
50+
this.name = 'UnsupportedShellError';
51+
// eslint-disable-next-line no-magic-numbers
52+
this.code = 5;
53+
}
54+
}
55+
56+
class CompletionScriptError extends TldrError {
57+
constructor(message) {
58+
super(message);
59+
this.name = 'CompletionScriptError';
60+
// eslint-disable-next-line no-magic-numbers
61+
this.code = 6;
62+
}
63+
}
64+
4765
module.exports = {
4866
TldrError,
4967
EmptyCacheError,
5068
MissingPageError,
51-
MissingRenderPathError
69+
MissingRenderPathError,
70+
UnsupportedShellError,
71+
CompletionScriptError
5272
};
5373

5474
function trim(strings, ...values) {

test/completion.spec.js

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
3+
const Completion = require('../lib/completion');
4+
const { UnsupportedShellError, CompletionScriptError } = require('../lib/errors');
5+
const sinon = require('sinon');
6+
const fs = require('fs');
7+
const os = require('os');
8+
const should = require('should');
9+
const path = require('path');
10+
11+
describe('Completion', () => {
12+
const zshrcPath = path.join(os.homedir(), '.zshrc');
13+
const bashrcPath = path.join(os.homedir(), '.bashrc');
14+
15+
describe('constructor()', () => {
16+
it('should construct with supported shell', () => {
17+
const completion = new Completion('zsh');
18+
should.exist(completion);
19+
completion.shell.should.equal('zsh');
20+
completion.rcFilename.should.equal('.zshrc');
21+
});
22+
23+
it('should throw UnsupportedShellError for unsupported shell', () => {
24+
(() => {return new Completion('fish');}).should.throw(UnsupportedShellError);
25+
});
26+
});
27+
28+
describe('getFilePath()', () => {
29+
it('should return .zshrc path for zsh', () => {
30+
const completion = new Completion('zsh');
31+
completion.getFilePath().should.equal(zshrcPath);
32+
});
33+
34+
it('should return .bashrc path for bash', () => {
35+
const completion = new Completion('bash');
36+
completion.getFilePath().should.equal(bashrcPath);
37+
});
38+
});
39+
40+
describe('appendScript()', () => {
41+
let appendFileStub;
42+
43+
beforeEach(() => {
44+
appendFileStub = sinon.stub(fs, 'appendFile').yields(null);
45+
});
46+
47+
afterEach(() => {
48+
appendFileStub.restore();
49+
});
50+
51+
it('should append script to file', () => {
52+
const completion = new Completion('zsh');
53+
return completion.appendScript('test script')
54+
.then(() => {
55+
appendFileStub.calledOnce.should.be.true();
56+
appendFileStub.firstCall.args[0].should.equal(zshrcPath);
57+
appendFileStub.firstCall.args[1].should.equal('\ntest script\n');
58+
});
59+
});
60+
61+
it('should reject with CompletionScriptError on fs error', () => {
62+
const completion = new Completion('zsh');
63+
appendFileStub.yields(new Error('File write error'));
64+
return completion.appendScript('test script')
65+
.should.be.rejectedWith(CompletionScriptError);
66+
});
67+
});
68+
69+
describe('getScript()', () => {
70+
it('should return zsh script for zsh shell', () => {
71+
const completion = new Completion('zsh');
72+
return completion.getScript()
73+
.then((script) => {
74+
script.should.containEql('# tldr zsh completion');
75+
script.should.containEql('fpath=(');
76+
});
77+
});
78+
79+
it('should return bash script for bash shell', () => {
80+
const completion = new Completion('bash');
81+
const readFileStub = sinon.stub(fs, 'readFile').yields(null, '# bash completion script');
82+
83+
return completion.getScript()
84+
.then((script) => {
85+
script.should.equal('# bash completion script');
86+
readFileStub.restore();
87+
});
88+
});
89+
});
90+
91+
describe('getZshScript()', () => {
92+
it('should return zsh completion script', () => {
93+
const completion = new Completion('zsh');
94+
const script = completion.getZshScript();
95+
script.should.containEql('# tldr zsh completion');
96+
script.should.containEql('fpath=(');
97+
script.should.containEql('compinit');
98+
});
99+
});
100+
101+
describe('getBashScript()', () => {
102+
let readFileStub;
103+
104+
beforeEach(() => {
105+
readFileStub = sinon.stub(fs, 'readFile');
106+
});
107+
108+
afterEach(() => {
109+
readFileStub.restore();
110+
});
111+
112+
it('should return bash completion script', () => {
113+
const completion = new Completion('bash');
114+
readFileStub.yields(null, '# bash completion script');
115+
116+
return completion.getBashScript()
117+
.then((script) => {
118+
script.should.equal('# bash completion script');
119+
});
120+
});
121+
122+
it('should reject with CompletionScriptError on fs error', () => {
123+
const completion = new Completion('bash');
124+
readFileStub.yields(new Error('File read error'));
125+
126+
return completion.getBashScript()
127+
.should.be.rejectedWith(CompletionScriptError);
128+
});
129+
});
130+
});

0 commit comments

Comments
 (0)