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',
+ ]);
+ },
+ );
+ });
+ });
+}