Skip to content

Commit

Permalink
Add fish shell completions support (#538)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexandre Archambault <[email protected]>
  • Loading branch information
natsukagami and alexarchambault authored Jun 5, 2024
1 parent 993ac30 commit d753d92
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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}
Expand Down Expand Up @@ -32,14 +32,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 ${completionsCommandName.mkString(" ")} install --shell $shell",
toStderr = true
)
printLine("", toStderr = true)
exit(1)
}
Expand All @@ -49,6 +46,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")))
Expand All @@ -69,14 +75,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,
Expand Down Expand Up @@ -123,9 +134,14 @@ trait PlatformCommandsMethods { self: CommandsEntryPoint =>

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) {
Expand Down Expand Up @@ -159,15 +175,15 @@ object PlatformCommandsMethods {
env: Boolean = false,
@HelpMessage("Custom completions name")
name: Option[String] = None,
@HelpMessage("Name of the shell, either zsh or bash")
@HelpMessage("Name of the shell, either zsh, fish or bash")
@Name("shell")
format: Option[String] = None,
@HelpMessage("Completions output directory")
@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")
@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")
@HelpMessage("Path to `*rc` file, defaults to `.bashrc` or `.zshrc` depending on shell (bash or zsh only)")
rcFile: Option[String] = None
)
// format: on
Expand All @@ -180,12 +196,15 @@ 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("Custom completions name")
name: Option[String] = None
name: Option[String] = None,
@HelpMessage("Completions output directory (defaults to $XDG_CONFIG_HOME/fish/completions on fish)")
@Name("o")
output: Option[String] = None,
)
// format: on

Expand All @@ -199,6 +218,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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 _ =>
Expand Down
30 changes: 30 additions & 0 deletions core/shared/src/main/scala/caseapp/core/complete/Fish.scala
Original file line number Diff line number Diff line change
@@ -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.find(_ => true).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()
}
}
2 changes: 1 addition & 1 deletion project/Mima.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion tests/shared/src/test/scala/caseapp/CompletionTests.scala
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit d753d92

Please sign in to comment.