From d7cdfd51b923d2d5720864b228678749f3010fb8 Mon Sep 17 00:00:00 2001 From: Renan <6718144+renancaraujo@users.noreply.github.com> Date: Tue, 15 Nov 2022 11:56:24 +0000 Subject: [PATCH] feat: add installation process for zsh (#7) --- .gitignore | 4 +- .idea/.gitignore | 1 + .idea/cli_completion.iml | 15 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + lib/cli_completion.dart | 11 - lib/install.dart | 5 + lib/src/cli_completion.dart | 14 - lib/src/exceptions.dart | 20 + lib/src/install/install_completion.dart | 36 ++ .../shell_completion_configuration.dart | 70 +++ .../shell_completion_installation.dart | 269 ++++++++++++ pubspec.yaml | 5 + test/src/cli_completion_test.dart | 18 - test/src/install/install_completion_test.dart | 79 ++++ .../shell_completion_configuration_test.dart | 46 ++ .../shell_completion_installation_test.dart | 413 ++++++++++++++++++ 18 files changed, 982 insertions(+), 44 deletions(-) create mode 100644 .idea/cli_completion.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml delete mode 100644 lib/cli_completion.dart create mode 100644 lib/install.dart delete mode 100644 lib/src/cli_completion.dart create mode 100644 lib/src/exceptions.dart create mode 100644 lib/src/install/install_completion.dart create mode 100644 lib/src/install/shell_completion_configuration.dart create mode 100644 lib/src/install/shell_completion_installation.dart delete mode 100644 test/src/cli_completion_test.dart create mode 100644 test/src/install/install_completion_test.dart create mode 100644 test/src/install/shell_completion_configuration_test.dart create mode 100644 test/src/install/shell_completion_installation_test.dart diff --git a/.gitignore b/.gitignore index 526da15..6fe3dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ .dart_tool/ .packages build/ -pubspec.lock \ No newline at end of file +pubspec.lock + +coverage/ diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d3352..644cc1f 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,4 @@ # Default ignored files /shelf/ /workspace.xml +/libraries \ No newline at end of file diff --git a/.idea/cli_completion.iml b/.idea/cli_completion.iml new file mode 100644 index 0000000..bfd1fc8 --- /dev/null +++ b/.idea/cli_completion.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b6d9a9b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/cli_completion.dart b/lib/cli_completion.dart deleted file mode 100644 index e354c04..0000000 --- a/lib/cli_completion.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2022, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -/// Completion toolkit for Command runner based Dart CLIs -library cli_completion; - -export 'src/cli_completion.dart'; diff --git a/lib/install.dart b/lib/install.dart new file mode 100644 index 0000000..b559686 --- /dev/null +++ b/lib/install.dart @@ -0,0 +1,5 @@ +/// Contains the functions related to the installation file +library install; + +export 'src/install/install_completion.dart'; +export 'src/install/shell_completion_installation.dart'; diff --git a/lib/src/cli_completion.dart b/lib/src/cli_completion.dart deleted file mode 100644 index 2ce3e7a..0000000 --- a/lib/src/cli_completion.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2022, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -/// {@template cli_completion} -/// Completion toolkit for Command runner based Dart CLIs -/// {@endtemplate} -class CliCompletion { - /// {@macro cli_completion} - const CliCompletion(); -} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart new file mode 100644 index 0000000..06106b1 --- /dev/null +++ b/lib/src/exceptions.dart @@ -0,0 +1,20 @@ +/// {@template completion_installation_exception} +/// Describes an exception during the installation of completion scripts. +/// {@endtemplate} +class CompletionInstallationException implements Exception { + /// {@macro completion_installation_exception} + CompletionInstallationException({ + required this.message, + required this.rootCommand, + }); + + /// The error message for this exception + final String message; + + /// The command for which the installation failed. + final String rootCommand; + + @override + String toString() => 'Could not install completion scripts for $rootCommand: ' + '$message'; +} diff --git a/lib/src/install/install_completion.dart b/lib/src/install/install_completion.dart new file mode 100644 index 0000000..1f9e3f3 --- /dev/null +++ b/lib/src/install/install_completion.dart @@ -0,0 +1,36 @@ +import 'package:cli_completion/src/exceptions.dart'; +import 'package:cli_completion/src/install/shell_completion_installation.dart'; + +import 'package:mason_logger/mason_logger.dart'; + +/// Install completion configuration hooks for a [rootCommand] in the +/// current shell. +void installCompletion({ + required Logger logger, + required String rootCommand, + bool? isWindowsOverride, + Map? environmentOverride, +}) { + logger + ..detail('Completion installation for $rootCommand started') + ..detail('Identifying system shell'); + + final completionInstallation = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: isWindowsOverride, + environmentOverride: environmentOverride, + ); + + if (completionInstallation == null) { + throw CompletionInstallationException( + message: 'Unknown shell.', + rootCommand: rootCommand, + ); + } + + logger.detail( + 'Shell identified as ${completionInstallation.configuration.name}', + ); + + completionInstallation.install(rootCommand); +} diff --git a/lib/src/install/shell_completion_configuration.dart b/lib/src/install/shell_completion_configuration.dart new file mode 100644 index 0000000..f05e0f2 --- /dev/null +++ b/lib/src/install/shell_completion_configuration.dart @@ -0,0 +1,70 @@ +import 'package:meta/meta.dart'; + +/// A type definition for functions that creates the content of a +/// completion script given a [rootCommand] +typedef CompletionScriptTemplate = String Function(String rootCommand); + +/// A type definition for functions that describes +/// the source line given a [scriptPath] +typedef SourceStringTemplate = String Function(String scriptPath); + +/// {@template shell_completion_configuration} +/// Describes the configuration of a completion script in a specific shell. +/// +/// See: +/// - [zshConfiguration] for zsh +@immutable +class ShellCompletionConfiguration { + /// {@macro shell_completion_configuration} + @visibleForTesting + const ShellCompletionConfiguration({ + required this.name, + required this.shellRCFile, + required this.sourceLineTemplate, + required this.scriptTemplate, + }); + + /// A descriptive string to identify the shell among others. + final String name; + + /// The location of a config file that is run upon shell start. + /// Eg: .bashrc or .zshrc + final String shellRCFile; + + /// Generates a line to sources of a script file. + final SourceStringTemplate sourceLineTemplate; + + /// Generates the contents of a completion script. + final CompletionScriptTemplate scriptTemplate; + + /// The name for the config file for this shell. + String get completionConfigForShellFileName => '$name-config.$name'; +} + +/// A [ShellCompletionConfiguration] for zsh. +final zshConfiguration = ShellCompletionConfiguration( + name: 'zsh', + shellRCFile: '~/.zshrc', + sourceLineTemplate: (String scriptPath) { + return '[[ -f $scriptPath ]] && . $scriptPath || true'; + }, + scriptTemplate: (String rootCommand) { + // Completion script for zsh. + // + // Based on https://github.com/mklabs/tabtab/blob/master/lib/scripts/zsh.sh + return ''' +if type compdef &>/dev/null; then + _${rootCommand}_completion () { + local reply + local si=\$IFS + + IFS=\$'\n' reply=(\$(COMP_CWORD="\$((CURRENT-1))" COMP_LINE="\$BUFFER" COMP_POINT="\$CURSOR" $rootCommand completion -- "\${words[@]}")) + IFS=\$si + + _describe 'values' reply + } + compdef _${rootCommand}_completion $rootCommand +fi +'''; + }, +); diff --git a/lib/src/install/shell_completion_installation.dart b/lib/src/install/shell_completion_installation.dart new file mode 100644 index 0000000..b0c2fe4 --- /dev/null +++ b/lib/src/install/shell_completion_installation.dart @@ -0,0 +1,269 @@ +import 'dart:io'; + +import 'package:cli_completion/src/exceptions.dart'; +import 'package:cli_completion/src/install/shell_completion_configuration.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; + +/// {@template shell_completion_installation} +/// A description of a completion installation process for a specific shell. +/// +/// Creation should be done via [ShellCompletionInstallation.fromCurrentShell]. +/// {@endtemplate} +class ShellCompletionInstallation { + /// {@macro shell_completion_installation} + @visibleForTesting + ShellCompletionInstallation({ + required this.configuration, + required this.logger, + required this.isWindows, + required this.environment, + }); + + /// Identify the current shell and from there, create a + /// [ShellCompletionInstallation] with the proper + /// [ShellCompletionConfiguration]. + static ShellCompletionInstallation? fromCurrentShell({ + required Logger logger, + bool? isWindowsOverride, + Map? environmentOverride, + }) { + final environment = environmentOverride ?? Platform.environment; + final isWindows = isWindowsOverride ?? Platform.isWindows; + + // TODO(renancaraujo): this detects the "login shell", which can be + // different from the actual shell. + final envShell = environment['SHELL']; + if (envShell == null || envShell.isEmpty) return null; + + final basename = path.basename(envShell); + + final ShellCompletionConfiguration configuration; + if (basename == 'zsh') { + configuration = zshConfiguration; + } else { + return null; + } + + return ShellCompletionInstallation( + configuration: configuration, + logger: logger, + isWindows: isWindows, + environment: environment, + ); + } + + /// The injected [Logger]; + final Logger logger; + + /// Defines whether the installation is running on windows or not. + final bool isWindows; + + /// Describes the environment variables. Usually + /// equals to [Platform.environment]. + final Map environment; + + /// The associated [ShellCompletionConfiguration]. + final ShellCompletionConfiguration configuration; + + /// Define the [Directory] in which the + /// completion configuration files will be stored. + @visibleForTesting + Directory get completionConfigDir { + if (isWindows) { + // Use localappdata on windows + final localAppData = environment['LOCALAPPDATA']!; + return Directory(path.join(localAppData, 'DartCLICompletion')); + } else { + // Use home on posix systems + final home = environment['HOME']!; + return Directory(path.join(home, '.dart-cli-completion')); + } + } + + /// Perform the installation process. + void install(String rootCommand) { + logger.detail( + 'Installing completion for the command $rootCommand ' + 'on ${configuration.name}', + ); + + createCompletionConfigDir(); + writeCompletionScriptForCommand(rootCommand); + writeCompletionConfigForShell(rootCommand); + writeToShellConfigFile(rootCommand); + } + + /// Create a directory in which the completion config files shall be saved + @visibleForTesting + void createCompletionConfigDir() { + final completionConfigDirPath = completionConfigDir.path; + + logger.detail( + 'Creating completion configuration directory ' + 'at $completionConfigDirPath', + ); + + if (completionConfigDir.existsSync()) { + logger.detail( + 'A ${completionConfigDir.path} directory was already found.', + ); + return; + } + + completionConfigDir.createSync(); + } + + /// Creates a configuration file exclusively to [rootCommand] and the + /// identified shell. + @visibleForTesting + void writeCompletionScriptForCommand(String rootCommand) { + final completionConfigDirPath = completionConfigDir.path; + final commandScriptName = '$rootCommand.${configuration.name}'; + final commandScriptPath = path.join( + completionConfigDirPath, + commandScriptName, + ); + logger.detail( + 'Writing completion script for $rootCommand on $commandScriptPath', + ); + + final scriptFile = File(commandScriptPath); + + if (scriptFile.existsSync()) { + logger.detail( + 'A script file for $rootCommand was already found on ' + '$commandScriptPath.', + ); + return; + } + + scriptFile.writeAsStringSync(configuration.scriptTemplate(rootCommand)); + } + + /// Adds a reference for the command-specific config file created on + /// [writeCompletionScriptForCommand] the the global completion config file. + @visibleForTesting + void writeCompletionConfigForShell(String rootCommand) { + final completionConfigDirPath = completionConfigDir.path; + + final configPath = path.join( + completionConfigDirPath, + configuration.completionConfigForShellFileName, + ); + logger.detail('Adding config for $rootCommand config entry to $configPath'); + + final configFile = File(configPath); + + if (!configFile.existsSync()) { + logger.detail('No file found at $configPath, creating one now'); + configFile.createSync(); + } + final commandScriptName = '$rootCommand.${configuration.name}'; + + final containsLine = + configFile.readAsStringSync().contains(commandScriptName); + + if (containsLine) { + logger.detail( + 'A config entry for $rootCommand was already found on $configPath.', + ); + return; + } + + _sourceScriptOnFile( + configFile: configFile, + scriptName: rootCommand, + scriptPath: path.join(completionConfigDirPath, commandScriptName), + ); + } + + String get _shellRCFilePath => + _resolveHome(configuration.shellRCFile, environment); + + /// Write a source to the completion global script in the shell configuration + /// file, which its location is described by the [configuration] + @visibleForTesting + void writeToShellConfigFile(String rootCommand) { + logger.detail( + 'Adding dart cli completion config entry ' + 'to $_shellRCFilePath', + ); + + final completionConfigDirPath = completionConfigDir.path; + + final completionConfigPath = path.join( + completionConfigDirPath, + configuration.completionConfigForShellFileName, + ); + + final shellRCFile = File(_shellRCFilePath); + + if (!shellRCFile.existsSync()) { + throw CompletionInstallationException( + rootCommand: rootCommand, + message: 'No file found at ${shellRCFile.path}', + ); + } + + final containsLine = + shellRCFile.readAsStringSync().contains(completionConfigPath); + + if (containsLine) { + logger.detail('A completion config entry was found on' + ' $_shellRCFilePath.'); + return; + } + + _sourceScriptOnFile( + configFile: shellRCFile, + scriptName: 'Completion', + description: 'Completion scripts setup. ' + 'Remove the following line to uninstall', + scriptPath: path.join( + completionConfigDir.path, + configuration.completionConfigForShellFileName, + ), + ); + } + + void _sourceScriptOnFile({ + required File configFile, + required String scriptName, + required String scriptPath, + String? description, + }) { + assert( + configFile.existsSync(), + 'Sourcing a script line into an nonexistent config file.', + ); + + final configFilePath = configFile.path; + + description ??= 'Completion config for "$scriptName"'; + + configFile.writeAsStringSync( + mode: FileMode.append, + ''' +## [$scriptName] +## $description +${configuration.sourceLineTemplate(scriptPath)} +## [/$scriptName] + +''', + ); + + logger.detail('Added config to $configFilePath'); + } +} + +/// Resolve the home from a path string +String _resolveHome( + String originalPath, + Map environment, +) { + final after = originalPath.split('~/').last; + final home = path.absolute(environment['HOME']!); + return path.join(home, after); +} diff --git a/pubspec.yaml b/pubspec.yaml index 2c2f8e7..6ec3890 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,6 +6,11 @@ publish_to: none environment: sdk: ">=2.18.0 <3.0.0" +dependencies: + mason_logger: ^0.2.2 + meta: ^1.8.0 + path: ^1.8.2 + dev_dependencies: mocktail: ^0.3.0 test: ^1.19.2 diff --git a/test/src/cli_completion_test.dart b/test/src/cli_completion_test.dart deleted file mode 100644 index 1c9401b..0000000 --- a/test/src/cli_completion_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2022, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import 'package:cli_completion/cli_completion.dart'; -// ignore_for_file: prefer_const_constructors -import 'package:test/test.dart'; - -void main() { - group('CliCompletion', () { - test('can be instantiated', () { - expect(CliCompletion(), isNotNull); - }); - }); -} diff --git a/test/src/install/install_completion_test.dart b/test/src/install/install_completion_test.dart new file mode 100644 index 0000000..b0d0e41 --- /dev/null +++ b/test/src/install/install_completion_test.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:cli_completion/install.dart'; +import 'package:cli_completion/src/exceptions.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +class MockLogger extends Mock implements Logger {} + +void main() { + late Logger logger; + late Directory tempDir; + + setUp(() { + logger = MockLogger(); + tempDir = Directory.systemTemp.createTempSync(); + }); + + group('installCompletion', () { + test('installs for zsh', () { + File(path.join(tempDir.path, '.zshrc')).createSync(); + + installCompletion( + logger: logger, + rootCommand: 'very_good', + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '/foo/bar/zsh', + 'HOME': tempDir.path, + }, + ); + + verify( + () => logger.detail( + 'Completion installation for very_good started', + ), + ); + + verify( + () => logger.detail( + 'Shell identified as zsh', + ), + ); + + expect(tempDir.listSync().map((e) => path.basename(e.path)), [ + '.zshrc', + '.dart-cli-completion', + ]); + }); + + test('do nothing on unknown shells', () { + expect( + () { + installCompletion( + logger: logger, + rootCommand: 'very_good', + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '/foo/bar/someshell', + 'HOME': tempDir.path, + }, + ); + }, + throwsA( + predicate( + (e) { + return e is CompletionInstallationException && + e.toString() == + 'Could not install completion scripts for very_good: ' + 'Unknown shell.'; + }, + ), + ), + ); + }); + }); +} diff --git a/test/src/install/shell_completion_configuration_test.dart b/test/src/install/shell_completion_configuration_test.dart new file mode 100644 index 0000000..5bbc5e0 --- /dev/null +++ b/test/src/install/shell_completion_configuration_test.dart @@ -0,0 +1,46 @@ +import 'package:cli_completion/src/install/shell_completion_configuration.dart'; +import 'package:test/test.dart'; + +void main() { + group('ShellCompletionConfiguration', () { + group('zshConfiguration', () { + test('name', () { + expect(zshConfiguration.name, 'zsh'); + }); + + test('shellRCFile', () { + expect(zshConfiguration.shellRCFile, '~/.zshrc'); + }); + + test('sourceStringTemplate', () { + final result = zshConfiguration.sourceLineTemplate('./pans/snaps'); + expect(result, '[[ -f ./pans/snaps ]] && . ./pans/snaps || true'); + }); + + test('completionScriptTemplate', () { + final result = zshConfiguration.scriptTemplate('very_good'); + expect(result, ''' +if type compdef &>/dev/null; then + _very_good_completion () { + local reply + local si=\$IFS + + IFS=\$'\n' reply=(\$(COMP_CWORD="\$((CURRENT-1))" COMP_LINE="\$BUFFER" COMP_POINT="\$CURSOR" very_good completion -- "\${words[@]}")) + IFS=\$si + + _describe 'values' reply + } + compdef _very_good_completion very_good +fi +'''); + }); + + test('completionConfigForShellFileName', () { + expect( + zshConfiguration.completionConfigForShellFileName, + 'zsh-config.zsh', + ); + }); + }); + }); +} diff --git a/test/src/install/shell_completion_installation_test.dart b/test/src/install/shell_completion_installation_test.dart new file mode 100644 index 0000000..155295b --- /dev/null +++ b/test/src/install/shell_completion_installation_test.dart @@ -0,0 +1,413 @@ +import 'dart:io'; + +import 'package:cli_completion/install.dart'; +import 'package:cli_completion/src/exceptions.dart'; +import 'package:cli_completion/src/install/shell_completion_configuration.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +class MockLogger extends Mock implements Logger {} + +void main() { + late Logger logger; + late Directory tempDir; + + setUp(() { + logger = MockLogger(); + tempDir = Directory.systemTemp.createTempSync(); + }); + + group('ShellCompletionInstallation', () { + group('fromCurrentShell', () { + test('instantiated without env', () { + expect( + () => ShellCompletionInstallation.fromCurrentShell(logger: logger), + returnsNormally, + ); + }); + + test('identifies zsh', () { + final result = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '/foo/bar/zsh', + }, + ); + + expect(result?.configuration, zshConfiguration); + }); + + group('identifies no shell', () { + test('for no shell env', () { + final result = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: false, + environmentOverride: {}, + ); + + expect(result, null); + }); + + test('for empty shell env', () { + final result = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '', + }, + ); + + expect(result, null); + }); + + test('for extraneous shell', () { + final result = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '/usr/bin/someshell', + }, + ); + + expect(result, null); + }); + }); + }); + + group('completionConfigDir', () { + test('gets config dir location on windows', () { + final installation = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: true, + environmentOverride: { + 'SHELL': '/foo/bar/zsh', + 'LOCALAPPDATA': tempDir.path, + }, + ); + + expect( + installation!.completionConfigDir.path, + path.join(tempDir.path, 'DartCLICompletion'), + ); + }); + + test('gets config dir location on posix', () { + final installation = ShellCompletionInstallation.fromCurrentShell( + logger: logger, + isWindowsOverride: false, + environmentOverride: { + 'SHELL': '/foo/bar/zsh', + 'HOME': tempDir.path, + }, + ); + + installation!; + + expect( + installation.completionConfigDir.path, + path.join(tempDir.path, '.dart-cli-completion'), + ); + }); + }); + + group('install', () { + test('createCompletionConfigDir', () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + expect(installation.completionConfigDir.existsSync(), false); + + installation.createCompletionConfigDir(); + + expect(installation.completionConfigDir.existsSync(), true); + + verifyNever( + () => logger.detail( + any( + that: endsWith( + 'directory was already found.', + ), + ), + ), + ); + + installation.createCompletionConfigDir(); + + verify( + () => logger.detail( + any( + that: endsWith( + 'directory was already found.', + ), + ), + ), + ).called(1); + }); + + test('writeCompletionScriptForCommand', () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + final configDir = installation.completionConfigDir; + + final configFile = File(path.join(configDir.path, 'very_good.zsh')); + + expect(configFile.existsSync(), false); + + installation + ..createCompletionConfigDir() + ..writeCompletionScriptForCommand('very_good'); + + expect(configFile.existsSync(), true); + + expect( + configFile.readAsStringSync(), + zshConfiguration.scriptTemplate('very_good'), + ); + + verifyNever( + () => logger.detail( + any( + that: startsWith( + 'A script file for very_good was already found on ', + ), + ), + ), + ); + + installation.writeCompletionScriptForCommand('very_good'); + + verify( + () => logger.detail( + any( + that: startsWith( + 'A script file for very_good was already found on ', + ), + ), + ), + ).called(1); + }); + + test('writeCompletionConfigForShell', () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + final configDir = installation.completionConfigDir; + + final configFile = File(path.join(configDir.path, 'zsh-config.zsh')); + + expect(configFile.existsSync(), false); + + installation + ..createCompletionConfigDir() + ..writeCompletionConfigForShell('very_good'); + + expect(configFile.existsSync(), true); + + // ignore: leading_newlines_in_multiline_strings + expect(configFile.readAsStringSync(), '''## [very_good] +## Completion config for "very_good" +[[ -f ${configDir.path}/very_good.zsh ]] && . ${configDir.path}/very_good.zsh || true +## [/very_good] + +'''); + + verify( + () => logger.detail( + any( + that: startsWith( + 'No file found at ${configFile.path}', + ), + ), + ), + ).called(1); + + installation.writeCompletionConfigForShell('very_good'); + + verify( + () => logger.detail( + any( + that: startsWith( + 'A config entry for very_good was already found on', + ), + ), + ), + ).called(1); + }); + + test('writeToShellConfigFile', () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + final configDir = installation.completionConfigDir; + + // When the rc file cannot be found, throw an exception + expect( + () => installation.writeToShellConfigFile('very_good'), + throwsA( + predicate( + (e) { + return e is CompletionInstallationException && + e.toString() == + 'Could not install completion scripts for very_good: ' + 'No file found at ${path.join( + tempDir.path, + '.zshrc', + )}'; + }, + ), + ), + ); + + final rcFile = File(path.join(tempDir.path, '.zshrc'))..createSync(); + + installation.writeToShellConfigFile('very_good'); + + // ignore: leading_newlines_in_multiline_strings + expect(rcFile.readAsStringSync(), '''## [Completion] +## Completion scripts setup. Remove the following line to uninstall +[[ -f ${configDir.path}/zsh-config.zsh ]] && . ${configDir.path}/zsh-config.zsh || true +## [/Completion] + +'''); + }); + + test( + 'installing completion for a command when it is already installed', + () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + File(path.join(tempDir.path, '.zshrc')).createSync(); + + installation.install('very_good'); + + reset(logger); + + // install again + installation.install('very_good'); + + verify( + () => logger.detail( + 'A ${installation.completionConfigDir.path} directory was already' + ' found.', + ), + ).called(1); + verify( + () => logger.detail( + 'A script file for very_good was already found on ${path.join( + installation.completionConfigDir.path, + 'very_good.zsh', + )}.', + ), + ).called(1); + verify( + () => logger.detail( + 'A config entry for very_good was already found on ' + '${path.join( + installation.completionConfigDir.path, + 'zsh-config.zsh', + )}.', + ), + ).called(1); + + verify( + () => logger.detail( + 'A completion config entry was found on ' + '${path.join(tempDir.path, '.zshrc')}.', + ), + ).called(1); + }, + ); + + test( + 'installing completion for two different commands', + () { + final installation = ShellCompletionInstallation( + logger: logger, + isWindows: false, + environment: { + 'HOME': tempDir.path, + }, + configuration: zshConfiguration, + ); + + final rcFile = File(path.join(tempDir.path, '.zshrc'))..createSync(); + + final configDir = installation.completionConfigDir; + + installation + ..install('very_good') + ..install('not_good'); + + // rc fle includes one reference to the global config + + // ignore: leading_newlines_in_multiline_strings + expect(rcFile.readAsStringSync(), '''## [Completion] +## Completion scripts setup. Remove the following line to uninstall +[[ -f ${configDir.path}/zsh-config.zsh ]] && . ${configDir.path}/zsh-config.zsh || true +## [/Completion] + +'''); + + // global config includes one reference for each command + final globalConfig = File( + path.join(configDir.path, 'zsh-config.zsh'), + ); + + // ignore: leading_newlines_in_multiline_strings + expect(globalConfig.readAsStringSync(), '''## [very_good] +## Completion config for "very_good" +[[ -f ${configDir.path}/very_good.zsh ]] && . ${configDir.path}/very_good.zsh || true +## [/very_good] + +## [not_good] +## Completion config for "not_good" +[[ -f ${configDir.path}/not_good.zsh ]] && . ${configDir.path}/not_good.zsh || true +## [/not_good] + +'''); + + expect(configDir.listSync().map((e) => path.basename(e.path)), [ + 'not_good.zsh', + 'very_good.zsh', + 'zsh-config.zsh', + ]); + }, + ); + }); + }); +}