From a4d97e4faf98c21f924e2c0fa4f3a38476a6ea53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <git@minelli.me>
Date: Mon, 11 Mar 2024 17:20:14 +0100
Subject: [PATCH] Completion => Adapt zsh and bash completion to updated
 procedure

---
 .../commander/completion/CompletionCommand.ts |  13 +-
 .../subcommands/CompletionBashCommand.ts      |  73 ++---------
 .../subcommands/CompletionZshCommand.ts       | 117 ++----------------
 NodeApp/src/helpers/AutoCompletionHelper.ts   |  40 ++++++
 4 files changed, 71 insertions(+), 172 deletions(-)

diff --git a/NodeApp/src/commander/completion/CompletionCommand.ts b/NodeApp/src/commander/completion/CompletionCommand.ts
index 1ee0668..f0b7190 100644
--- a/NodeApp/src/commander/completion/CompletionCommand.ts
+++ b/NodeApp/src/commander/completion/CompletionCommand.ts
@@ -1,7 +1,9 @@
-import CommanderCommand      from '../CommanderCommand';
-import CompletionBashCommand from './subcommands/CompletionBashCommand';
-import CompletionFishCommand from './subcommands/CompletionFishCommand';
-import CompletionZshCommand  from './subcommands/CompletionZshCommand';
+import CommanderCommand        from '../CommanderCommand';
+import CompletionBashCommand   from './subcommands/CompletionBashCommand';
+import CompletionFishCommand   from './subcommands/CompletionFishCommand';
+import CompletionZshCommand    from './subcommands/CompletionZshCommand';
+import CompletionGetCommand    from './subcommands/CompletionGetCommand';
+import CompletionScriptCommand from './subcommands/CompletionScriptCommand';
 
 
 class CompletionCommand extends CommanderCommand {
@@ -16,6 +18,9 @@ class CompletionCommand extends CommanderCommand {
         CompletionBashCommand.registerOnCommand(this.command);
         CompletionFishCommand.registerOnCommand(this.command);
         CompletionZshCommand.registerOnCommand(this.command);
+
+        CompletionGetCommand.registerOnCommand(this.command);
+        CompletionScriptCommand.registerOnCommand(this.command);
     }
 
     protected async commandAction(): Promise<void> {
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts
index d135440..54b0a1e 100644
--- a/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts
+++ b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts
@@ -1,75 +1,26 @@
-import CommanderCommand                                   from '../../CommanderCommand';
-import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
-import ora                                                from 'ora';
-import TextStyle                                          from '../../../types/TextStyle';
-import fs                                                 from 'fs-extra';
-import path                                               from 'path';
-import os                                                 from 'os';
-import GlobalHelper                                       from '../../../helpers/GlobalHelper';
+import CommanderCommand from '../../CommanderCommand';
+import path             from 'path';
+import os               from 'os';
+import { updateRcFile } from '../../../helpers/AutoCompletionHelper';
 
 
 class CompletionBashCommand extends CommanderCommand {
     protected commandName: string = 'bash';
 
-    private readonly installPath = path.join(os.homedir(), '.bash_completion');
+    private readonly bashrcPath = path.join(os.homedir(), '.bashrc');
+    private readonly completionCommand = `
+# Added by DojoCLI
+source <(dojo completion script bash)
+`;
 
     protected defineCommand() {
-        GlobalHelper.completionCommandDefinition(this.command)
-            .description('generate bash completion')
+        this.command.description('generate bash completion')
             .action(this.commandAction.bind(this));
     }
 
-    private writeFile(filename: string, showInstructions: boolean) {
-        const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
-
-        try {
-            fs.mkdirsSync(path.dirname(filename));
-
-            fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
-
-            spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
-            if ( showInstructions ) {
-                console.log(`
-The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE('~/.bash_completion') } file or to overwrite it, if it only contains the 'dojo' completion.
-
-This can be performed by either
-${ TextStyle.CODE(`
-cat ${ filename } > ~/.bash_completion  # overwrites .bash_completion
-cat ${ filename } >> ~/.bash_completion # appends to .bash_completion
-`) }
-For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
-`);
-            }
-        } catch ( error ) {
-            spinner.fail(`Bash completion error: ${ error }`);
-        }
+    protected async commandAction(): Promise<void> {
+        updateRcFile('bash', this.bashrcPath, this.completionCommand);
     }
-
-    /* The completion command must do the following:
-    - if a file is provided:
-        - if force is not enabled:
-            - check if the bash completion file exists:
-                - if it exists, prompt the user that it will be overwritten
-                    - if ok is given write the file and prompt that a backup has been created
-        - else create the file containing the completion
-    - else
-        - if force is not enabled:
-            - check if the default file exists:
-                - if it exists, prompt the user that it will be erased:
-                    - if ok is given write the file and prompt that a backup has been created
-        - else
-            - create the file containing the completion
-    - create a .zprofile or append the appropriate commands into the .zprofile file
-    */
-    protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
-        const filePath = path.resolve(options.file ?? this.installPath); // change that if file is empty
-        const showInstructions = !!options.file;
-        if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
-            return;
-        }
-        this.writeFile(filePath, showInstructions);
-    }
-
 }
 
 
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
index f9d279e..94fe5b4 100644
--- a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
+++ b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
@@ -1,123 +1,26 @@
-import CommanderCommand                                   from '../../CommanderCommand';
-import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
-import ora                                                from 'ora';
-import TextStyle                                          from '../../../types/TextStyle';
-import path                                               from 'path';
-import { homedir }                                        from 'os';
-import fs                                                 from 'fs-extra';
-import GlobalHelper                                       from '../../../helpers/GlobalHelper';
+import CommanderCommand from '../../CommanderCommand';
+import path             from 'path';
+import { homedir }      from 'os';
+import { updateRcFile } from '../../../helpers/AutoCompletionHelper';
 
 
 class CompletionZshCommand extends CommanderCommand {
     protected commandName: string = 'zsh';
 
-    private readonly zprofile: string = path.join(homedir(), '.zprofile');
-    private readonly bashCompletion = path.join(homedir(), '.bash_completion');
-    private readonly loadBashCompletion = `
+    private readonly zshrcPath: string = path.join(homedir(), '.zshrc');
+    private readonly completionCommand = `
 # Added by DojoCLI
-autoload -U +X compinit && compinit
-autoload -U +X bashcompinit && bashcompinit
-source ${ this.bashCompletion }
+source <(dojo completion script zsh)
 `;
 
 
     protected defineCommand() {
-        GlobalHelper.completionCommandDefinition(this.command)
-            .description('generate zsh completion, which is derived from the bash completion')
+        this.command.description('generate zsh completion')
             .action(this.commandAction.bind(this));
     }
 
-    private writeFile(filename: string, showInstructions: boolean) {
-        const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
-
-        try {
-            fs.mkdirsSync(path.dirname(filename));
-
-            fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
-
-            spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
-            if ( showInstructions ) {
-                const zprofileContent = TextStyle.CODE(`
-autoload -U +X compinit && compinit
-autoload -U +X bashcompinit && bashcompinit
-source ${ filename }
-`);
-                console.log(`
-The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE(filename) } file or to overwrite it, if it only contains the 'dojo' completion.
-
-This can be performed by either ${ TextStyle.CODE(`
-cat ${ filename } > ~/.bash_completion  # overwrites .bash_completion
-cat ${ filename } >> ~/.bash_completion # appends to .bash_completion`) }
-For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
-
-Next add the following lines to your ${ TextStyle.CODE('~/.zprofile') } file: ${ zprofileContent } `);
-            }
-        } catch ( error ) {
-            spinner.fail(`Bash completion writing error: ${ error }`);
-        }
-    }
-
-    protected addToZprofile(zprofilePath: string, bashPath: string) {
-        const spinner: ora.Ora = ora(`Modifying ${ zprofilePath } ...`).start();
-        if ( fs.existsSync(zprofilePath) ) {
-            const data = fs.readFileSync(zprofilePath);
-            let updated = false;
-            try {
-                if ( !data.includes('autoload -U +X compinit && compinit') ) {
-                    fs.appendFileSync(zprofilePath, '\nautoload -U +X compinit && compinit');
-                    updated = true;
-                }
-                if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) {
-                    fs.appendFileSync(zprofilePath, '\nautoload -U +X bashcompinit && bashcompinit');
-                    updated = true;
-                }
-                if ( !data.includes(`source ${ bashPath }`) ) {
-                    fs.appendFileSync(zprofilePath, `\nsource ${ bashPath }`);
-                    updated = true;
-                }
-            } catch {
-                spinner.fail(`Error appending in ${ this.zprofile }`);
-                return;
-            }
-
-            spinner.succeed(updated ? `Zsh profile updated.` : `Zsh profile already up to date.`);
-        } else {
-            try {
-                fs.writeFileSync(zprofilePath, this.loadBashCompletion);
-                spinner.succeed(`Zsh profile written.`);
-            } catch ( error ) {
-                spinner.fail(`Error writing in ${ this.zprofile }`);
-            }
-        }
-    }
-
-    /* The completion command must do the following:
-    - if a file is provided:
-        - if force is not enabled:
-            - check if the bash completion file exists:
-                - if it exists, prompt the user that it will be overwritten
-                    - if ok is given write the file and prompt that a backup has been created
-        - else create the file containing the completion
-    - else
-        - if force is not enabled:
-            - check if the default file exists:
-                - if it exists, prompt the user that it will be erased:
-                    - if ok is given write the file and prompt that a backup has been created
-        - else
-            - create the file containing the completion
-    - create a .zprofile or append the appropriate commands into the .zprofile file
-    */
-    protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
-        const filePath = path.resolve(options.file ?? this.bashCompletion); // change that if file is empty
-        const showInstructions = !!options.file;
-        if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
-            return;
-        }
-        this.writeFile(filePath, showInstructions);
-        // Do not modify if custom file was provided
-        if ( !options.file ) {
-            this.addToZprofile(this.zprofile, filePath);
-        }
+    protected async commandAction(): Promise<void> {
+        updateRcFile('zsh', this.zshrcPath, this.completionCommand);
     }
 }
 
diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts
index 56988eb..2e14837 100644
--- a/NodeApp/src/helpers/AutoCompletionHelper.ts
+++ b/NodeApp/src/helpers/AutoCompletionHelper.ts
@@ -3,6 +3,7 @@ import { existsSync, renameSync } from 'fs';
 import ora                        from 'ora';
 import TextStyle                  from '../types/TextStyle';
 import inquirer                   from 'inquirer';
+import fs                         from 'fs-extra';
 
 
 function renameFile(filename: string, showWarning: boolean) {
@@ -126,6 +127,19 @@ function addLine(identLevel: number, pattern: string): string {
     return `${ '    '.repeat(identLevel) }${ pattern }\n`;
 }
 
+export function getCommandFromChain(currentCmd: Command, chain: Array<string>): Command | null {
+    if ( chain.length === 0 ) {
+        return currentCmd;
+    } else {
+        const subCmd = currentCmd.commands.find(c => c.name() === chain[0]);
+        if ( subCmd === undefined ) {
+            return currentCmd;
+        } else {
+            return getCommandFromChain(subCmd, chain.slice(1));
+        }
+    }
+}
+
 function generateBashSubCommands(cmd: Command, current: number, maxDepth: number, ident: number): string {
     if ( current === maxDepth ) {
         return addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`) + addLine(ident + 2, `words="${ commandsAndOptionsToString(cmd) }"`) + addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac');
