From 9a6c85a1d3a89238b3ce86770b81cfad69ef4718 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Sat, 14 Jan 2023 18:19:52 -0300 Subject: [PATCH 01/43] feat: support space-separated cmds --- src/commands/autocomplete/create.ts | 199 +++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 57fb3066..3a97221c 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -1,10 +1,13 @@ import * as path from 'path' +import * as util from 'util' import * as fs from 'fs-extra' +import * as _ from 'lodash' import bashAutocomplete from '../../autocomplete/bash' import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces' import {AutocompleteBase} from '../../base' +import {Interfaces } from '@oclif/core' const debug = require('debug')('autocomplete:create') @@ -53,7 +56,7 @@ export default class Create extends AutocompleteBase { await fs.writeFile(this.bashSetupScriptPath, this.bashSetupScript) await fs.writeFile(this.bashCompletionFunctionPath, this.bashCompletionFunction) await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript) - await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction) + await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionWithSpacesFunction) } private get bashSetupScriptPath(): string { @@ -141,6 +144,29 @@ compinit;\n` return this._commands } + // TODO: handle commands without flags + // private genZshCmdFlagsCompFun(id: string, skipFunc: boolean = false): string { + // const command = this.config.findCommand(id,{must:true}) + // + // const flagNames = Object.keys(command.flags) + // let flagsComp='' + // + // for (const flagName of flagNames){ + // const flag = command.flags[flagName] + // + // if (flag.char) { + // flagsComp+=` {-${flag.char},--${flagName}}'[${sanitizeDescription(flag.summary ||flag.description)}]' \\\n` + // } else { + // flagsComp+=` --${flagName}'[${sanitizeDescription(flag.summary || flag.description)}]' \\\n` + // } + // } + // if (skipFunc) { + // return flagsComp + // } + // + // return util.format(`_${this.cliBin}_${command.id.replace(/:/g,'_')}() { \n _arguments -S \\\n%s}`, flagsComp) + // } + private genZshFlagSpecs(Klass: any): string { return Object.keys(Klass.flags || {}) .filter(flag => Klass.flags && !Klass.flags[flag].hidden) @@ -200,6 +226,177 @@ compinit;\n` return bashScript.replace(//g, cliBin).replace(//g, this.bashCommandsWithFlagsList) } + + private get zshCompletionWithSpacesFunction(): string { + const valueTemplate = ` "%s[%s]" \\\n` + const argTemplate = ` "%s")\n %s\n ;;\n` + const flagCompWithOptsTpl = + ` %s"[%s]:%s options:(%s)" \\\n` + + // TODO: + // * include command aliases + // * ignore hidden commands + const commands = this.config.commands + .map(c=>{ + c.description = sanitizeDescription(c.summary || c.description || '') + return c + }) + .sort((a, b) => { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + }); + + let topics = this.config.topics.filter((topic: Interfaces.Topic) => { + // it is assumed a topic has a child if it has children + const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) + return hasChild + }) + .sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }) + .map(t=> { + return { + name: t.name, + description: sanitizeDescription(t.description) + } + }) + + + const genZshTopicCompFun = (id: string): string => { + const underscoreSepId = id.replace(/:/g,'_') + const depth = id.split(':').length + + let valuesBlock = '' + let argsBlock = '' + + topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] + + valuesBlock+=util.format(valueTemplate,subArg,t.description) + argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) + }) + + commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + const subArg = c.id.split(':')[depth] + + // TODO: skip commands without flags + const flagNames = Object.keys(c.flags) + let flagsComp='' + + for (const flagName of flagNames){ + const flag = c.flags[flagName] + + if (flag.type ==='option' && flag.options) { + // generate completions for `flag.options` array + if (flag.char) { + flagsComp+=util.format( + flagCompWithOptsTpl, + `{-${flag.char},--${flagName}}`, + sanitizeDescription(flag.summary ||flag.description), + flagName, + flag.options.join(' ') + ) + } else { + flagsComp+=util.format( + flagCompWithOptsTpl, + `--${flag.name}`, + sanitizeDescription(flag.summary ||flag.description), + flagName, + flag.options.join(' ') + ) + } + } else { + if (flag.char) { + flagsComp+=` {-${flag.char},--${flagName}}"[${sanitizeDescription(flag.summary ||flag.description)}]" \\\n` + } else { + flagsComp+=` --${flagName}"[${sanitizeDescription(flag.summary || flag.description)}]" \\\n` + } + } + } + + valuesBlock+=util.format(valueTemplate,subArg,c.description) + + const flagArgsTemplate = ` "%s")\n _arguments -S \\\n%s\n ;;\n` + argsBlock+= util.format(flagArgsTemplate,subArg,flagsComp) + }) + + const topicCompFunc = +`_${this.cliBin}_${underscoreSepId}() { + local line state + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + _values "${this.cliBin} command" \\ +%s + ;; + args) + case $line[1] in +%s + esac + ;; + esac +} +` + + return util.format(topicCompFunc, valuesBlock, argsBlock) + } + + const compFunc = +`#compdef ${this.cliBin} + +${topics.map(t=> { + if (t.name.includes('data')) { + return genZshTopicCompFun(t.name) + } +}).join('\n')} + +${genZshTopicCompFun('force')} + +_sfdx() { + local line state + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + _values "${this.cliBin} command" \\ + "force[force stuff]" \\ + "data[data stuff]" + ;; + args) + case $line[1] in + data) + _sfdx_data + ;; + force) + _sfdx_force + ;; + esac + ;; + esac +} + +_${this.cliBin} +` + return compFunc + } private get zshCompletionFunction(): string { const cliBin = this.cliBin const allCommandsMeta = this.genAllCommandsMetaString From bb5596af192855b488f80ee5e3f53a7f71e4f722 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Sat, 14 Jan 2023 23:13:31 -0300 Subject: [PATCH 02/43] fix: make short/long exclusives --- src/commands/autocomplete/create.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 3a97221c..3b081b2c 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -231,7 +231,7 @@ compinit;\n` const valueTemplate = ` "%s[%s]" \\\n` const argTemplate = ` "%s")\n %s\n ;;\n` const flagCompWithOptsTpl = - ` %s"[%s]:%s options:(%s)" \\\n` + ` "%s"%s"[%s]:%s options:(%s)" \\\n` // TODO: // * include command aliases @@ -306,6 +306,7 @@ compinit;\n` if (flag.char) { flagsComp+=util.format( flagCompWithOptsTpl, + `(-${flag.char} --${flag.name})`, `{-${flag.char},--${flagName}}`, sanitizeDescription(flag.summary ||flag.description), flagName, @@ -322,7 +323,7 @@ compinit;\n` } } else { if (flag.char) { - flagsComp+=` {-${flag.char},--${flagName}}"[${sanitizeDescription(flag.summary ||flag.description)}]" \\\n` + flagsComp+=` "(-${flag.char} --${flag.name})"{-${flag.char},--${flagName}}"[${sanitizeDescription(flag.summary ||flag.description)}]" \\\n` } else { flagsComp+=` --${flagName}"[${sanitizeDescription(flag.summary || flag.description)}]" \\\n` } From 3c6db62e1df2651dfc6a58bf32d38c60ba5a9241 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Sun, 15 Jan 2023 18:09:08 -0300 Subject: [PATCH 03/43] fix: separate flag template for f.options && f.char --- src/commands/autocomplete/create.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 3b081b2c..f02e8a26 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -232,6 +232,8 @@ compinit;\n` const argTemplate = ` "%s")\n %s\n ;;\n` const flagCompWithOptsTpl = ` "%s"%s"[%s]:%s options:(%s)" \\\n` + const flagCompWithCharWithOptsTpl = + ` %s"[%s]:%s options:(%s)" \\\n` // TODO: // * include command aliases @@ -314,7 +316,7 @@ compinit;\n` ) } else { flagsComp+=util.format( - flagCompWithOptsTpl, + flagCompWithCharWithOptsTpl, `--${flag.name}`, sanitizeDescription(flag.summary ||flag.description), flagName, From ed51b1481347ef3dc53498ee584aab35e3b75c75 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 16 Jan 2023 16:43:20 -0300 Subject: [PATCH 04/43] fix: support `multiple` flag option --- src/commands/autocomplete/create.ts | 67 +++++++++++++++++------------ 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index f02e8a26..767b6eee 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -230,10 +230,6 @@ compinit;\n` private get zshCompletionWithSpacesFunction(): string { const valueTemplate = ` "%s[%s]" \\\n` const argTemplate = ` "%s")\n %s\n ;;\n` - const flagCompWithOptsTpl = - ` "%s"%s"[%s]:%s options:(%s)" \\\n` - const flagCompWithCharWithOptsTpl = - ` %s"[%s]:%s options:(%s)" \\\n` // TODO: // * include command aliases @@ -301,35 +297,50 @@ compinit;\n` let flagsComp='' for (const flagName of flagNames){ - const flag = c.flags[flagName] - - if (flag.type ==='option' && flag.options) { - // generate completions for `flag.options` array - if (flag.char) { - flagsComp+=util.format( - flagCompWithOptsTpl, - `(-${flag.char} --${flag.name})`, - `{-${flag.char},--${flagName}}`, - sanitizeDescription(flag.summary ||flag.description), - flagName, - flag.options.join(' ') - ) + const f = c.flags[flagName] + + let flagCompValue = ' ' + + // Flag.Option + if (f.type ==='option') { + if (f.char) { + if (f.multiple) { + flagCompValue += `*{-${f.char},--${f.name}}` + } else { + flagCompValue += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` + } + + flagCompValue += `"[${sanitizeDescription(f.summary || f.description)}]` + + if (f.options) { + flagCompValue += `:${f.name} options:(${f.options?.join(' ')})"` + } else { + flagCompValue += ':"' + } } else { - flagsComp+=util.format( - flagCompWithCharWithOptsTpl, - `--${flag.name}`, - sanitizeDescription(flag.summary ||flag.description), - flagName, - flag.options.join(' ') - ) + if (f.multiple) { + flagCompValue += '"*"' + } + + flagCompValue += `--${f.name}"[${sanitizeDescription(f.summary || f.description)}]:` + + if (f.options) { + flagCompValue += `${f.name} options:(${f.options.join(' ')})"` + } else { + flagCompValue += '"' + } } } else { - if (flag.char) { - flagsComp+=` "(-${flag.char} --${flag.name})"{-${flag.char},--${flagName}}"[${sanitizeDescription(flag.summary ||flag.description)}]" \\\n` + // Flag.Boolean + if (f.char) { + flagCompValue += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${sanitizeDescription(f.summary || f.description)}]"` } else { - flagsComp+=` --${flagName}"[${sanitizeDescription(flag.summary || flag.description)}]" \\\n` + flagCompValue+=`--${f.name}"[${sanitizeDescription(f.summary || f.description)}]"` } - } + } + + flagCompValue += ` \\\n` + flagsComp += flagCompValue } valuesBlock+=util.format(valueTemplate,subArg,c.description) From d64c5ccefad762c8cdf807174ba9ae75f6a8d44e Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 16 Jan 2023 18:12:46 -0300 Subject: [PATCH 05/43] feat: file completion if no options are defined --- src/commands/autocomplete/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 767b6eee..67b152f4 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -315,7 +315,7 @@ compinit;\n` if (f.options) { flagCompValue += `:${f.name} options:(${f.options?.join(' ')})"` } else { - flagCompValue += ':"' + flagCompValue += ':file:_files"' } } else { if (f.multiple) { From 438396f817023a389b77bcade0949f2ecc17dc4e Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Thu, 19 Jan 2023 19:46:21 -0300 Subject: [PATCH 06/43] fix: complete files by default --- src/commands/autocomplete/create.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 67b152f4..ff764cf0 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -327,7 +327,7 @@ compinit;\n` if (f.options) { flagCompValue += `${f.name} options:(${f.options.join(' ')})"` } else { - flagCompValue += '"' + flagCompValue += 'file:_files"' } } } else { @@ -342,6 +342,7 @@ compinit;\n` flagCompValue += ` \\\n` flagsComp += flagCompValue } + flagsComp += ' "*: :_files"' valuesBlock+=util.format(valueTemplate,subArg,c.description) From 0d626fb77d5288c011144fcf7f5d4fdac7848c36 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 09:54:27 -0300 Subject: [PATCH 07/43] fix: wrap flag repeat spec in double quotes --- src/commands/autocomplete/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index ff764cf0..17748d4d 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -305,7 +305,7 @@ compinit;\n` if (f.type ==='option') { if (f.char) { if (f.multiple) { - flagCompValue += `*{-${f.char},--${f.name}}` + flagCompValue += `"*"{-${f.char},--${f.name}}` } else { flagCompValue += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` } From 7e14bd683ba05b7345bf7ce0913384c8bc356989 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 11:55:34 -0300 Subject: [PATCH 08/43] chore: refactor --- src/commands/autocomplete/create.ts | 121 +++++++++++++++------------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 17748d4d..805d6e07 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -270,6 +270,71 @@ compinit;\n` } }) + const genZshFlagArgumentsBlock = (flags?: { [name: string]: Interfaces.Command.Flag; }): string => { + // if a command doesn't have flags make it only complete files + if (!flags) return '_arguments "*: :_files"' + + const flagNames = Object.keys(flags) + + // `-S`: + // Do not complete flags after a ‘--’ appearing on the line, and ignore the ‘--’. For example, with -S, in the line: + // foobar -x -- -y + // the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither. + let argumentsBlock = '_arguments -S \\\n' + + for (const flagName of flagNames){ + const f = flags[flagName] + f.summary = sanitizeDescription(f.summary || f.description) + + let flagSpec = '' + + if (f.type ==='option') { + if (f.char) { + if (f.multiple) { + // this flag can be present multiple times on the line + flagSpec += `"*"{-${f.char},--${f.name}}` + } else { + flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` + } + + flagSpec += `"[${f.summary}]` + + if (f.options) { + flagSpec += `:${f.name} options:(${f.options?.join(' ')})"` + } else { + flagSpec += ':file:_files"' + } + } else { + if (f.multiple) { + // this flag can be present multiple times on the line + flagSpec += '"*"' + } + + flagSpec += `--${f.name}"[${f.summary}]:` + + if (f.options) { + flagSpec += `${f.name} options:(${f.options.join(' ')})"` + } else { + flagSpec += 'file:_files"' + } + } + } else { + // Flag.Boolean + if (f.char) { + flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"` + } else { + flagSpec+=`--${f.name}"[${f.summary}]"` + } + } + + flagSpec += ' \\\n' + argumentsBlock += flagSpec + } + // complete files if `-` is not present on the current line + argumentsBlock+='"*: :_files"' + + return argumentsBlock + } const genZshTopicCompFun = (id: string): string => { const underscoreSepId = id.replace(/:/g,'_') @@ -292,62 +357,10 @@ compinit;\n` .forEach(c => { const subArg = c.id.split(':')[depth] - // TODO: skip commands without flags - const flagNames = Object.keys(c.flags) - let flagsComp='' - - for (const flagName of flagNames){ - const f = c.flags[flagName] - - let flagCompValue = ' ' - - // Flag.Option - if (f.type ==='option') { - if (f.char) { - if (f.multiple) { - flagCompValue += `"*"{-${f.char},--${f.name}}` - } else { - flagCompValue += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` - } - - flagCompValue += `"[${sanitizeDescription(f.summary || f.description)}]` - - if (f.options) { - flagCompValue += `:${f.name} options:(${f.options?.join(' ')})"` - } else { - flagCompValue += ':file:_files"' - } - } else { - if (f.multiple) { - flagCompValue += '"*"' - } - - flagCompValue += `--${f.name}"[${sanitizeDescription(f.summary || f.description)}]:` - - if (f.options) { - flagCompValue += `${f.name} options:(${f.options.join(' ')})"` - } else { - flagCompValue += 'file:_files"' - } - } - } else { - // Flag.Boolean - if (f.char) { - flagCompValue += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${sanitizeDescription(f.summary || f.description)}]"` - } else { - flagCompValue+=`--${f.name}"[${sanitizeDescription(f.summary || f.description)}]"` - } - } - - flagCompValue += ` \\\n` - flagsComp += flagCompValue - } - flagsComp += ' "*: :_files"' - valuesBlock+=util.format(valueTemplate,subArg,c.description) - const flagArgsTemplate = ` "%s")\n _arguments -S \\\n%s\n ;;\n` - argsBlock+= util.format(flagArgsTemplate,subArg,flagsComp) + const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) }) const topicCompFunc = From 264995dc83d39276acaa00f926aec9481ced8e67 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 12:05:35 -0300 Subject: [PATCH 09/43] fix: skip hidden commands --- src/commands/autocomplete/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 805d6e07..901aa7bf 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -233,8 +233,8 @@ compinit;\n` // TODO: // * include command aliases - // * ignore hidden commands const commands = this.config.commands + .filter(c => !c.hidden) .map(c=>{ c.description = sanitizeDescription(c.summary || c.description || '') return c From defca503fa6bccd33a382ed56edfb6c4246ed056 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 12:07:53 -0300 Subject: [PATCH 10/43] chore: remove comments --- src/commands/autocomplete/create.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 901aa7bf..884a3e33 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -144,29 +144,6 @@ compinit;\n` return this._commands } - // TODO: handle commands without flags - // private genZshCmdFlagsCompFun(id: string, skipFunc: boolean = false): string { - // const command = this.config.findCommand(id,{must:true}) - // - // const flagNames = Object.keys(command.flags) - // let flagsComp='' - // - // for (const flagName of flagNames){ - // const flag = command.flags[flagName] - // - // if (flag.char) { - // flagsComp+=` {-${flag.char},--${flagName}}'[${sanitizeDescription(flag.summary ||flag.description)}]' \\\n` - // } else { - // flagsComp+=` --${flagName}'[${sanitizeDescription(flag.summary || flag.description)}]' \\\n` - // } - // } - // if (skipFunc) { - // return flagsComp - // } - // - // return util.format(`_${this.cliBin}_${command.id.replace(/:/g,'_')}() { \n _arguments -S \\\n%s}`, flagsComp) - // } - private genZshFlagSpecs(Klass: any): string { return Object.keys(Klass.flags || {}) .filter(flag => Klass.flags && !Klass.flags[flag].hidden) From 1807e7d64e5dd0feed1b3a265349fdb6af9fef77 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 14:42:10 -0300 Subject: [PATCH 11/43] chore: refactor --- src/commands/autocomplete/create.ts | 42 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 884a3e33..f069c769 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -205,7 +205,6 @@ compinit;\n` private get zshCompletionWithSpacesFunction(): string { - const valueTemplate = ` "%s[%s]" \\\n` const argTemplate = ` "%s")\n %s\n ;;\n` // TODO: @@ -247,6 +246,19 @@ compinit;\n` } }) + // alternative name: tommands + const cotopics=[] + + for (const cmd of commands) { + for (const topic of topics) { + if (cmd.id === topic.name) { + cotopics.push(cmd.id) + } + } + } + console.log(cotopics) + + const genZshFlagArgumentsBlock = (flags?: { [name: string]: Interfaces.Command.Flag; }): string => { // if a command doesn't have flags make it only complete files if (!flags) return '_arguments "*: :_files"' @@ -313,19 +325,33 @@ compinit;\n` return argumentsBlock } + const genZshValuesBlock = (subArgs: {arg: string, summary?: string}[]): string => { + let valuesBlock = '_values "completions" \\\n' + + subArgs.forEach(subArg => { + valuesBlock += `"${subArg.arg}[${subArg.summary}]" \\\n` + }) + + return valuesBlock + } + const genZshTopicCompFun = (id: string): string => { const underscoreSepId = id.replace(/:/g,'_') const depth = id.split(':').length - let valuesBlock = '' let argsBlock = '' - + + const subArgs: {arg: string, summary?: string}[] = [] topics .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) .forEach(t => { const subArg = t.name.split(':')[depth] - valuesBlock+=util.format(valueTemplate,subArg,t.description) + subArgs.push({ + arg: subArg, + summary: t.description + }) + argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) }) @@ -334,7 +360,10 @@ compinit;\n` .forEach(c => { const subArg = c.id.split(':')[depth] - valuesBlock+=util.format(valueTemplate,subArg,c.description) + subArgs.push({ + arg: subArg, + summary: c.description + }) const flagArgsTemplate = ` "%s")\n %s\n ;;\n` argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) @@ -348,7 +377,6 @@ compinit;\n` case "$state" in cmds) - _values "${this.cliBin} command" \\ %s ;; args) @@ -360,7 +388,7 @@ compinit;\n` } ` - return util.format(topicCompFunc, valuesBlock, argsBlock) + return util.format(topicCompFunc, genZshValuesBlock(subArgs), argsBlock) } const compFunc = From abbdd7f6efbd41f91c50ba46bf9c579d9613ba58 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 14:43:26 -0300 Subject: [PATCH 12/43] chore: main func --- src/commands/autocomplete/create.ts | 84 ++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index f069c769..9e92b4b9 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -394,15 +394,9 @@ compinit;\n` const compFunc = `#compdef ${this.cliBin} -${topics.map(t=> { - if (t.name.includes('data')) { - return genZshTopicCompFun(t.name) - } -}).join('\n')} - -${genZshTopicCompFun('force')} +${topics.map(t=> genZshTopicCompFun(t.name)).join('\n')} -_sfdx() { +_${this.cliBin}() { local line state _arguments -C "1: :->cmds" "*::arg:->args" @@ -410,16 +404,80 @@ _sfdx() { case "$state" in cmds) _values "${this.cliBin} command" \\ - "force[force stuff]" \\ - "data[data stuff]" + "deploy[deploy]" \\ + "data[data]" \\ + "alias[alias]" \\ + "community[community]" \\ + "config[config]" \\ + "env[env]" \\ + "generate[generate]" \\ + "info[info]" \\ + "limits[limits]" \\ + "login[login]" \\ + "logout[logout]" \\ + "org[org]" \\ + "plugins[plugins]" \\ + "retrieve[retrieve]" \\ + "run[run]" \\ + "object[object]" \\ + "update[update]" \\ + "whoami[whoami]" \\ ;; args) case $line[1] in data) - _sfdx_data + _sf_data + ;; + deploy) + _sf_deploy + ;; + alias) + _sf_alias + ;; + community) + _sf_community + ;; + config) + _sf_config + ;; + env) + _sf_env + ;; + generate) + _sf_generate + ;; + info) + _sf_info + ;; + limits) + _sf_limits + ;; + login) + _sf_login + ;; + logout) + _sf_logout + ;; + org) + _sf_org + ;; + plugins) + _sf_plugins + ;; + retrieve) + _sf_retrieve + ;; + run) + _sf_run + ;; + object) + _sf_sobject + ;; + update) + _sf_update ;; - force) - _sfdx_force + whoami) + _sf_whoami ;; esac ;; From 8f44d64efd60abeea7bc5d5ff82a7259416b792b Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 20 Jan 2023 15:45:25 -0300 Subject: [PATCH 13/43] chore: declare contex/state parameters --- src/commands/autocomplete/create.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 9e92b4b9..dacead4c 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -371,7 +371,8 @@ compinit;\n` const topicCompFunc = `_${this.cliBin}_${underscoreSepId}() { - local line state + local context state state_descr line + typeset -A opt_args _arguments -C "1: :->cmds" "*::arg:->args" From 8f7672c501b109667d62ddc620cd7d9502b4184e Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 11:36:34 -0300 Subject: [PATCH 14/43] feat: support cotopics --- src/commands/autocomplete/create.ts | 242 ++++++++++++++++------------ 1 file changed, 138 insertions(+), 104 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index dacead4c..cc5bab28 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -246,17 +246,15 @@ compinit;\n` } }) - // alternative name: tommands - const cotopics=[] + const coTopics:string[]=[] - for (const cmd of commands) { - for (const topic of topics) { - if (cmd.id === topic.name) { - cotopics.push(cmd.id) + for (const topic of topics) { + for (const cmd of commands) { + if (topic.name === cmd.id) { + coTopics.push(topic.name) } } } - console.log(cotopics) const genZshFlagArgumentsBlock = (flags?: { [name: string]: Interfaces.Command.Flag; }): string => { @@ -325,11 +323,11 @@ compinit;\n` return argumentsBlock } - const genZshValuesBlock = (subArgs: {arg: string, summary?: string}[]): string => { + const genZshValuesBlock = (subArgs: {id: string, summary?: string}[]): string => { let valuesBlock = '_values "completions" \\\n' subArgs.forEach(subArg => { - valuesBlock += `"${subArg.arg}[${subArg.summary}]" \\\n` + valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n` }) return valuesBlock @@ -339,18 +337,64 @@ compinit;\n` const underscoreSepId = id.replace(/:/g,'_') const depth = id.split(':').length - let argsBlock = '' - - const subArgs: {arg: string, summary?: string}[] = [] - topics - .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) - .forEach(t => { - const subArg = t.name.split(':')[depth] - - subArgs.push({ - arg: subArg, - summary: t.description + const isCotopic = coTopics.includes(id) + + if (isCotopic) { + const compFuncName = `${this.cliBin}_${underscoreSepId}` + + const coTopicCompFunc = +`_${compFuncName}() { + _${compFuncName}_flags() { + local context state state_descr line + typeset -A opt_args + + ${genZshFlagArgumentsBlock(commands.find(c=>c.id===id)?.flags)} + } + + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*: :->args" + + case "$state" in + cmds) + if [[ "\${words[CURRENT]}" == -* ]]; then + _${compFuncName}_flags + else +%s + fi + ;; + args) + case $line[1] in +%s + *) + _${compFuncName}_flags + ;; + esac + ;; + esac +} +` + const subArgs: {id: string, summary?: string}[] = [] + + let argsBlock = '' + commands + .filter(c => c.id === id && c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + subArgs.push({ + id: c.id.split(':')[depth], + summary: c.description + }) }) + topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: t.description + }) argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) }) @@ -360,16 +404,49 @@ compinit;\n` .forEach(c => { const subArg = c.id.split(':')[depth] - subArgs.push({ - arg: subArg, - summary: c.description + subArgs.push({ + id: subArg, + summary: c.description + }) + + const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) }) - const flagArgsTemplate = ` "%s")\n %s\n ;;\n` - argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) - }) + return util.format(coTopicCompFunc, genZshValuesBlock(subArgs), argsBlock) + } else { + let argsBlock = '' + + const subArgs: {id: string, summary?: string}[] = [] + topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: t.description + }) + + argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) + }) + + commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + if (isCotopic) return + const subArg = c.id.split(':')[depth] - const topicCompFunc = + subArgs.push({ + id: subArg, + summary: c.description + }) + + const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) + }) + + const topicCompFunc = `_${this.cliBin}_${underscoreSepId}() { local context state state_descr line typeset -A opt_args @@ -388,8 +465,39 @@ compinit;\n` esac } ` + return util.format(topicCompFunc, genZshValuesBlock(subArgs), argsBlock) + } + } - return util.format(topicCompFunc, genZshValuesBlock(subArgs), argsBlock) + const firstArgs: {id: string, summary?: string}[] = [] + + topics.forEach(t=>{ + if(!t.name.includes(':')) firstArgs.push({ + id: t.name, + summary: t.description + }) + }) + commands.forEach(c => { + if(!firstArgs.find(a=> a.id === c.id) && !c.id.includes(':')) firstArgs.push({ + id: c.id, + summary: c.description + }) + }) + + const mainCaseBlock = () => { + // case $line[1] in + // ${mainCaseBlock()} + // esac + // ;; + let caseBlock = 'case $line[1] in\n' + + firstArgs.forEach(arg=>{ + caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` + }) + + caseBlock+='esac\n;;' + + return caseBlock } const compFunc = @@ -404,84 +512,10 @@ _${this.cliBin}() { case "$state" in cmds) - _values "${this.cliBin} command" \\ - "deploy[deploy]" \\ - "data[data]" \\ - "alias[alias]" \\ - "community[community]" \\ - "config[config]" \\ - "env[env]" \\ - "generate[generate]" \\ - "info[info]" \\ - "limits[limits]" \\ - "login[login]" \\ - "logout[logout]" \\ - "org[org]" \\ - "plugins[plugins]" \\ - "retrieve[retrieve]" \\ - "run[run]" \\ - "object[object]" \\ - "update[update]" \\ - "whoami[whoami]" \\ + ${genZshValuesBlock(firstArgs)} ;; args) - case $line[1] in - data) - _sf_data - ;; - deploy) - _sf_deploy - ;; - alias) - _sf_alias - ;; - community) - _sf_community - ;; - config) - _sf_config - ;; - env) - _sf_env - ;; - generate) - _sf_generate - ;; - info) - _sf_info - ;; - limits) - _sf_limits - ;; - login) - _sf_login - ;; - logout) - _sf_logout - ;; - org) - _sf_org - ;; - plugins) - _sf_plugins - ;; - retrieve) - _sf_retrieve - ;; - run) - _sf_run - ;; - object) - _sf_sobject - ;; - update) - _sf_update - ;; - whoami) - _sf_whoami - ;; - esac - ;; + ${mainCaseBlock()} esac } From 6ec795ed47eb201a4f2a3641da108b4ba4e3b459 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 15:21:40 -0300 Subject: [PATCH 15/43] fix: generate flag comp for top-level commands --- src/commands/autocomplete/create.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index cc5bab28..d18eb6b4 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -485,12 +485,19 @@ compinit;\n` }) const mainCaseBlock = () => { - // case $line[1] in - // ${mainCaseBlock()} - // esac - // ;; let caseBlock = 'case $line[1] in\n' + for (const arg of firstArgs) { + if (!coTopics.includes(arg.id)) { + const cmd = commands.find(c=>c.id===arg.id) + if (cmd) { + caseBlock += `${arg.id})\n ${genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + } + } else { + caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` + } + } + firstArgs.forEach(arg=>{ caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` }) From a83ce0c5284217bf1acc85bd44dae039e0985eee Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 15:38:36 -0300 Subject: [PATCH 16/43] fix: skip hidden flags --- src/commands/autocomplete/create.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index d18eb6b4..e014325e 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -271,6 +271,10 @@ compinit;\n` for (const flagName of flagNames){ const f = flags[flagName] + + // skip hidden flags + if (f.hidden) continue + f.summary = sanitizeDescription(f.summary || f.description) let flagSpec = '' From cba5ed6d2bff7d8d6597f473d34a7e7e2c952c4c Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 16:04:48 -0300 Subject: [PATCH 17/43] chore: refactor --- src/commands/autocomplete/create.ts | 32 +++++++---------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index e014325e..5eb5b155 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -207,24 +207,6 @@ compinit;\n` private get zshCompletionWithSpacesFunction(): string { const argTemplate = ` "%s")\n %s\n ;;\n` - // TODO: - // * include command aliases - const commands = this.config.commands - .filter(c => !c.hidden) - .map(c=>{ - c.description = sanitizeDescription(c.summary || c.description || '') - return c - }) - .sort((a, b) => { - if (a.id < b.id) { - return -1; - } - if (a.id > b.id) { - return 1; - } - return 0; - }); - let topics = this.config.topics.filter((topic: Interfaces.Topic) => { // it is assumed a topic has a child if it has children const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) @@ -249,7 +231,7 @@ compinit;\n` const coTopics:string[]=[] for (const topic of topics) { - for (const cmd of commands) { + for (const cmd of this.commands) { if (topic.name === cmd.id) { coTopics.push(topic.name) } @@ -352,7 +334,7 @@ compinit;\n` local context state state_descr line typeset -A opt_args - ${genZshFlagArgumentsBlock(commands.find(c=>c.id===id)?.flags)} + ${genZshFlagArgumentsBlock(this.commands.find(c=>c.id===id)?.flags)} } local context state state_descr line @@ -382,7 +364,7 @@ compinit;\n` const subArgs: {id: string, summary?: string}[] = [] let argsBlock = '' - commands + this.commands .filter(c => c.id === id && c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) .forEach(c => { subArgs.push({ @@ -403,7 +385,7 @@ compinit;\n` argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) }) - commands + this.commands .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) .forEach(c => { const subArg = c.id.split(':')[depth] @@ -435,7 +417,7 @@ compinit;\n` argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) }) - commands + this.commands .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) .forEach(c => { if (isCotopic) return @@ -481,7 +463,7 @@ compinit;\n` summary: t.description }) }) - commands.forEach(c => { + this.commands.forEach(c => { if(!firstArgs.find(a=> a.id === c.id) && !c.id.includes(':')) firstArgs.push({ id: c.id, summary: c.description @@ -493,7 +475,7 @@ compinit;\n` for (const arg of firstArgs) { if (!coTopics.includes(arg.id)) { - const cmd = commands.find(c=>c.id===arg.id) + const cmd = this.commands.find(c=>c.id===arg.id) if (cmd) { caseBlock += `${arg.id})\n ${genZshFlagArgumentsBlock(cmd.flags)} ;; \n` } From a83b00311909664d05b539de96a4f4acc18d9351 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 16:38:43 -0300 Subject: [PATCH 18/43] fix: prefer summary over description --- src/commands/autocomplete/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 5eb5b155..bef07411 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -117,7 +117,7 @@ compinit;\n` p.commands.forEach(c => { try { if (c.hidden) return - const description = sanitizeDescription(c.description || '') + const description = sanitizeDescription(c.summary || c.description || '') const flags = c.flags cmds.push({ id: c.id, From 543766e054497c8c457c1aa7f98ab9432e088e69 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 16:55:37 -0300 Subject: [PATCH 19/43] chore: refactor --- src/commands/autocomplete/create.ts | 37 ++++++++++++----------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index bef07411..d3242777 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -320,6 +320,8 @@ compinit;\n` } const genZshTopicCompFun = (id: string): string => { + const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + const underscoreSepId = id.replace(/:/g,'_') const depth = id.split(':').length @@ -364,14 +366,7 @@ compinit;\n` const subArgs: {id: string, summary?: string}[] = [] let argsBlock = '' - this.commands - .filter(c => c.id === id && c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - subArgs.push({ - id: c.id.split(':')[depth], - summary: c.description - }) - }) + topics .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) .forEach(t => { @@ -385,19 +380,18 @@ compinit;\n` argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) }) - this.commands - .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - const subArg = c.id.split(':')[depth] + this.commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + const subArg = c.id.split(':')[depth] - subArgs.push({ - id: subArg, - summary: c.description - }) + subArgs.push({ + id: subArg, + summary: c.description + }) - const flagArgsTemplate = ` "%s")\n %s\n ;;\n` - argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) - }) + argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) + }) return util.format(coTopicCompFunc, genZshValuesBlock(subArgs), argsBlock) } else { @@ -428,7 +422,6 @@ compinit;\n` summary: c.description }) - const flagArgsTemplate = ` "%s")\n %s\n ;;\n` argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) }) @@ -470,7 +463,7 @@ compinit;\n` }) }) - const mainCaseBlock = () => { + const mainArgsCaseBlock = () => { let caseBlock = 'case $line[1] in\n' for (const arg of firstArgs) { @@ -508,7 +501,7 @@ _${this.cliBin}() { ${genZshValuesBlock(firstArgs)} ;; args) - ${mainCaseBlock()} + ${mainArgsCaseBlock()} esac } From a1412d68b2bbe4e384c61f5836a2d863765a5608 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 18:27:44 -0300 Subject: [PATCH 20/43] fix: inline/skip flag comp for top-level args --- src/commands/autocomplete/create.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index d3242777..5166ecbf 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -14,7 +14,7 @@ const debug = require('debug')('autocomplete:create') type CommandCompletion = { id: string; description: string; - flags: any; + flags?: any; } function sanitizeDescription(description?: string): string { @@ -467,20 +467,22 @@ compinit;\n` let caseBlock = 'case $line[1] in\n' for (const arg of firstArgs) { - if (!coTopics.includes(arg.id)) { + if (coTopics.includes(arg.id)) { + // coTopics already have a completion function. + caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` + } else { const cmd = this.commands.find(c=>c.id===arg.id) - if (cmd) { - caseBlock += `${arg.id})\n ${genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + + // if it's a command and has flags, inline flag completion statement. + if (cmd && Object.keys(cmd.flags).length > 0) { + caseBlock += `${arg.id})\n${genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + } else { + // it's a topic, redirect to its completion function. + caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` } - } else { - caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` } } - firstArgs.forEach(arg=>{ - caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` - }) - caseBlock+='esac\n;;' return caseBlock From e2600692788b6a9506b59e00e2d24c968a9afd5d Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 18:38:24 -0300 Subject: [PATCH 21/43] fix: make arg spec to set `words/CURRENT` [skip ci] --- src/commands/autocomplete/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 5166ecbf..b0fc6464 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -342,7 +342,7 @@ compinit;\n` local context state state_descr line typeset -A opt_args - _arguments -C "1: :->cmds" "*: :->args" + _arguments -C "1: :->cmds" "*::arg:->args" case "$state" in cmds) From dfb0da048b315df71d99ad68ffff0aa98a63834d Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 18:47:55 -0300 Subject: [PATCH 22/43] chore: remove lodash import --- src/commands/autocomplete/create.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index b0fc6464..9c9a2482 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -2,7 +2,6 @@ import * as path from 'path' import * as util from 'util' import * as fs from 'fs-extra' -import * as _ from 'lodash' import bashAutocomplete from '../../autocomplete/bash' import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces' From 895e5d35d48952cc611734ceb13c4f7eb4cb5fe0 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 24 Jan 2023 18:51:07 -0300 Subject: [PATCH 23/43] fix: don't gen flag case block if cmd=cotopic [no ci] --- src/commands/autocomplete/create.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 9c9a2482..16c30951 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -382,6 +382,7 @@ compinit;\n` this.commands .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) .forEach(c => { + if (coTopics.includes(c.id)) return const subArg = c.id.split(':')[depth] subArgs.push({ @@ -413,7 +414,7 @@ compinit;\n` this.commands .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) .forEach(c => { - if (isCotopic) return + if (coTopics.includes(c.id)) return const subArg = c.id.split(':')[depth] subArgs.push({ From 1300dc1c7e3b374f4908392ee596bde6628e7806 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 25 Jan 2023 11:57:25 -0300 Subject: [PATCH 24/43] chore: refactor --- src/autocomplete/zsh.ts | 398 ++++++++++++++++++++++++++++ src/commands/autocomplete/create.ts | 319 +--------------------- 2 files changed, 407 insertions(+), 310 deletions(-) create mode 100644 src/autocomplete/zsh.ts diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts new file mode 100644 index 00000000..6d739eb8 --- /dev/null +++ b/src/autocomplete/zsh.ts @@ -0,0 +1,398 @@ +import * as util from 'util' +import { Config, Interfaces } from '@oclif/core' + +type CommandCompletion = { + id: string; + summary: string; + flags: CommandFlags; +} + +type CommandFlags = { + [name: string]: Interfaces.Command.Flag; +} + +type Topic = { + name: string; + description: string; +} + +export default class ZshCompWithSpaces { + protected config: Config; + private _topics?: Topic[] + private _commands?: CommandCompletion[] + private _coTopics?: string[] + + constructor(config: Config) { + this.config = config + } + + public generate(): string { + const firstArgs: {id: string, summary?: string}[] = [] + + this.topics.forEach(t=>{ + if(!t.name.includes(':')) firstArgs.push({ + id: t.name, + summary: t.description + }) + }) + this.commands.forEach(c => { + if(!firstArgs.find(a=> a.id === c.id) && !c.id.includes(':')) firstArgs.push({ + id: c.id, + summary: c.summary + }) + }) + + const mainArgsCaseBlock = () => { + let caseBlock = 'case $line[1] in\n' + + for (const arg of firstArgs) { + if (this.coTopics.includes(arg.id)) { + // coTopics already have a completion function. + caseBlock +=`${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + } else { + const cmd = this.commands.find(c=>c.id===arg.id) + + // if it's a command and has flags, inline flag completion statement. + if (cmd && Object.keys(cmd.flags).length > 0) { + caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + } else { + // it's a topic, redirect to its completion function. + caseBlock +=`${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + } + } + } + + caseBlock+='esac\n' + + return caseBlock + } + + const compFunc = +`#compdef ${this.config.bin} + +${this.topics.map(t=> this.genZshTopicCompFun(t.name)).join('\n')} + +_${this.config.bin}() { + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + ${this.genZshValuesBlock(firstArgs)} + ;; + args) + ${mainArgsCaseBlock()} + ;; + esac +} + +_${this.config.bin} +` + return compFunc + } + + private genZshFlagArgumentsBlock(flags?: CommandFlags): string { + // if a command doesn't have flags make it only complete files + if (!flags) return '_arguments "*: :_files"' + + const flagNames = Object.keys(flags) + + // `-S`: + // Do not complete flags after a ‘--’ appearing on the line, and ignore the ‘--’. For example, with -S, in the line: + // foobar -x -- -y + // the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither. + let argumentsBlock = '_arguments -S \\\n' + + for (const flagName of flagNames){ + const f = flags[flagName] + + // skip hidden flags + if (f.hidden) continue + + f.summary = sanitizeSummary(f.summary || f.description) + + let flagSpec = '' + + if (f.type ==='option') { + if (f.char) { + if (f.multiple) { + // this flag can be present multiple times on the line + flagSpec += `"*"{-${f.char},--${f.name}}` + } else { + flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` + } + + flagSpec += `"[${f.summary}]` + + if (f.options) { + flagSpec += `:${f.name} options:(${f.options?.join(' ')})"` + } else { + flagSpec += ':file:_files"' + } + } else { + if (f.multiple) { + // this flag can be present multiple times on the line + flagSpec += '"*"' + } + + flagSpec += `--${f.name}"[${f.summary}]:` + + if (f.options) { + flagSpec += `${f.name} options:(${f.options.join(' ')})"` + } else { + flagSpec += 'file:_files"' + } + } + } else { + // Flag.Boolean + if (f.char) { + flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"` + } else { + flagSpec+=`--${f.name}"[${f.summary}]"` + } + } + + flagSpec += ' \\\n' + argumentsBlock += flagSpec + } + // complete files if `-` is not present on the current line + argumentsBlock+='"*: :_files"' + + return argumentsBlock + } + + private genZshValuesBlock(subArgs: {id: string, summary?: string}[]): string { + let valuesBlock = '_values "completions" \\\n' + + subArgs.forEach(subArg => { + valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n` + }) + + return valuesBlock + } + + private genZshTopicCompFun(id: string): string { + const coTopics:string[]=[] + + for (const topic of this.topics) { + for (const cmd of this.commands) { + if (topic.name === cmd.id) { + coTopics.push(topic.name) + } + } + } + + const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + + const underscoreSepId = id.replace(/:/g,'_') + const depth = id.split(':').length + + const isCotopic = coTopics.includes(id) + + if (isCotopic) { + const compFuncName = `${this.config.bin}_${underscoreSepId}` + + const coTopicCompFunc = +`_${compFuncName}() { + _${compFuncName}_flags() { + local context state state_descr line + typeset -A opt_args + + ${this.genZshFlagArgumentsBlock(this.commands.find(c=>c.id===id)?.flags)} + } + + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + if [[ "\${words[CURRENT]}" == -* ]]; then + _${compFuncName}_flags + else +%s + fi + ;; + args) + case $line[1] in +%s + *) + _${compFuncName}_flags + ;; + esac + ;; + esac +} +` + const subArgs: {id: string, summary?: string}[] = [] + + let argsBlock = '' + + this.topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: t.description + }) + + argsBlock+= util.format(argTemplate,subArg,`_${this.config.bin}_${underscoreSepId}_${subArg}`) + }) + + this.commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + if (coTopics.includes(c.id)) return + const subArg = c.id.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: c.summary + }) + + argsBlock+= util.format(flagArgsTemplate,subArg,this.genZshFlagArgumentsBlock(c.flags)) + }) + + return util.format(coTopicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) + } else { + let argsBlock = '' + + const subArgs: {id: string, summary?: string}[] = [] + this.topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: t.description + }) + + argsBlock+= util.format(argTemplate,subArg,`_${this.config.bin}_${underscoreSepId}_${subArg}`) + }) + + this.commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + if (coTopics.includes(c.id)) return + const subArg = c.id.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: c.summary + }) + + argsBlock+= util.format(flagArgsTemplate,subArg,this.genZshFlagArgumentsBlock(c.flags)) + }) + + const topicCompFunc = +`_${this.config.bin}_${underscoreSepId}() { + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) +%s + ;; + args) + case $line[1] in +%s + esac + ;; + esac +} +` + return util.format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) + } + } + + private get coTopics(): string [] { + if (this._coTopics) return this._coTopics + + const coTopics:string[]=[] + + for (const topic of this.topics) { + for (const cmd of this.commands) { + if (topic.name === cmd.id) { + coTopics.push(topic.name) + } + } + } + + this._coTopics = coTopics + + return this._coTopics + } + + private get topics(): Topic[] { + if (this._topics) return this._topics + + const topics = this.config.topics.filter((topic: Interfaces.Topic) => { + // it is assumed a topic has a child if it has children + const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) + return hasChild + }) + .sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }) + .map(t=> { + return { + name: t.name, + description: sanitizeSummary(t.description) + } + }) + + this._topics = topics + + return this._topics + } + + private get commands(): CommandCompletion[] { + if (this._commands) return this._commands + + const cmds: CommandCompletion[] = [] + + this.config.plugins.forEach(p => { + p.commands.forEach(c => { + if (c.hidden) return + const summary = sanitizeSummary(c.summary || c.description) + const flags = c.flags + cmds.push({ + id: c.id, + summary, + flags, + }) + }) + }) + + this._commands = cmds + + return this._commands + } +} + +const argTemplate = ` "%s")\n %s\n ;;\n` + +function sanitizeSummary(description?: string): string { + if (description === undefined) { + return '' + } + return description + .replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes + // eslint-disable-next-line no-useless-escape + .replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes + .split('\n')[0] // only use the first line +} diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 16c30951..8c34f6b8 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -4,6 +4,7 @@ import * as util from 'util' import * as fs from 'fs-extra' import bashAutocomplete from '../../autocomplete/bash' +import ZshCompWithSpaces from '../../autocomplete/zsh' import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces' import {AutocompleteBase} from '../../base' import {Interfaces } from '@oclif/core' @@ -55,7 +56,14 @@ export default class Create extends AutocompleteBase { await fs.writeFile(this.bashSetupScriptPath, this.bashSetupScript) await fs.writeFile(this.bashCompletionFunctionPath, this.bashCompletionFunction) await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript) - await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionWithSpacesFunction) + + // zsh + if (this.config.topicSeparator === ":") { + await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction) + } else { + const zshCompWithSpaces = new ZshCompWithSpaces(this.config) + await fs.writeFile(this.zshCompletionFunctionPath, zshCompWithSpaces.generate()) + } } private get bashSetupScriptPath(): string { @@ -202,315 +210,6 @@ compinit;\n` return bashScript.replace(//g, cliBin).replace(//g, this.bashCommandsWithFlagsList) } - - private get zshCompletionWithSpacesFunction(): string { - const argTemplate = ` "%s")\n %s\n ;;\n` - - let topics = this.config.topics.filter((topic: Interfaces.Topic) => { - // it is assumed a topic has a child if it has children - const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) - return hasChild - }) - .sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }) - .map(t=> { - return { - name: t.name, - description: sanitizeDescription(t.description) - } - }) - - const coTopics:string[]=[] - - for (const topic of topics) { - for (const cmd of this.commands) { - if (topic.name === cmd.id) { - coTopics.push(topic.name) - } - } - } - - - const genZshFlagArgumentsBlock = (flags?: { [name: string]: Interfaces.Command.Flag; }): string => { - // if a command doesn't have flags make it only complete files - if (!flags) return '_arguments "*: :_files"' - - const flagNames = Object.keys(flags) - - // `-S`: - // Do not complete flags after a ‘--’ appearing on the line, and ignore the ‘--’. For example, with -S, in the line: - // foobar -x -- -y - // the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither. - let argumentsBlock = '_arguments -S \\\n' - - for (const flagName of flagNames){ - const f = flags[flagName] - - // skip hidden flags - if (f.hidden) continue - - f.summary = sanitizeDescription(f.summary || f.description) - - let flagSpec = '' - - if (f.type ==='option') { - if (f.char) { - if (f.multiple) { - // this flag can be present multiple times on the line - flagSpec += `"*"{-${f.char},--${f.name}}` - } else { - flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` - } - - flagSpec += `"[${f.summary}]` - - if (f.options) { - flagSpec += `:${f.name} options:(${f.options?.join(' ')})"` - } else { - flagSpec += ':file:_files"' - } - } else { - if (f.multiple) { - // this flag can be present multiple times on the line - flagSpec += '"*"' - } - - flagSpec += `--${f.name}"[${f.summary}]:` - - if (f.options) { - flagSpec += `${f.name} options:(${f.options.join(' ')})"` - } else { - flagSpec += 'file:_files"' - } - } - } else { - // Flag.Boolean - if (f.char) { - flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"` - } else { - flagSpec+=`--${f.name}"[${f.summary}]"` - } - } - - flagSpec += ' \\\n' - argumentsBlock += flagSpec - } - // complete files if `-` is not present on the current line - argumentsBlock+='"*: :_files"' - - return argumentsBlock - } - - const genZshValuesBlock = (subArgs: {id: string, summary?: string}[]): string => { - let valuesBlock = '_values "completions" \\\n' - - subArgs.forEach(subArg => { - valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n` - }) - - return valuesBlock - } - - const genZshTopicCompFun = (id: string): string => { - const flagArgsTemplate = ` "%s")\n %s\n ;;\n` - - const underscoreSepId = id.replace(/:/g,'_') - const depth = id.split(':').length - - const isCotopic = coTopics.includes(id) - - if (isCotopic) { - const compFuncName = `${this.cliBin}_${underscoreSepId}` - - const coTopicCompFunc = -`_${compFuncName}() { - _${compFuncName}_flags() { - local context state state_descr line - typeset -A opt_args - - ${genZshFlagArgumentsBlock(this.commands.find(c=>c.id===id)?.flags)} - } - - local context state state_descr line - typeset -A opt_args - - _arguments -C "1: :->cmds" "*::arg:->args" - - case "$state" in - cmds) - if [[ "\${words[CURRENT]}" == -* ]]; then - _${compFuncName}_flags - else -%s - fi - ;; - args) - case $line[1] in -%s - *) - _${compFuncName}_flags - ;; - esac - ;; - esac -} -` - const subArgs: {id: string, summary?: string}[] = [] - - let argsBlock = '' - - topics - .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) - .forEach(t => { - const subArg = t.name.split(':')[depth] - - subArgs.push({ - id: subArg, - summary: t.description - }) - - argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) - }) - - this.commands - .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - if (coTopics.includes(c.id)) return - const subArg = c.id.split(':')[depth] - - subArgs.push({ - id: subArg, - summary: c.description - }) - - argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) - }) - - return util.format(coTopicCompFunc, genZshValuesBlock(subArgs), argsBlock) - } else { - let argsBlock = '' - - const subArgs: {id: string, summary?: string}[] = [] - topics - .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) - .forEach(t => { - const subArg = t.name.split(':')[depth] - - subArgs.push({ - id: subArg, - summary: t.description - }) - - argsBlock+= util.format(argTemplate,subArg,`_${this.cliBin}_${underscoreSepId}_${subArg}`) - }) - - this.commands - .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - if (coTopics.includes(c.id)) return - const subArg = c.id.split(':')[depth] - - subArgs.push({ - id: subArg, - summary: c.description - }) - - argsBlock+= util.format(flagArgsTemplate,subArg,genZshFlagArgumentsBlock(c.flags)) - }) - - const topicCompFunc = -`_${this.cliBin}_${underscoreSepId}() { - local context state state_descr line - typeset -A opt_args - - _arguments -C "1: :->cmds" "*::arg:->args" - - case "$state" in - cmds) -%s - ;; - args) - case $line[1] in -%s - esac - ;; - esac -} -` - return util.format(topicCompFunc, genZshValuesBlock(subArgs), argsBlock) - } - } - - const firstArgs: {id: string, summary?: string}[] = [] - - topics.forEach(t=>{ - if(!t.name.includes(':')) firstArgs.push({ - id: t.name, - summary: t.description - }) - }) - this.commands.forEach(c => { - if(!firstArgs.find(a=> a.id === c.id) && !c.id.includes(':')) firstArgs.push({ - id: c.id, - summary: c.description - }) - }) - - const mainArgsCaseBlock = () => { - let caseBlock = 'case $line[1] in\n' - - for (const arg of firstArgs) { - if (coTopics.includes(arg.id)) { - // coTopics already have a completion function. - caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` - } else { - const cmd = this.commands.find(c=>c.id===arg.id) - - // if it's a command and has flags, inline flag completion statement. - if (cmd && Object.keys(cmd.flags).length > 0) { - caseBlock += `${arg.id})\n${genZshFlagArgumentsBlock(cmd.flags)} ;; \n` - } else { - // it's a topic, redirect to its completion function. - caseBlock +=`${arg.id})\n _${this.cliBin}_${arg.id} \n ;;\n` - } - } - } - - caseBlock+='esac\n;;' - - return caseBlock - } - - const compFunc = -`#compdef ${this.cliBin} - -${topics.map(t=> genZshTopicCompFun(t.name)).join('\n')} - -_${this.cliBin}() { - local line state - - _arguments -C "1: :->cmds" "*::arg:->args" - - case "$state" in - cmds) - ${genZshValuesBlock(firstArgs)} - ;; - args) - ${mainArgsCaseBlock()} - esac -} - -_${this.cliBin} -` - return compFunc - } private get zshCompletionFunction(): string { const cliBin = this.cliBin const allCommandsMeta = this.genAllCommandsMetaString From 38f3061e7b236a116cf6888ca778cf9cb8e49e66 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 25 Jan 2023 12:23:16 -0300 Subject: [PATCH 25/43] fix: lint fix --- src/autocomplete/zsh.ts | 216 ++++++++++++++-------------- src/commands/autocomplete/create.ts | 4 +- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 6d739eb8..ed340772 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -1,5 +1,18 @@ import * as util from 'util' -import { Config, Interfaces } from '@oclif/core' +import {Config, Interfaces} from '@oclif/core' + +function sanitizeSummary(description?: string): string { + if (description === undefined) { + return '' + } + return description + .replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes + // eslint-disable-next-line no-useless-escape + .replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes + .split('\n')[0] // only use the first line +} + +const argTemplate = ' "%s")\n %s\n ;;\n' type CommandCompletion = { id: string; @@ -7,8 +20,8 @@ type CommandCompletion = { flags: CommandFlags; } -type CommandFlags = { - [name: string]: Interfaces.Command.Flag; +type CommandFlags = { + [name: string]: Interfaces.Command.Flag; } type Topic = { @@ -18,8 +31,11 @@ type Topic = { export default class ZshCompWithSpaces { protected config: Config; + private _topics?: Topic[] + private _commands?: CommandCompletion[] + private _coTopics?: string[] constructor(config: Config) { @@ -27,18 +43,18 @@ export default class ZshCompWithSpaces { } public generate(): string { - const firstArgs: {id: string, summary?: string}[] = [] + const firstArgs: {id: string; summary?: string}[] = [] - this.topics.forEach(t=>{ - if(!t.name.includes(':')) firstArgs.push({ + this.topics.forEach(t => { + if (!t.name.includes(':')) firstArgs.push({ id: t.name, - summary: t.description + summary: t.description, }) }) - this.commands.forEach(c => { - if(!firstArgs.find(a=> a.id === c.id) && !c.id.includes(':')) firstArgs.push({ + this.commands.forEach(c => { + if (!firstArgs.find(a => a.id === c.id) && !c.id.includes(':')) firstArgs.push({ id: c.id, - summary: c.summary + summary: c.summary, }) }) @@ -48,21 +64,21 @@ export default class ZshCompWithSpaces { for (const arg of firstArgs) { if (this.coTopics.includes(arg.id)) { // coTopics already have a completion function. - caseBlock +=`${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` } else { - const cmd = this.commands.find(c=>c.id===arg.id) + const cmd = this.commands.find(c => c.id === arg.id) // if it's a command and has flags, inline flag completion statement. if (cmd && Object.keys(cmd.flags).length > 0) { caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` } else { // it's a topic, redirect to its completion function. - caseBlock +=`${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` } } } - caseBlock+='esac\n' + caseBlock += 'esac\n' return caseBlock } @@ -70,7 +86,7 @@ export default class ZshCompWithSpaces { const compFunc = `#compdef ${this.config.bin} -${this.topics.map(t=> this.genZshTopicCompFun(t.name)).join('\n')} +${this.topics.map(t => this.genZshTopicCompFun(t.name)).join('\n')} _${this.config.bin}() { local context state state_descr line @@ -105,7 +121,7 @@ _${this.config.bin} // the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither. let argumentsBlock = '_arguments -S \\\n' - for (const flagName of flagNames){ + for (const flagName of flagNames) { const f = flags[flagName] // skip hidden flags @@ -115,7 +131,7 @@ _${this.config.bin} let flagSpec = '' - if (f.type ==='option') { + if (f.type === 'option') { if (f.char) { if (f.multiple) { // this flag can be present multiple times on the line @@ -145,25 +161,24 @@ _${this.config.bin} flagSpec += 'file:_files"' } } + } else if (f.char) { + // Flag.Boolean + flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"` } else { // Flag.Boolean - if (f.char) { - flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"` - } else { - flagSpec+=`--${f.name}"[${f.summary}]"` - } + flagSpec += `--${f.name}"[${f.summary}]"` } flagSpec += ' \\\n' argumentsBlock += flagSpec } // complete files if `-` is not present on the current line - argumentsBlock+='"*: :_files"' + argumentsBlock += '"*: :_files"' - return argumentsBlock + return argumentsBlock } - private genZshValuesBlock(subArgs: {id: string, summary?: string}[]): string { + private genZshValuesBlock(subArgs: {id: string; summary?: string}[]): string { let valuesBlock = '_values "completions" \\\n' subArgs.forEach(subArg => { @@ -174,8 +189,8 @@ _${this.config.bin} } private genZshTopicCompFun(id: string): string { - const coTopics:string[]=[] - + const coTopics: string[] = [] + for (const topic of this.topics) { for (const cmd of this.commands) { if (topic.name === cmd.id) { @@ -184,9 +199,9 @@ _${this.config.bin} } } - const flagArgsTemplate = ` "%s")\n %s\n ;;\n` + const flagArgsTemplate = ' "%s")\n %s\n ;;\n' - const underscoreSepId = id.replace(/:/g,'_') + const underscoreSepId = id.replace(/:/g, '_') const depth = id.split(':').length const isCotopic = coTopics.includes(id) @@ -200,7 +215,7 @@ _${this.config.bin} local context state state_descr line typeset -A opt_args - ${this.genZshFlagArgumentsBlock(this.commands.find(c=>c.id===id)?.flags)} + ${this.genZshFlagArgumentsBlock(this.commands.find(c => c.id === id)?.flags)} } local context state state_descr line @@ -227,70 +242,70 @@ _${this.config.bin} esac } ` - const subArgs: {id: string, summary?: string}[] = [] + const subArgs: {id: string; summary?: string}[] = [] let argsBlock = '' this.topics - .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) - .forEach(t => { - const subArg = t.name.split(':')[depth] + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] - subArgs.push({ - id: subArg, - summary: t.description - }) + subArgs.push({ + id: subArg, + summary: t.description, + }) - argsBlock+= util.format(argTemplate,subArg,`_${this.config.bin}_${underscoreSepId}_${subArg}`) + argsBlock += util.format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`) }) this.commands - .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - if (coTopics.includes(c.id)) return - const subArg = c.id.split(':')[depth] - - subArgs.push({ - id: subArg, - summary: c.summary - }) + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + if (coTopics.includes(c.id)) return + const subArg = c.id.split(':')[depth] + + subArgs.push({ + id: subArg, + summary: c.summary, + }) - argsBlock+= util.format(flagArgsTemplate,subArg,this.genZshFlagArgumentsBlock(c.flags)) - }) + argsBlock += util.format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags)) + }) return util.format(coTopicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) - } else { - let argsBlock = '' - - const subArgs: {id: string, summary?: string}[] = [] - this.topics - .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) - .forEach(t => { - const subArg = t.name.split(':')[depth] + } + let argsBlock = '' - subArgs.push({ - id: subArg, - summary: t.description - }) + const subArgs: {id: string; summary?: string}[] = [] + this.topics + .filter(t => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1) + .forEach(t => { + const subArg = t.name.split(':')[depth] - argsBlock+= util.format(argTemplate,subArg,`_${this.config.bin}_${underscoreSepId}_${subArg}`) - }) + subArgs.push({ + id: subArg, + summary: t.description, + }) - this.commands - .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) - .forEach(c => { - if (coTopics.includes(c.id)) return - const subArg = c.id.split(':')[depth] + argsBlock += util.format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`) + }) - subArgs.push({ - id: subArg, - summary: c.summary - }) + this.commands + .filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1) + .forEach(c => { + if (coTopics.includes(c.id)) return + const subArg = c.id.split(':')[depth] - argsBlock+= util.format(flagArgsTemplate,subArg,this.genZshFlagArgumentsBlock(c.flags)) - }) + subArgs.push({ + id: subArg, + summary: c.summary, + }) - const topicCompFunc = + argsBlock += util.format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags)) + }) + + const topicCompFunc = `_${this.config.bin}_${underscoreSepId}() { local context state state_descr line typeset -A opt_args @@ -309,15 +324,14 @@ _${this.config.bin} esac } ` - return util.format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) - } + return util.format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) } private get coTopics(): string [] { if (this._coTopics) return this._coTopics - const coTopics:string[]=[] - + const coTopics: string[] = [] + for (const topic of this.topics) { for (const cmd of this.commands) { if (topic.name === cmd.id) { @@ -339,21 +353,21 @@ _${this.config.bin} const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) return hasChild }) - .sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }) - .map(t=> { - return { - name: t.name, - description: sanitizeSummary(t.description) - } - }) + .sort((a, b) => { + if (a.name < b.name) { + return -1 + } + if (a.name > b.name) { + return 1 + } + return 0 + }) + .map(t => { + return { + name: t.name, + description: sanitizeSummary(t.description), + } + }) this._topics = topics @@ -384,15 +398,3 @@ _${this.config.bin} } } -const argTemplate = ` "%s")\n %s\n ;;\n` - -function sanitizeSummary(description?: string): string { - if (description === undefined) { - return '' - } - return description - .replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes - // eslint-disable-next-line no-useless-escape - .replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes - .split('\n')[0] // only use the first line -} diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 8c34f6b8..9768cb5a 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -1,5 +1,4 @@ import * as path from 'path' -import * as util from 'util' import * as fs from 'fs-extra' @@ -7,7 +6,6 @@ import bashAutocomplete from '../../autocomplete/bash' import ZshCompWithSpaces from '../../autocomplete/zsh' import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces' import {AutocompleteBase} from '../../base' -import {Interfaces } from '@oclif/core' const debug = require('debug')('autocomplete:create') @@ -58,7 +56,7 @@ export default class Create extends AutocompleteBase { await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript) // zsh - if (this.config.topicSeparator === ":") { + if (this.config.topicSeparator === ':') { await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction) } else { const zshCompWithSpaces = new ZshCompWithSpaces(this.config) From c31e3c9278ee6ff5338fe81e46fe85537b4a1a5d Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Thu, 26 Jan 2023 12:52:35 -0300 Subject: [PATCH 26/43] test: add test --- src/autocomplete/zsh.ts | 6 +- test/autocomplete/zsh.test.ts | 238 ++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 test/autocomplete/zsh.test.ts diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index ed340772..f833188f 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -64,7 +64,7 @@ export default class ZshCompWithSpaces { for (const arg of firstArgs) { if (this.coTopics.includes(arg.id)) { // coTopics already have a completion function. - caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n` } else { const cmd = this.commands.find(c => c.id === arg.id) @@ -73,7 +73,7 @@ export default class ZshCompWithSpaces { caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` } else { // it's a topic, redirect to its completion function. - caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id} \n ;;\n` + caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n` } } } @@ -239,7 +239,7 @@ _${this.config.bin} ;; esac ;; - esac + esac } ` const subArgs: {id: string; summary?: string}[] = [] diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts new file mode 100644 index 00000000..f9196bf2 --- /dev/null +++ b/test/autocomplete/zsh.test.ts @@ -0,0 +1,238 @@ +import {Config} from '@oclif/core' +import * as path from 'path' +import {Plugin as IPlugin} from '@oclif/core/lib/interfaces' +import {expect} from 'chai' +import {Command as ICommand} from '@oclif/core/lib/interfaces' +import ZshCompWithSpaces from '../../src/autocomplete/zsh' + +// autocomplete will throw error on windows ci +const {default: skipWindows} = require('../helpers/runtest') + +// @ts-expect-error +class MyCommandClass implements ICommand.Class { + _base = '' + + aliases: string[] = [] + + hidden = false + + id = 'foo:bar' + + flags = {} + + new(): ICommand.Instance { + return { + _run(): Promise { + return Promise.resolve() + }} + } + + run(): PromiseLike { + return Promise.resolve() + } +} + +const commandPluginA: ICommand.Loadable = { + strict: false, + aliases: [], + args: [], + flags: { + metadata: { + name: 'metadata', + type: 'option', + char: 'm', + multiple: true, + }, + 'api-version': { + name: 'api-version', + type: 'option', + char: 'a', + multiple: false, + }, + json: { + name: 'json', + type: 'boolean', + summary: 'Format output as json.', + allowNo: false, + }, + 'ignore-errors': { + name: 'ignore-errors', + type: 'boolean', + char: 'i', + summary: 'Ignore errors.', + allowNo: false, + }, + }, + hidden: false, + id: 'deploy', + summary: 'Deploy a project', + async load(): Promise { + return new MyCommandClass() as unknown as ICommand.Class + }, + pluginType: 'core', + pluginAlias: '@My/plugina', +} + +const commandPluginB: ICommand.Loadable = { + strict: false, + aliases: [], + args: [], + flags: { + branch: { + name: 'branch', + type: 'option', + char: 'b', + multiple: false, + }, + }, + hidden: false, + id: 'deploy:functions', + summary: 'Deploy a function.', + async load(): Promise { + return new MyCommandClass() as unknown as ICommand.Class + }, + pluginType: 'core', + pluginAlias: '@My/pluginb', +} + +const commandPluginC: ICommand.Loadable = { + strict: false, + aliases: [], + args: [], + flags: {}, + hidden: false, + id: 'search', + summary: 'Search for a command', + async load(): Promise { + return new MyCommandClass() as unknown as ICommand.Class + }, + pluginType: 'core', + pluginAlias: '@My/pluginc', +} + +const pluginA: IPlugin = { + load: async (): Promise => {}, + findCommand: async (): Promise => { + return new MyCommandClass() as unknown as ICommand.Class + }, + name: '@My/plugina', + alias: '@My/plugina', + commands: [commandPluginA, commandPluginB, commandPluginC], + _base: '', + pjson: {} as any, + commandIDs: ['deploy'], + root: '', + version: '0.0.0', + type: 'core', + hooks: {}, + topics: [{ + name: 'foo', + description: 'foo commands', + }], + valid: true, + tag: 'tag', +} + +const plugins: IPlugin[] = [pluginA] + +skipWindows('zsh comp', () => { + describe('zsh completion with spaces', () => { + const root = path.resolve(__dirname, '../../package.json') + const config = new Config({root}) + + before(async () => { + await config.load() + /* eslint-disable require-atomic-updates */ + config.plugins = plugins + config.pjson.oclif.plugins = ['@My/pluginb'] + config.pjson.dependencies = {'@My/pluginb': '0.0.0'} + for (const plugin of config.plugins) { + // @ts-expect-error private method + config.loadCommands(plugin) + // @ts-expect-error private method + config.loadTopics(plugin) + } + }) + + it('generates a valid completion file.', () => { + config.bin = 'test-cli' + const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) + expect(zshCompWithSpaces.generate()).to.equal(`#compdef test-cli + +_test-cli_deploy() { + _test-cli_deploy_flags() { + local context state state_descr line + typeset -A opt_args + + _arguments -S \\ +"*"{-m,--metadata}"[]:file:_files" \\ +"(-a --api-version)"{-a,--api-version}"[]:file:_files" \\ +--json"[Format output as json.]" \\ +"(-i --ignore-errors)"{-i,--ignore-errors}"[Ignore errors.]" \\ +"*: :_files" + } + + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + if [[ "\${words[CURRENT]}" == -* ]]; then + _test-cli_deploy_flags + else +_values "completions" \\ +"functions[Deploy a function.]" \\ + + fi + ;; + args) + case $line[1] in + "functions") + _arguments -S \\ +"(-b --branch)"{-b,--branch}"[]:file:_files" \\ +"*: :_files" + ;; + + *) + _test-cli_deploy_flags + ;; + esac + ;; + esac +} + + +_test-cli() { + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) + _values "completions" \\ +"deploy[Deploy a project]" \\ +"search[Search for a command]" \\ + + ;; + args) + case $line[1] in +deploy) + _test-cli_deploy + ;; +search) + _test-cli_search + ;; +esac + + ;; + esac +} + +_test-cli +`) + }) + }) +}) From 18cd38d90446f94a2d0d053171fba8a1db396798 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 27 Jan 2023 16:05:17 -0300 Subject: [PATCH 27/43] fix: cotopic func missing flags current state --- src/autocomplete/zsh.ts | 2 +- test/autocomplete/zsh.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index f833188f..4421f628 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -221,7 +221,7 @@ _${this.config.bin} local context state state_descr line typeset -A opt_args - _arguments -C "1: :->cmds" "*::arg:->args" + _arguments -C "1: :->cmds" "*: :->args" case "$state" in cmds) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index f9196bf2..773510c2 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -175,7 +175,7 @@ _test-cli_deploy() { local context state state_descr line typeset -A opt_args - _arguments -C "1: :->cmds" "*::arg:->args" + _arguments -C "1: :->cmds" "*: :->args" case "$state" in cmds) From 146735d4825d22278c9f21a20b57cad51fd08f01 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 13 Feb 2023 14:43:51 -0300 Subject: [PATCH 28/43] chore: refactor --- src/autocomplete/zsh.ts | 46 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 4421f628..aa03120b 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -32,14 +32,16 @@ type Topic = { export default class ZshCompWithSpaces { protected config: Config; - private _topics?: Topic[] + private topics: Topic[] - private _commands?: CommandCompletion[] + private commands: CommandCompletion[] private _coTopics?: string[] constructor(config: Config) { this.config = config + this.topics = this.getTopics() + this.commands = this.getCommands() } public generate(): string { @@ -345,9 +347,7 @@ _${this.config.bin} return this._coTopics } - private get topics(): Topic[] { - if (this._topics) return this._topics - + private getTopics(): Topic[] { const topics = this.config.topics.filter((topic: Interfaces.Topic) => { // it is assumed a topic has a child if it has children const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) @@ -369,14 +369,10 @@ _${this.config.bin} } }) - this._topics = topics - - return this._topics + return topics } - private get commands(): CommandCompletion[] { - if (this._commands) return this._commands - + private getCommands(): CommandCompletion[] { const cmds: CommandCompletion[] = [] this.config.plugins.forEach(p => { @@ -389,12 +385,34 @@ _${this.config.bin} summary, flags, }) + + c.aliases.forEach(a=>{ + cmds.push({ + id: a, + summary, + flags + }) + + const split = a.split(':') + + let words = split[0] + + for (let i = 1; i < split.length; words += `:${split[i]}`) { + if(this.topics.find(t=> t.name === words) === undefined) { + console.log(words); + this.topics.push({ + name: words, + description: '' + }) + } + i++; + } + + }) }) }) - this._commands = cmds - - return this._commands + return cmds } } From ca03e1761d07cc2dde4e57f7c1a986c15f6e18e1 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 13 Feb 2023 16:21:13 -0300 Subject: [PATCH 29/43] fix: generate topics for aliases --- src/autocomplete/zsh.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index aa03120b..024fdf10 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -386,28 +386,31 @@ _${this.config.bin} flags, }) - c.aliases.forEach(a=>{ + c.aliases.forEach(a => { cmds.push({ id: a, summary, - flags + flags, }) - + const split = a.split(':') let words = split[0] - for (let i = 1; i < split.length; words += `:${split[i]}`) { - if(this.topics.find(t=> t.name === words) === undefined) { - console.log(words); + // Completion funcs are generated from topics: + // `force` -> `force:org` -> `force:org:open|list` + // + // but aliases aren't guaranteed to follow the plugin command tree + // so we need to add any missing topic between the starting point and the alias. + for (let i = 0; i < split.length - 1; i++) { + if (!this.topics.find(t => t.name === words)) { this.topics.push({ name: words, - description: '' + description: '', }) } - i++; + words += `:${split[i + 1]}` } - }) }) }) From b00353f179373661538d9de34bfe796058c31477 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 10:14:02 -0300 Subject: [PATCH 30/43] fix(aliases): add generic topic description --- src/autocomplete/zsh.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 024fdf10..23db077e 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -395,7 +395,7 @@ _${this.config.bin} const split = a.split(':') - let words = split[0] + let topic = split[0] // Completion funcs are generated from topics: // `force` -> `force:org` -> `force:org:open|list` @@ -403,13 +403,13 @@ _${this.config.bin} // but aliases aren't guaranteed to follow the plugin command tree // so we need to add any missing topic between the starting point and the alias. for (let i = 0; i < split.length - 1; i++) { - if (!this.topics.find(t => t.name === words)) { + if (!this.topics.find(t => t.name === topic)) { this.topics.push({ - name: words, - description: '', + name: topic, + description: `${topic.replace(/:/g, ' ')} commands`, }) } - words += `:${split[i + 1]}` + topic += `:${split[i + 1]}` } }) }) From 9a6b4fb0572f72e9f1f7c92972186208c930465b Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 10:22:20 -0300 Subject: [PATCH 31/43] fix: handle topics without desc --- src/autocomplete/zsh.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 23db077e..4a545cc7 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -363,9 +363,11 @@ _${this.config.bin} return 0 }) .map(t => { + const description = t.description ? sanitizeSummary(t.description) : `${t.name.replace(/:/g, ' ')} commands` + return { name: t.name, - description: sanitizeSummary(t.description), + description, } }) From f0075d538df189675d9b351de68945d8c8069be4 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 10:40:47 -0300 Subject: [PATCH 32/43] fix: skip top-level commands without flags --- src/autocomplete/zsh.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 4a545cc7..89c388dd 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -70,9 +70,12 @@ export default class ZshCompWithSpaces { } else { const cmd = this.commands.find(c => c.id === arg.id) - // if it's a command and has flags, inline flag completion statement. - if (cmd && Object.keys(cmd.flags).length > 0) { - caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + if (cmd) { + // if it's a command and has flags, inline flag completion statement. + // skip it from the args statement if it doesn't accept any flag. + if (Object.keys(cmd.flags).length > 0) { + caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` + } } else { // it's a topic, redirect to its completion function. caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n` From 728499c65183ee466cc40a0778999a79cc61a399 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 10:47:57 -0300 Subject: [PATCH 33/43] test: update test --- test/autocomplete/zsh.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index 773510c2..357dbae0 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -222,9 +222,6 @@ _test-cli() { deploy) _test-cli_deploy ;; -search) - _test-cli_search - ;; esac ;; From cf554cfea3542c72526347e96954b38536f20922 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 11:04:06 -0300 Subject: [PATCH 34/43] test: add test to cover topic generation --- test/autocomplete/zsh.test.ts | 68 ++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index 357dbae0..99aacc1c 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -110,6 +110,21 @@ const commandPluginC: ICommand.Loadable = { pluginAlias: '@My/pluginc', } +const commandPluginD: ICommand.Loadable = { + strict: false, + aliases: [], + args: [], + flags: {}, + hidden: false, + id: 'app:execute:code', + summary: 'execute code', + async load(): Promise { + return new MyCommandClass() as unknown as ICommand.Class + }, + pluginType: 'core', + pluginAlias: '@My/plugind', +} + const pluginA: IPlugin = { load: async (): Promise => {}, findCommand: async (): Promise => { @@ -117,7 +132,7 @@ const pluginA: IPlugin = { }, name: '@My/plugina', alias: '@My/plugina', - commands: [commandPluginA, commandPluginB, commandPluginC], + commands: [commandPluginA, commandPluginB, commandPluginC, commandPluginD], _base: '', pjson: {} as any, commandIDs: ['deploy'], @@ -159,6 +174,53 @@ skipWindows('zsh comp', () => { const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) expect(zshCompWithSpaces.generate()).to.equal(`#compdef test-cli +_test-cli_app() { + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) +_values "completions" \\ +"execute[execute code]" \\ + + ;; + args) + case $line[1] in + "execute") + _test-cli_app_execute + ;; + + esac + ;; + esac +} + +_test-cli_app_execute() { + local context state state_descr line + typeset -A opt_args + + _arguments -C "1: :->cmds" "*::arg:->args" + + case "$state" in + cmds) +_values "completions" \\ +"code[execute code]" \\ + + ;; + args) + case $line[1] in + "code") + _arguments -S \\ +"*: :_files" + ;; + + esac + ;; + esac +} + _test-cli_deploy() { _test-cli_deploy_flags() { local context state state_descr line @@ -213,12 +275,16 @@ _test-cli() { case "$state" in cmds) _values "completions" \\ +"app[execute code]" \\ "deploy[Deploy a project]" \\ "search[Search for a command]" \\ ;; args) case $line[1] in +app) + _test-cli_app + ;; deploy) _test-cli_deploy ;; From be3dee4c433e22e9c30f62a333cf87a66746f395 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 14:48:40 -0300 Subject: [PATCH 35/43] feat: support global help flag --- src/autocomplete/zsh.ts | 5 ++++- test/autocomplete/zsh.test.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 89c388dd..c9502b8f 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -116,7 +116,8 @@ _${this.config.bin} private genZshFlagArgumentsBlock(flags?: CommandFlags): string { // if a command doesn't have flags make it only complete files - if (!flags) return '_arguments "*: :_files"' + // also add comp for the global `--help` flag. + if (!flags) return '_arguments -S \\\n --help"[Show help for command]" "*: :_files' const flagNames = Object.keys(flags) @@ -177,6 +178,8 @@ _${this.config.bin} flagSpec += ' \\\n' argumentsBlock += flagSpec } + // add global `--help` flag + argumentsBlock += '--help"[Show help for command]" \\\n' // complete files if `-` is not present on the current line argumentsBlock += '"*: :_files"' diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index 99aacc1c..020428b5 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -213,6 +213,7 @@ _values "completions" \\ case $line[1] in "code") _arguments -S \\ +--help"[Show help for command]" \\ "*: :_files" ;; @@ -231,6 +232,7 @@ _test-cli_deploy() { "(-a --api-version)"{-a,--api-version}"[]:file:_files" \\ --json"[Format output as json.]" \\ "(-i --ignore-errors)"{-i,--ignore-errors}"[Ignore errors.]" \\ +--help"[Show help for command]" \\ "*: :_files" } @@ -254,6 +256,7 @@ _values "completions" \\ "functions") _arguments -S \\ "(-b --branch)"{-b,--branch}"[]:file:_files" \\ +--help"[Show help for command]" \\ "*: :_files" ;; From e9b42aa033f2bb0aa6d5841949c95436aafa4528 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 17:53:13 -0300 Subject: [PATCH 36/43] fix: allow to switch between comp implementation --- src/commands/autocomplete/create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 9768cb5a..8a77ef18 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -56,7 +56,7 @@ export default class Create extends AutocompleteBase { await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript) // zsh - if (this.config.topicSeparator === ':') { + if (process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon') { await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction) } else { const zshCompWithSpaces = new ZshCompWithSpaces(this.config) @@ -204,7 +204,7 @@ compinit;\n` private get bashCompletionFunction(): string { const cliBin = this.cliBin - const bashScript = this.config.topicSeparator === ' ' ? bashAutocompleteWithSpaces : bashAutocomplete + const bashScript = process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' ? bashAutocomplete : bashAutocompleteWithSpaces return bashScript.replace(//g, cliBin).replace(//g, this.bashCommandsWithFlagsList) } From 4890d4fa11e0effcb832a1fb6df1f96a5a14ec69 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 18:20:01 -0300 Subject: [PATCH 37/43] fix: check config.topicSeparator prop --- src/commands/autocomplete/create.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 8a77ef18..45ee2208 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -56,7 +56,9 @@ export default class Create extends AutocompleteBase { await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript) // zsh - if (process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon') { + const supportSpaces = this.config.topicSeparator === ' ' + + if (process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' || !supportSpaces) { await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction) } else { const zshCompWithSpaces = new ZshCompWithSpaces(this.config) @@ -204,7 +206,9 @@ compinit;\n` private get bashCompletionFunction(): string { const cliBin = this.cliBin - const bashScript = process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' ? bashAutocomplete : bashAutocompleteWithSpaces + const supportSpaces = this.config.topicSeparator === ' ' + console.log(`supportSpaces: ${supportSpaces}`) + const bashScript = (process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' || !supportSpaces) ? bashAutocomplete : bashAutocompleteWithSpaces return bashScript.replace(//g, cliBin).replace(//g, this.bashCommandsWithFlagsList) } From 3209cc5f876d1bfe687838b2eed8e4f03f8e8f7d Mon Sep 17 00:00:00 2001 From: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Date: Tue, 14 Feb 2023 18:25:46 -0300 Subject: [PATCH 38/43] Update README.md --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39110a0b..129ce556 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ autocomplete plugin for oclif (bash & zsh) [![Version](https://img.shields.io/npm/v/@oclif/plugin-autocomplete.svg)](https://npmjs.org/package/@oclif/plugin-autocomplete) -[![CircleCI](https://circleci.com/gh/oclif/plugin-autocomplete/tree/main.svg?style=shield)](https://circleci.com/gh/oclif/plugin-autocomplete/tree/main) -[![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/oclif/plugin-autocomplete?branch=main&svg=true)](https://ci.appveyor.com/project/oclif/plugin-autocomplete/branch/main) [![Downloads/week](https://img.shields.io/npm/dw/@oclif/plugin-autocomplete.svg)](https://npmjs.org/package/@oclif/plugin-autocomplete) [![License](https://img.shields.io/npm/l/@oclif/plugin-autocomplete.svg)](https://github.com/oclif/plugin-autocomplete/blob/main/package.json) @@ -14,7 +12,18 @@ autocomplete plugin for oclif (bash & zsh) * [Commands](#commands) # Usage -See https://oclif.io/docs/plugins.html + +Run ` autocomplete` to generate the autocomplete files for your current shell. + +## Topic separator +Since oclif v2 it's possible to use spaces as a topic separator in addition to colons. + +For bash and zsh each topic separator has different autocomplete implementations, if the CLI supports using a space as the separator, plugin-autocomplete will generate completion for that topic. + +If you still want to use the colon-separated autocomplete you can set `OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR` to `colon` and re-generate the autocomplete files. + +Docs: https://oclif.io/docs/topic_separator + # Commands * [`oclif-example autocomplete [SHELL]`](#oclif-example-autocomplete-shell) From 2bf9e515f9f6e3e1a56d7f1717ffc818d1fabf96 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 18:57:21 -0300 Subject: [PATCH 39/43] chore: bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c8be6d8f..14e2bf1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/plugin-autocomplete", "description": "autocomplete plugin for oclif", - "version": "1.3.7", + "version": "2.0.0", "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-autocomplete/issues", "dependencies": { @@ -65,4 +65,4 @@ "version": "oclif readme && git add README.md", "build": "shx rm -rf lib && tsc" } -} \ No newline at end of file +} From c79004598a40e5b4a87501f2841bc63d7e65eedb Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 14 Feb 2023 18:57:40 -0300 Subject: [PATCH 40/43] chore: remove console.log --- src/commands/autocomplete/create.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 45ee2208..f4d6d793 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -207,7 +207,6 @@ compinit;\n` private get bashCompletionFunction(): string { const cliBin = this.cliBin const supportSpaces = this.config.topicSeparator === ' ' - console.log(`supportSpaces: ${supportSpaces}`) const bashScript = (process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' || !supportSpaces) ? bashAutocomplete : bashAutocompleteWithSpaces return bashScript.replace(//g, cliBin).replace(//g, this.bashCommandsWithFlagsList) } From 14b4e389a616a3c9491a7191a090a9fe2191d320 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 15 Feb 2023 11:06:59 -0300 Subject: [PATCH 41/43] fix: oclif v2 changes --- src/autocomplete/zsh.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index c9502b8f..814b4d81 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -1,5 +1,5 @@ import * as util from 'util' -import {Config, Interfaces} from '@oclif/core' +import {Config, Interfaces, Command} from '@oclif/core' function sanitizeSummary(description?: string): string { if (description === undefined) { @@ -21,7 +21,7 @@ type CommandCompletion = { } type CommandFlags = { - [name: string]: Interfaces.Command.Flag; + [name: string]: Command.Flag.Cached; } type Topic = { From d1cabe986ede8f480957eef6cd30ae8d280bc877 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 15 Feb 2023 11:59:57 -0300 Subject: [PATCH 42/43] test: fix tests --- test/autocomplete/zsh.test.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index 020428b5..72faae3c 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -1,15 +1,17 @@ -import {Config} from '@oclif/core' +import {Config, Command as ICommand} from '@oclif/core' import * as path from 'path' import {Plugin as IPlugin} from '@oclif/core/lib/interfaces' import {expect} from 'chai' -import {Command as ICommand} from '@oclif/core/lib/interfaces' import ZshCompWithSpaces from '../../src/autocomplete/zsh' // autocomplete will throw error on windows ci const {default: skipWindows} = require('../helpers/runtest') -// @ts-expect-error -class MyCommandClass implements ICommand.Class { +class MyCommandClass implements ICommand.Cached { + [key: string]: unknown; + + args: {[name: string]: ICommand.Arg.Cached} = {} + _base = '' aliases: string[] = [] @@ -20,7 +22,8 @@ class MyCommandClass implements ICommand.Class { flags = {} - new(): ICommand.Instance { + new(): ICommand.Cached { + // @ts-ignore return { _run(): Promise { return Promise.resolve() @@ -35,7 +38,7 @@ class MyCommandClass implements ICommand.Class { const commandPluginA: ICommand.Loadable = { strict: false, aliases: [], - args: [], + args: {}, flags: { metadata: { name: 'metadata', @@ -76,7 +79,7 @@ const commandPluginA: ICommand.Loadable = { const commandPluginB: ICommand.Loadable = { strict: false, aliases: [], - args: [], + args: {}, flags: { branch: { name: 'branch', @@ -98,7 +101,7 @@ const commandPluginB: ICommand.Loadable = { const commandPluginC: ICommand.Loadable = { strict: false, aliases: [], - args: [], + args: {}, flags: {}, hidden: false, id: 'search', @@ -113,7 +116,7 @@ const commandPluginC: ICommand.Loadable = { const commandPluginD: ICommand.Loadable = { strict: false, aliases: [], - args: [], + args: {}, flags: {}, hidden: false, id: 'app:execute:code', From 9a3550a83088218d1b1ebdf9a0db82364057397f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 15 Feb 2023 09:04:16 -0600 Subject: [PATCH 43/43] style: explain comment and rename command import --- test/autocomplete/zsh.test.ts | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index 72faae3c..86b8cb09 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -1,4 +1,4 @@ -import {Config, Command as ICommand} from '@oclif/core' +import {Config, Command} from '@oclif/core' import * as path from 'path' import {Plugin as IPlugin} from '@oclif/core/lib/interfaces' import {expect} from 'chai' @@ -7,10 +7,10 @@ import ZshCompWithSpaces from '../../src/autocomplete/zsh' // autocomplete will throw error on windows ci const {default: skipWindows} = require('../helpers/runtest') -class MyCommandClass implements ICommand.Cached { +class MyCommandClass implements Command.Cached { [key: string]: unknown; - args: {[name: string]: ICommand.Arg.Cached} = {} + args: {[name: string]: Command.Arg.Cached} = {} _base = '' @@ -22,8 +22,8 @@ class MyCommandClass implements ICommand.Cached { flags = {} - new(): ICommand.Cached { - // @ts-ignore + new(): Command.Cached { + // @ts-expect-error this is not the full interface but enough for testing return { _run(): Promise { return Promise.resolve() @@ -35,7 +35,7 @@ class MyCommandClass implements ICommand.Cached { } } -const commandPluginA: ICommand.Loadable = { +const commandPluginA: Command.Loadable = { strict: false, aliases: [], args: {}, @@ -69,14 +69,14 @@ const commandPluginA: ICommand.Loadable = { hidden: false, id: 'deploy', summary: 'Deploy a project', - async load(): Promise { - return new MyCommandClass() as unknown as ICommand.Class + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class }, pluginType: 'core', pluginAlias: '@My/plugina', } -const commandPluginB: ICommand.Loadable = { +const commandPluginB: Command.Loadable = { strict: false, aliases: [], args: {}, @@ -91,14 +91,14 @@ const commandPluginB: ICommand.Loadable = { hidden: false, id: 'deploy:functions', summary: 'Deploy a function.', - async load(): Promise { - return new MyCommandClass() as unknown as ICommand.Class + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class }, pluginType: 'core', pluginAlias: '@My/pluginb', } -const commandPluginC: ICommand.Loadable = { +const commandPluginC: Command.Loadable = { strict: false, aliases: [], args: {}, @@ -106,14 +106,14 @@ const commandPluginC: ICommand.Loadable = { hidden: false, id: 'search', summary: 'Search for a command', - async load(): Promise { - return new MyCommandClass() as unknown as ICommand.Class + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class }, pluginType: 'core', pluginAlias: '@My/pluginc', } -const commandPluginD: ICommand.Loadable = { +const commandPluginD: Command.Loadable = { strict: false, aliases: [], args: {}, @@ -121,8 +121,8 @@ const commandPluginD: ICommand.Loadable = { hidden: false, id: 'app:execute:code', summary: 'execute code', - async load(): Promise { - return new MyCommandClass() as unknown as ICommand.Class + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class }, pluginType: 'core', pluginAlias: '@My/plugind', @@ -130,8 +130,8 @@ const commandPluginD: ICommand.Loadable = { const pluginA: IPlugin = { load: async (): Promise => {}, - findCommand: async (): Promise => { - return new MyCommandClass() as unknown as ICommand.Class + findCommand: async (): Promise => { + return new MyCommandClass() as unknown as Command.Class }, name: '@My/plugina', alias: '@My/plugina',