Skip to content

Commit

Permalink
feat: add preliminary fish shell completion
Browse files Browse the repository at this point in the history
  • Loading branch information
wraithgar committed Mar 1, 2023
1 parent 6374930 commit 55435dc
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 6 deletions.
7 changes: 3 additions & 4 deletions lib/commands/completion.js
Expand Up @@ -79,12 +79,10 @@ class Completion extends BaseCommand {
})
}

const { COMP_CWORD, COMP_LINE, COMP_POINT } = process.env
const { COMP_CWORD, COMP_LINE, COMP_POINT, COMP_FISH } = process.env

// if the COMP_* isn't in the env, then just dump the script.
if (COMP_CWORD === undefined ||
COMP_LINE === undefined ||
COMP_POINT === undefined) {
if (COMP_CWORD === undefined || COMP_LINE === undefined || COMP_POINT === undefined) {
return dumpScript(resolve(this.npm.npmRoot, 'lib', 'utils', 'completion.sh'))
}

Expand All @@ -111,6 +109,7 @@ class Completion extends BaseCommand {
partialWords.push(partialWord)

const opts = {
isFish: COMP_FISH === 'true',
words,
w,
word,
Expand Down
3 changes: 3 additions & 0 deletions lib/commands/run-script.js
Expand Up @@ -51,6 +51,9 @@ class RunScript extends BaseCommand {
// find the script name
const json = resolve(this.npm.localPrefix, 'package.json')
const { scripts = {} } = await rpj(json).catch(er => ({}))
if (opts.isFish) {
return Object.keys(scripts).map(s => `${s}\t${scripts[s].slice(0, 30)}`)
}
return Object.keys(scripts)
}
}
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/completion.fish
@@ -0,0 +1,40 @@
# npm completions for Fish shell
# This script is a work in progress and does not fall under the normal semver contract as the rest of npm.

# __fish_npm_needs_command taken from:
# https://stackoverflow.com/questions/16657803/creating-autocomplete-script-with-sub-commands
function __fish_npm_needs_command
set -l cmd (commandline -opc)

if test (count $cmd) -eq 1
return 0
end

return 1
end

# Taken from https://github.com/fish-shell/fish-shell/blob/HEAD/share/completions/npm.fish
function __fish_complete_npm -d "Complete the commandline using npm's 'completion' tool"
# tell npm we are fish shell
set -lx COMP_FISH true
if command -sq npm
# npm completion is bash-centric, so we need to translate fish's "commandline" stuff to bash's $COMP_* stuff
# COMP_LINE is an array with the words in the commandline
set -lx COMP_LINE (commandline -opc)
# COMP_CWORD is the index of the current word in COMP_LINE
# bash starts arrays with 0, so subtract 1
set -lx COMP_CWORD (math (count $COMP_LINE) - 1)
# COMP_POINT is the index of point/cursor when the commandline is viewed as a string
set -lx COMP_POINT (commandline -C)
# If the cursor is after the last word, the empty token will disappear in the expansion
# Readd it
if test (commandline -ct) = ""
set COMP_CWORD (math $COMP_CWORD + 1)
set COMP_LINE $COMP_LINE ""
end
command npm completion -- $COMP_LINE 2>/dev/null
end
end

# flush out what ships with fish
complete -e npm
54 changes: 54 additions & 0 deletions scripts/fish-completion.js
@@ -0,0 +1,54 @@
/* eslint-disable no-console */
const fs = require('fs/promises')
const { resolve } = require('path')

const { commands, aliases } = require('../lib/utils/cmd-list.js')
const { definitions } = require('../lib/utils/config/index.js')

async function main () {
const file = resolve(__dirname, '..', 'lib', 'utils', 'completion.fish')
console.log(await fs.readFile(file, 'utf-8'))
const cmds = {}
for (const cmd of commands) {
cmds[cmd] = { aliases: [cmd] }
const cmdClass = require(`../lib/commands/${cmd}.js`)
cmds[cmd].description = cmdClass.description
cmds[cmd].params = cmdClass.params
}
for (const alias in aliases) {
cmds[aliases[alias]].aliases.push(alias)
}
for (const cmd in cmds) {
console.log(`# ${cmd}`)
const { aliases: cmdAliases, description, params = [] } = cmds[cmd]
// If npm completion could return all commands in a fish friendly manner
// like we do w/ run-script these wouldn't be needed.
/* eslint-disable-next-line max-len */
console.log(`complete -x -c npm -n __fish_npm_needs_command -a '${cmdAliases.join(' ')}' -d '${description}'`)
const shorts = params.map(p => {
// Our multi-character short params (e.g. -ws) are not very standard and
// don't work with things that assume short params are only ever single
// characters.
if (definitions[p].short?.length === 1) {
return `-s ${definitions[p].short}`
}
}).filter(p => p).join(' ')
// The config descriptions are not appropriate for -d here. We may want to
// consider having a more terse description for these.
// We can also have a mechanism to auto-generate the long form of options
// that have predefined values.
// params completion
/* eslint-disable-next-line max-len */
console.log(`complete -x -c npm -n '__fish_seen_subcommand_from ${cmdAliases.join(' ')}' ${params.map(p => `-l ${p}`).join(' ')} ${shorts}`)
// builtin npm completion
/* eslint-disable-next-line max-len */
console.log(`complete -x -c npm -n '__fish_seen_subcommand_from ${cmdAliases.join(' ')}' -a '(__fish_complete_npm)'`)
}
}

main().then(() => {
return process.exit()
}).catch(err => {
console.error(err)
process.exit(1)
})
11 changes: 9 additions & 2 deletions test/lib/commands/run-script.js
Expand Up @@ -34,12 +34,12 @@ const mockRs = async (t, { windows = false, runScript, ...opts } = {}) => {
}

t.test('completion', async t => {
const completion = async (t, remain, pkg) => {
const completion = async (t, remain, pkg, isFish = false) => {
const { npm } = await mockRs(t,
pkg ? { prefixDir: { 'package.json': JSON.stringify(pkg) } } : {}
)
const cmd = await npm.cmd('run-script')
return cmd.completion({ conf: { argv: { remain } } })
return cmd.completion({ conf: { argv: { remain } }, isFish })
}

t.test('already have a script name', async t => {
Expand All @@ -60,6 +60,13 @@ t.test('completion', async t => {
})
t.strictSame(res, ['hello', 'world'])
})

t.test('fish shell', async t => {
const res = await completion(t, ['npm', 'run'], {
scripts: { hello: 'echo hello', world: 'echo world' },
}, true)
t.strictSame(res, ['hello\techo hello', 'world\techo world'])
})
})

t.test('fail if no package.json', async t => {
Expand Down

0 comments on commit 55435dc

Please sign in to comment.