From addf807bc3e37e33ca979c206710890f0c37f3d7 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 29 May 2024 13:47:28 +0200 Subject: [PATCH 1/7] Add fish completion support --- .../scala/caseapp/core/complete/Fish.scala | 30 +++++++++++++++++++ .../test/scala/caseapp/CompletionTests.scala | 9 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 core/shared/src/main/scala/caseapp/core/complete/Fish.scala diff --git a/core/shared/src/main/scala/caseapp/core/complete/Fish.scala b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala new file mode 100644 index 00000000..6652ab60 --- /dev/null +++ b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala @@ -0,0 +1,30 @@ +package caseapp.core.complete + +object Fish { + + val shellName: String = + "fish" + val id: String = + s"$shellName-v1" + + def script(progName: String): String = + s""" + complete $progName -a '($progName complete $id (math 1 + (count (__fish_print_cmd_args))) (__fish_print_cmd_args))' + |""".stripMargin + + private def escape(s: String): String = + s.replace("\t", " ").linesIterator.to(LazyList).headOption.getOrElse("") + def print(items: Seq[CompletionItem]): String = { + val newLine = System.lineSeparator() + val b = new StringBuilder + for (item <- items; value <- item.values) { + b.append(escape(value)) + for (desc <- item.description) { + b.append("\t") + b.append(escape(desc)) + } + b.append(newLine) + } + b.result() + } +} diff --git a/tests/shared/src/test/scala/caseapp/CompletionTests.scala b/tests/shared/src/test/scala/caseapp/CompletionTests.scala index 9ae0e58d..56b9ca69 100644 --- a/tests/shared/src/test/scala/caseapp/CompletionTests.scala +++ b/tests/shared/src/test/scala/caseapp/CompletionTests.scala @@ -1,6 +1,6 @@ package caseapp -import caseapp.core.complete.{Bash, CompletionItem, Zsh} +import caseapp.core.complete.{Bash, CompletionItem, Fish, Zsh} import utest._ object CompletionTests extends TestSuite { @@ -155,6 +155,13 @@ object CompletionTests extends TestSuite { assert(compRely.contains(expectedCompRely)) } + test("fish") { + val res = Prog.complete(Seq("back-tick", "-"), 1) + val compRely = Fish.print(res) + val expectedCompRely = "--backtick\tA pattern with backtick `--`\n".stripMargin + + assert(compRely.contains(expectedCompRely)) + } test("zsh") { val res = Prog.complete(Seq("back-tick", "-"), 1) val expected = List( From 2c2810e7ae3fd8ce906da3b9c640a2a822cdc7a2 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 29 May 2024 13:50:43 +0200 Subject: [PATCH 2/7] Add fish shell handling to install/uninstall commands --- .../core/app/PlatformCommandsMethods.scala | 140 ++++++++++++------ .../caseapp/core/app/CommandsEntryPoint.scala | 5 +- 2 files changed, 99 insertions(+), 46 deletions(-) diff --git a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala index ad7d64c0..45a149d4 100644 --- a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala +++ b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala @@ -1,6 +1,6 @@ package caseapp.core.app -import caseapp.core.complete.{Bash, Zsh} +import caseapp.core.complete.{Bash, Fish, Zsh} import java.io.File import java.nio.charset.{Charset, StandardCharsets} @@ -21,8 +21,61 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => // Adapted from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletions.scala def completionsInstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) + val (options, _) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) + PlatformCommandsMethods.completionsInstall( + completionsWorkingDirectory, + options, + progName = progName, + installCompletionsCommand = s"${completionsCommandName.mkString(" ")} install", + printLine = printLine(_, _), + exit = exit + ) + } + + def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { + val (options, _) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) + PlatformCommandsMethods.completionsUninstall( + completionsWorkingDirectory, + options, + progName = progName, + printLine = printLine(_, _) + ) + } +} + +object PlatformCommandsMethods { + import caseapp.{HelpMessage, Name} + import caseapp.core.help.Help + import caseapp.core.parser.Parser + + // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala + // format: off + final case class CompletionsInstallOptions( + @HelpMessage("Print completions to stdout") + env: Boolean = false, + @HelpMessage("Custom completions name") + name: Option[String] = None, + @HelpMessage("Name of the shell, either zsh, fish or bash") + @Name("shell") + format: Option[String] = None, + @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") + @Name("o") + output: Option[String] = None, + @HelpMessage("Custom banner in comment placed in rc file (bash or zsh only)") + banner: String = "{NAME} completions", + @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") + rcFile: Option[String] = None + ) + // format: on + def completionsInstall( + completionsWorkingDirectory: String, + options: PlatformCommandsMethods.CompletionsInstallOptions, + progName: String, + installCompletionsCommand: String, + printLine: (message: String, toStderr: Boolean) => Unit, + exit: (code: Int) => Nothing + ): Unit = { lazy val completionsDir = Paths.get(options.output.getOrElse(completionsWorkingDirectory)) val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) @@ -32,14 +85,11 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => toStderr = true ) printLine("", toStderr = true) - printLine( - s" $name ${completionsCommandName.mkString(" ")} install --shell zsh", - toStderr = true - ) - printLine( - s" $name ${completionsCommandName.mkString(" ")} install --shell bash", - toStderr = true - ) + for (shell <- Seq(Bash.shellName, Zsh.shellName, Fish.shellName)) + printLine( + s" $name $installCompletionsCommand --shell $shell", + toStderr = true + ) printLine("", toStderr = true) exit(1) } @@ -49,6 +99,15 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => val script = Bash.script(name) val defaultRcFile = Paths.get(sys.props("user.home")).resolve(".bashrc") (script, defaultRcFile) + case Fish.id | Fish.shellName => + val script = Fish.script(name) + val defaultRcFile = + Option(System.getenv("XDG_CONFIG_HOME")).map(Paths.get(_)) + .getOrElse(Paths.get(sys.props("user.home"), ".config")) + .resolve("fish") + .resolve("completions") + .resolve(s"$name.fish") + (script, defaultRcFile) case Zsh.id | Zsh.shellName => val completionScript = Zsh.script(name) val zDotDir = Paths.get(Option(System.getenv("ZDOTDIR")).getOrElse(sys.props("user.home"))) @@ -59,7 +118,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => val needsWrite = !Files.exists(completionScriptDest) || !Arrays.equals(Files.readAllBytes(completionScriptDest), content) if (needsWrite) { - printLine(s"Writing $completionScriptDest") + printLine(s"Writing $completionScriptDest", toStderr = false) Files.createDirectories(completionScriptDest.getParent) Files.write(completionScriptDest, content) } @@ -69,14 +128,19 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => ).map(_ + System.lineSeparator()).mkString (script, defaultRcFile) case _ => - printLine(s"Unrecognized or unsupported shell: $format") + printLine(s"Unrecognized or unsupported shell: $format", toStderr = true) exit(1) } if (options.env) println(rcScript) else { - val rcFile = options.rcFile.map(Paths.get(_)).getOrElse(defaultRcFile) + val rcFile = format match { + case Fish.id | Fish.shellName => + options.output.map(Paths.get(_, s"$name.fish")).getOrElse(defaultRcFile) + case _ => + options.rcFile.map(Paths.get(_)).getOrElse(defaultRcFile) + } val banner = options.banner.replace("{NAME}", name) val updated = ProfileFileUpdater.addToProfileFile( rcFile, @@ -87,7 +151,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => val q = "\"" val evalCommand = - s"eval $q$$($progName ${completionsCommandName.mkString(" ")} install --env)$q" + s"eval $q$$($progName $installCompletionsCommand --env)$q" if (updated) { printLine(s"Updated $rcFile", toStderr = true) printLine("", toStderr = true) @@ -116,16 +180,24 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => } } - def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) - + def completionsUninstall( + completionsWorkingDirectory: String, + options: PlatformCommandsMethods.CompletionsUninstallOptions, + progName: String, + printLine: (message: String, toStderr: Boolean) => Unit + ): Unit = { val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) val home = Paths.get(sys.props("user.home")) val zDotDir = Option(System.getenv("ZDOTDIR")).map(Paths.get(_)).getOrElse(home) + val fishCompletionsDir = options.output.map(Paths.get(_)) + .getOrElse(sys.env.get("XDG_CONFIG_HOME").map(Paths.get(_)).getOrElse(home) + .resolve("fish") + .resolve("completions")) val rcFiles = options.rcFile.map(file => Seq(Paths.get(file))).getOrElse(Seq( zDotDir.resolve(".zshrc"), - home.resolve(".bashrc") + home.resolve(".bashrc"), + fishCompletionsDir.resolve(s"$name.fish") )).filter(Files.exists(_)) for (rcFile <- rcFiles) { @@ -145,32 +217,6 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => printLine(s"No $name completion section found in $rcFile", toStderr = true) } } -} - -object PlatformCommandsMethods { - import caseapp.{HelpMessage, Name} - import caseapp.core.help.Help - import caseapp.core.parser.Parser - - // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala - // format: off - final case class CompletionsInstallOptions( - @HelpMessage("Print completions to stdout") - env: Boolean = false, - @HelpMessage("Custom completions name") - name: Option[String] = None, - @HelpMessage("Name of the shell, either zsh or bash") - @Name("shell") - format: Option[String] = None, - @HelpMessage("Completions output directory") - @Name("o") - output: Option[String] = None, - @HelpMessage("Custom banner in comment placed in rc file") - banner: String = "{NAME} completions", - @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell") - rcFile: Option[String] = None - ) - // format: on object CompletionsInstallOptions { implicit lazy val parser: Parser[CompletionsInstallOptions] = Parser.derive @@ -180,10 +226,13 @@ object PlatformCommandsMethods { // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/uninstallcompletions/SharedUninstallCompletionsOptions.scala // format: off final case class CompletionsUninstallOptions( - @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell") + @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") rcFile: Option[String] = None, @HelpMessage("Custom banner in comment placed in rc file") banner: String = "{NAME} completions", + @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") + @Name("o") + output: Option[String] = None, @HelpMessage("Custom completions name") name: Option[String] = None ) @@ -199,6 +248,7 @@ object PlatformCommandsMethods { .orElse { Option(System.getenv("SHELL")).map(_.split(File.separator).last).map { case Bash.shellName => Bash.id + case Fish.shellName => Fish.id case Zsh.shellName => Zsh.id case other => other } diff --git a/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala b/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala index 54108a3a..41c9df8e 100644 --- a/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala +++ b/core/shared/src/main/scala/caseapp/core/app/CommandsEntryPoint.scala @@ -1,7 +1,7 @@ package caseapp.core.app import caseapp.core.commandparser.RuntimeCommandParser -import caseapp.core.complete.{Bash, CompletionItem, Zsh} +import caseapp.core.complete.{Bash, CompletionItem, Fish, Zsh} import caseapp.core.help.{Help, HelpFormat, RuntimeCommandHelp, RuntimeCommandsHelp} abstract class CommandsEntryPoint extends PlatformCommandsMethods { @@ -70,6 +70,7 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods { def script(format: String): String = format match { case Bash.shellName | Bash.id => Bash.script(progName) + case Fish.shellName | Fish.id => Fish.script(progName) case Zsh.shellName | Zsh.id => Zsh.script(progName) case _ => completeUnrecognizedFormat(format) @@ -114,6 +115,8 @@ abstract class CommandsEntryPoint extends PlatformCommandsMethods { format match { case Bash.id => printLine(Bash.print(items)) + case Fish.id => + printLine(Fish.print(items)) case Zsh.id => printLine(Zsh.print(items)) case _ => From 920fb43865d3f35c5f2c9abb5bcc91768ab9de45 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 4 Jun 2024 14:22:13 +0200 Subject: [PATCH 3/7] Make syntax scala-2 compatible --- .../core/app/PlatformCommandsMethods.scala | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala index 45a149d4..7649b959 100644 --- a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala +++ b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala @@ -73,8 +73,10 @@ object PlatformCommandsMethods { options: PlatformCommandsMethods.CompletionsInstallOptions, progName: String, installCompletionsCommand: String, - printLine: (message: String, toStderr: Boolean) => Unit, - exit: (code: Int) => Nothing + /* (message: String, toStderr: Boolean) => Unit */ + printLine: (String, Boolean) => Unit, + /* (code: Int) */ + exit: Int => Nothing ): Unit = { lazy val completionsDir = Paths.get(options.output.getOrElse(completionsWorkingDirectory)) @@ -82,15 +84,15 @@ object PlatformCommandsMethods { val format = PlatformCommandsMethods.getFormat(options.format).getOrElse { printLine( "Cannot determine current shell, pass the shell you use with --shell, like", - toStderr = true + true ) - printLine("", toStderr = true) + printLine("", true) for (shell <- Seq(Bash.shellName, Zsh.shellName, Fish.shellName)) printLine( s" $name $installCompletionsCommand --shell $shell", - toStderr = true + true ) - printLine("", toStderr = true) + printLine("", true) exit(1) } @@ -118,7 +120,7 @@ object PlatformCommandsMethods { val needsWrite = !Files.exists(completionScriptDest) || !Arrays.equals(Files.readAllBytes(completionScriptDest), content) if (needsWrite) { - printLine(s"Writing $completionScriptDest", toStderr = false) + printLine(s"Writing $completionScriptDest", false) Files.createDirectories(completionScriptDest.getParent) Files.write(completionScriptDest, content) } @@ -128,7 +130,7 @@ object PlatformCommandsMethods { ).map(_ + System.lineSeparator()).mkString (script, defaultRcFile) case _ => - printLine(s"Unrecognized or unsupported shell: $format", toStderr = true) + printLine(s"Unrecognized or unsupported shell: $format", true) exit(1) } @@ -153,29 +155,29 @@ object PlatformCommandsMethods { val evalCommand = s"eval $q$$($progName $installCompletionsCommand --env)$q" if (updated) { - printLine(s"Updated $rcFile", toStderr = true) - printLine("", toStderr = true) + printLine(s"Updated $rcFile", true) + printLine("", true) printLine( s"It is recommended to reload your shell, or source $rcFile in the " + "current session, for its changes to be taken into account.", - toStderr = true + true ) - printLine("", toStderr = true) + printLine("", true) printLine( "Alternatively, enable completions in the current session with", - toStderr = true + true ) - printLine("", toStderr = true) - printLine(s" $evalCommand", toStderr = true) - printLine("", toStderr = true) + printLine("", true) + printLine(s" $evalCommand", true) + printLine("", true) } else { - printLine(s"$rcFile already up-to-date.", toStderr = true) - printLine("", toStderr = true) - printLine("If needed, enable completions in the current session with", toStderr = true) - printLine("", toStderr = true) - printLine(s" $evalCommand", toStderr = true) - printLine("", toStderr = true) + printLine(s"$rcFile already up-to-date.", true) + printLine("", true) + printLine("If needed, enable completions in the current session with", true) + printLine("", true) + printLine(s" $evalCommand", true) + printLine("", true) } } } @@ -184,7 +186,8 @@ object PlatformCommandsMethods { completionsWorkingDirectory: String, options: PlatformCommandsMethods.CompletionsUninstallOptions, progName: String, - printLine: (message: String, toStderr: Boolean) => Unit + /* (message: String, toStderr: Boolean) => Unit */ + printLine: (String, Boolean) => Unit ): Unit = { val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) @@ -210,11 +213,11 @@ object PlatformCommandsMethods { ) if (updated) { - printLine(s"Updated $rcFile", toStderr = true) - printLine(s"$name completions uninstalled successfully", toStderr = true) + printLine(s"Updated $rcFile", true) + printLine(s"$name completions uninstalled successfully", true) } else - printLine(s"No $name completion section found in $rcFile", toStderr = true) + printLine(s"No $name completion section found in $rcFile", true) } } @@ -230,11 +233,11 @@ object PlatformCommandsMethods { rcFile: Option[String] = None, @HelpMessage("Custom banner in comment placed in rc file") banner: String = "{NAME} completions", + @HelpMessage("Custom completions name") + name: Option[String] = None, @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") @Name("o") output: Option[String] = None, - @HelpMessage("Custom completions name") - name: Option[String] = None ) // format: on From 927973295fa65c719c597796a7f58710ecff3ef9 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 4 Jun 2024 14:46:57 +0200 Subject: [PATCH 4/7] Reset MiMA ... due to an extra field added to PlatformCommandsMethods.CompletionsUninstallOPtions. --- project/Mima.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Mima.scala b/project/Mima.scala index a4f1b08f..9be90ba7 100644 --- a/project/Mima.scala +++ b/project/Mima.scala @@ -8,7 +8,7 @@ import scala.sys.process._ object Mima { def binaryCompatibilityVersions: Set[String] = - Seq("git", "tag", "--merged", "HEAD^", "--contains", "8e0544e5ce1f9ce1dd3bd85308332b5dd1cd4985") + Seq("git", "tag", "--merged", "HEAD^", "--contains", "920fb43865d3f35c5f2c9abb5bcc91768ab9de45") .!! .linesIterator .map(_.trim) From 3ff7fe37128a78e99c0cf1ea21d1636436c8d27f Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Tue, 4 Jun 2024 16:11:23 +0200 Subject: [PATCH 5/7] Minimize diff to ease review --- .../core/app/PlatformCommandsMethods.scala | 143 +++++++----------- 1 file changed, 54 insertions(+), 89 deletions(-) diff --git a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala index 7649b959..6a2912bc 100644 --- a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala +++ b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala @@ -21,78 +21,22 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => // Adapted from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletions.scala def completionsInstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, _) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) - PlatformCommandsMethods.completionsInstall( - completionsWorkingDirectory, - options, - progName = progName, - installCompletionsCommand = s"${completionsCommandName.mkString(" ")} install", - printLine = printLine(_, _), - exit = exit - ) - } - - def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { - val (options, _) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) - PlatformCommandsMethods.completionsUninstall( - completionsWorkingDirectory, - options, - progName = progName, - printLine = printLine(_, _) - ) - } -} - -object PlatformCommandsMethods { - import caseapp.{HelpMessage, Name} - import caseapp.core.help.Help - import caseapp.core.parser.Parser - - // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala - // format: off - final case class CompletionsInstallOptions( - @HelpMessage("Print completions to stdout") - env: Boolean = false, - @HelpMessage("Custom completions name") - name: Option[String] = None, - @HelpMessage("Name of the shell, either zsh, fish or bash") - @Name("shell") - format: Option[String] = None, - @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") - @Name("o") - output: Option[String] = None, - @HelpMessage("Custom banner in comment placed in rc file (bash or zsh only)") - banner: String = "{NAME} completions", - @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") - rcFile: Option[String] = None - ) - // format: on - - def completionsInstall( - completionsWorkingDirectory: String, - options: PlatformCommandsMethods.CompletionsInstallOptions, - progName: String, - installCompletionsCommand: String, - /* (message: String, toStderr: Boolean) => Unit */ - printLine: (String, Boolean) => Unit, - /* (code: Int) */ - exit: Int => Nothing - ): Unit = { + val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) lazy val completionsDir = Paths.get(options.output.getOrElse(completionsWorkingDirectory)) val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) val format = PlatformCommandsMethods.getFormat(options.format).getOrElse { printLine( "Cannot determine current shell, pass the shell you use with --shell, like", - true + toStderr = true ) - printLine("", true) + printLine("", toStderr = true) for (shell <- Seq(Bash.shellName, Zsh.shellName, Fish.shellName)) printLine( - s" $name $installCompletionsCommand --shell $shell", - true + s" $name ${completionsCommandName.mkString(" ")} install --shell $shell", + toStderr = true ) - printLine("", true) + printLine("", toStderr = true) exit(1) } @@ -120,7 +64,7 @@ object PlatformCommandsMethods { val needsWrite = !Files.exists(completionScriptDest) || !Arrays.equals(Files.readAllBytes(completionScriptDest), content) if (needsWrite) { - printLine(s"Writing $completionScriptDest", false) + printLine(s"Writing $completionScriptDest") Files.createDirectories(completionScriptDest.getParent) Files.write(completionScriptDest, content) } @@ -130,7 +74,7 @@ object PlatformCommandsMethods { ).map(_ + System.lineSeparator()).mkString (script, defaultRcFile) case _ => - printLine(s"Unrecognized or unsupported shell: $format", true) + printLine(s"Unrecognized or unsupported shell: $format", toStderr = true) exit(1) } @@ -153,42 +97,37 @@ object PlatformCommandsMethods { val q = "\"" val evalCommand = - s"eval $q$$($progName $installCompletionsCommand --env)$q" + s"eval $q$$($progName ${completionsCommandName.mkString(" ")} install --env)$q" if (updated) { - printLine(s"Updated $rcFile", true) - printLine("", true) + printLine(s"Updated $rcFile", toStderr = true) + printLine("", toStderr = true) printLine( s"It is recommended to reload your shell, or source $rcFile in the " + "current session, for its changes to be taken into account.", - true + toStderr = true ) - printLine("", true) + printLine("", toStderr = true) printLine( "Alternatively, enable completions in the current session with", - true + toStderr = true ) - printLine("", true) - printLine(s" $evalCommand", true) - printLine("", true) + printLine("", toStderr = true) + printLine(s" $evalCommand", toStderr = true) + printLine("", toStderr = true) } else { - printLine(s"$rcFile already up-to-date.", true) - printLine("", true) - printLine("If needed, enable completions in the current session with", true) - printLine("", true) - printLine(s" $evalCommand", true) - printLine("", true) + printLine(s"$rcFile already up-to-date.", toStderr = true) + printLine("", toStderr = true) + printLine("If needed, enable completions in the current session with", toStderr = true) + printLine("", toStderr = true) + printLine(s" $evalCommand", toStderr = true) + printLine("", toStderr = true) } } } - def completionsUninstall( - completionsWorkingDirectory: String, - options: PlatformCommandsMethods.CompletionsUninstallOptions, - progName: String, - /* (message: String, toStderr: Boolean) => Unit */ - printLine: (String, Boolean) => Unit - ): Unit = { + def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { + val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) val home = Paths.get(sys.props("user.home")) @@ -213,13 +152,39 @@ object PlatformCommandsMethods { ) if (updated) { - printLine(s"Updated $rcFile", true) - printLine(s"$name completions uninstalled successfully", true) + printLine(s"Updated $rcFile", toStderr = true) + printLine(s"$name completions uninstalled successfully", toStderr = true) } else - printLine(s"No $name completion section found in $rcFile", true) + printLine(s"No $name completion section found in $rcFile", toStderr = true) } } +} + +object PlatformCommandsMethods { + import caseapp.{HelpMessage, Name} + import caseapp.core.help.Help + import caseapp.core.parser.Parser + + // from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletionsOptions.scala + // format: off + final case class CompletionsInstallOptions( + @HelpMessage("Print completions to stdout") + env: Boolean = false, + @HelpMessage("Custom completions name") + name: Option[String] = None, + @HelpMessage("Name of the shell, either zsh, fish or bash") + @Name("shell") + format: Option[String] = None, + @HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)") + @Name("o") + output: Option[String] = None, + @HelpMessage("Custom banner in comment placed in rc file (bash or zsh only)") + banner: String = "{NAME} completions", + @HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)") + rcFile: Option[String] = None + ) + // format: on object CompletionsInstallOptions { implicit lazy val parser: Parser[CompletionsInstallOptions] = Parser.derive From 7362fec6ae7a6a9bdf1702a98a8d88bde2949182 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Tue, 4 Jun 2024 16:15:46 +0200 Subject: [PATCH 6/7] fixup --- .../main/scala/caseapp/core/app/PlatformCommandsMethods.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala index 6a2912bc..a49662c5 100644 --- a/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala +++ b/core/jvm/src/main/scala/caseapp/core/app/PlatformCommandsMethods.scala @@ -22,6 +22,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => // Adapted from https://github.com/VirtusLab/scala-cli/blob/eced0b35c769eca58ae6f1b1a3be0f29a8700859/modules/cli/src/main/scala/scala/cli/commands/installcompletions/InstallCompletions.scala def completionsInstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsInstallOptions](args) + lazy val completionsDir = Paths.get(options.output.getOrElse(completionsWorkingDirectory)) val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) @@ -128,6 +129,7 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint => def completionsUninstall(completionsWorkingDirectory: String, args: Seq[String]): Unit = { val (options, rem) = CaseApp.process[PlatformCommandsMethods.CompletionsUninstallOptions](args) + val name = options.name.getOrElse(Paths.get(progName).getFileName.toString) val home = Paths.get(sys.props("user.home")) From bb067312f05ff9d0f3548200f8fe46b943a45b42 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Tue, 4 Jun 2024 16:17:31 +0200 Subject: [PATCH 7/7] Fix compilation --- core/shared/src/main/scala/caseapp/core/complete/Fish.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/caseapp/core/complete/Fish.scala b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala index 6652ab60..36f970ce 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Fish.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Fish.scala @@ -13,7 +13,7 @@ object Fish { |""".stripMargin private def escape(s: String): String = - s.replace("\t", " ").linesIterator.to(LazyList).headOption.getOrElse("") + s.replace("\t", " ").linesIterator.find(_ => true).getOrElse("") def print(items: Seq[CompletionItem]): String = { val newLine = System.lineSeparator() val b = new StringBuilder