Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fish shell completions support #538

Merged
merged 8 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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