@@ -178,6 +192,32 @@ export function generateFishCompletion(root: Command): string {
                                                                                                                                                                              commands.filter(c => !isHidden(c)).filter(cmd => !isLeaf(cmd)).map(cmd => cmd.commands.filter(c => !isHidden(c)).map(subCmd => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a ${ subCmd.name() } -d "${ subCmd.description() }"`).join('\n').concat('\n')).join(''));
 }
 
+export function updateRcFile(shellType: 'bash' | 'zsh', filePath: string, completionCommand: string) {
+    const spinner: ora.Ora = ora(`Modifying ${ filePath } ...`).start();
+    if ( fs.existsSync(filePath) ) {
+        const data = fs.readFileSync(filePath);
+        let updated = false;
+        try {
+            if ( !data.includes(completionCommand) ) {
+                fs.appendFileSync(filePath, completionCommand);
+                updated = true;
+            }
+        } catch {
+            spinner.fail(`Error appending in ${ filePath }`);
+            return;
+        }
+
+        spinner.succeed(updated ? `${ shellType } updated.` : `${ shellType } already up to date.`);
+    } else {
+        try {
+            fs.writeFileSync(filePath, completionCommand);
+            spinner.succeed(`${ shellType } written.`);
+        } catch ( error ) {
+            spinner.fail(`Error writing in ${ filePath }`);
+        }
+    }
+}
+
 
 // The following code should create a bash completion automatically from the Commander
 // CLI library. The file should look something like that (it looks at the time
-- 
GitLab