Skip to content

Commit

Permalink
Merge pull request #458 from alexarchambault/better-completer-api
Browse files Browse the repository at this point in the history
Better Completer API
  • Loading branch information
alexarchambault authored Feb 9, 2023
2 parents 65eb6bd + 6d7865b commit 2e8c201
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 64 deletions.
36 changes: 26 additions & 10 deletions core/shared/src/main/scala/caseapp/core/complete/Completer.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
package caseapp.core.complete

import caseapp.core.Arg
import caseapp.core.help.{WithFullHelp, WithHelp}
import caseapp.core.{Arg, RemainingArgs}

trait Completer[-T] { self =>
def optionName(prefix: String, state: Option[T]): List[CompletionItem]
def optionValue(arg: Arg, prefix: String, state: Option[T]): List[CompletionItem]
def argument(prefix: String, state: Option[T]): List[CompletionItem]
def optionName(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem]
def optionValue(
arg: Arg,
prefix: String,
state: Option[T],
args: RemainingArgs
): List[CompletionItem]
def argument(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem]

def postDoubleDash(state: Option[T], args: RemainingArgs): Option[Completer[T]] =
None

def contramapOpt[U](f: U => Option[T]): Completer[U] =
new Completer[U] {
def optionName(prefix: String, state: Option[U]): List[CompletionItem] =
self.optionName(prefix, state.flatMap(f))
def optionValue(arg: Arg, prefix: String, state: Option[U]): List[CompletionItem] =
self.optionValue(arg, prefix, state.flatMap(f))
def argument(prefix: String, state: Option[U]): List[CompletionItem] =
self.argument(prefix, state.flatMap(f))
def optionName(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] =
self.optionName(prefix, state.flatMap(f), args)
def optionValue(
arg: Arg,
prefix: String,
state: Option[U],
args: RemainingArgs
): List[CompletionItem] =
self.optionValue(arg, prefix, state.flatMap(f), args)
def argument(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] =
self.argument(prefix, state.flatMap(f), args)

override def postDoubleDash(state: Option[U], args: RemainingArgs): Option[Completer[U]] =
self.postDoubleDash(state.flatMap(f), args).map(_.contramapOpt(f))
}
def withHelp: Completer[WithHelp[T]] =
contramapOpt(_.baseOrError.toOption)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package caseapp.core.complete

import caseapp.core.Arg
import caseapp.core.help.Help
import caseapp.core.{Arg, RemainingArgs}

class HelpCompleter[T](help: Help[T]) extends Completer[T] {
def optionName(prefix: String, state: Option[T]): List[CompletionItem] =
def optionName(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] =
help
.args
.iterator
Expand All @@ -18,8 +18,13 @@ class HelpCompleter[T](help: Help[T]) extends Completer[T] {
Iterator(CompletionItem(names.head, arg.helpMessage.map(_.message), names.tail))
}
.toList
def optionValue(arg: Arg, prefix: String, state: Option[T]): List[CompletionItem] =
def optionValue(
arg: Arg,
prefix: String,
state: Option[T],
args: RemainingArgs
): List[CompletionItem] =
Nil
def argument(prefix: String, state: Option[T]): List[CompletionItem] =
def argument(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] =
Nil
}
100 changes: 58 additions & 42 deletions core/shared/src/main/scala/caseapp/core/parser/ParserMethods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,25 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
stopAtFirstUnrecognized: Boolean,
ignoreUnrecognized: Boolean
): Either[Error, (T, RemainingArgs)] = {
val (res, _) = scan(args, stopAtFirstUnrecognized, ignoreUnrecognized)
res.left.map(_._1)
val (res, remArgs, _) = scan(args, stopAtFirstUnrecognized, ignoreUnrecognized)
res
.left.map(_._1)
.map((_, remArgs))
}

final def scan(
args: Seq[String],
stopAtFirstUnrecognized: Boolean,
ignoreUnrecognized: Boolean
): (Either[(Error, Either[D, T]), (T, RemainingArgs)], List[Step]) = {
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) = {

def runHelper(
current: D,
args: List[String],
extraArgsReverse: List[Indexed[String]],
reverseSteps: List[Step],
index: Int
): (Either[(Error, Either[D, T]), (T, RemainingArgs)], List[Step]) =
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) =
helper(current, args, extraArgsReverse, reverseSteps, index)

@tailrec
Expand All @@ -151,41 +153,40 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
extraArgsReverse: List[Indexed[String]],
reverseSteps: List[Step],
index: Int
): (Either[(Error, Either[D, T]), (T, RemainingArgs)], List[Step]) = {
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) = {

def done = {
val remArgs = RemainingArgs(extraArgsReverse.reverse, Nil)
val res = get(current)
.left.map((_, Left(current)))
.map((_, RemainingArgs(extraArgsReverse.reverse, Nil)))
(res, reverseSteps.reverse)
(res, remArgs, reverseSteps.reverse)
}

def stopParsing(tailArgs: List[String]) = {
val remArgs =
if (stopAtFirstUnrecognized)
// extraArgsReverse should be empty anyway here
RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
else
RemainingArgs(extraArgsReverse.reverse, Indexed.seq(tailArgs, index + 1))
val res = get(current)
.left.map((_, Left(current)))
.map { t =>
if (stopAtFirstUnrecognized)
// extraArgsReverse should be empty anyway here
(t, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil))
else
(t, RemainingArgs(extraArgsReverse.reverse, Indexed.seq(tailArgs, index + 1)))
}
val reverseSteps0 = Step.DoubleDash(index) :: reverseSteps.reverse
(res, reverseSteps0.reverse)
(res, remArgs, reverseSteps0.reverse)
}

def unrecognized(headArg: String, tailArgs: List[String]) =
if (stopAtFirstUnrecognized) {
// extraArgsReverse should be empty anyway here
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
val res = get(current)
.left.map((_, Left(current)))
// extraArgsReverse should be empty anyway here
.map((_, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)))
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = true) :: reverseSteps
(res, reverseSteps0.reverse)
(res, remArgs, reverseSteps0.reverse)
}
else {
val err = Error.UnrecognizedArgument(headArg)
val (remaining, steps) = runHelper(
val (remaining, remArgs, steps) = runHelper(
current,
tailArgs,
extraArgsReverse,
Expand All @@ -194,18 +195,18 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
)
val res = Left((
remaining.fold(t => err.append(t._1), _ => err),
remaining.fold(_._2, t => Right(t._1))
remaining.fold(_._2, Right(_))
))
(res, steps)
(res, remArgs, steps)
}

def stoppingAtUnrecognized = {
// extraArgsReverse should be empty anyway here
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
val res = get(current)
.left.map((_, Left(current)))
// extraArgsReverse should be empty anyway here
.map((_, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)))
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = false) :: reverseSteps
(res, reverseSteps0.reverse)
(res, remArgs, reverseSteps0.reverse)
}

args match {
Expand Down Expand Up @@ -257,7 +258,7 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
case Left((msg, matchedArg, rem)) =>
val consumed0 = Parser.consumed(args, rem)
assert(consumed0 > 0)
val (remaining, steps) = runHelper(
val (remaining, remArgs, steps) = runHelper(
current,
rem,
extraArgsReverse,
Expand All @@ -266,9 +267,9 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
)
val res = Left((
remaining.fold(errs => msg.append(errs._1), _ => msg),
remaining.fold(_._2, t => Right(t._1))
remaining.fold(_._2, Right(_))
))
(res, steps)
(res, remArgs, steps)
}
}
}
Expand All @@ -286,11 +287,11 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>

val args0 = if (index < args.length) args else args ++ Seq.fill(index + 1 - args.length)("")

val (res, steps) = scan(args0, stopAtFirstUnrecognized, ignoreUnrecognized)
val (res, remArgs, steps) = scan(args0, stopAtFirstUnrecognized, ignoreUnrecognized)
lazy val stateOpt = res match {
case Left((_, Left(state))) => get(state).toOption
case Left((_, Right(t))) => Some(t)
case Right((t, _)) => Some(t)
case Right(t) => Some(t)
}

assert(index >= 0)
Expand All @@ -305,38 +306,53 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
val value = args0(index)

stepOpt match {
case None => Nil
case None =>
val isAfterDoubleDash = steps.lastOption.exists {
case Step.DoubleDash(ddIdx) => ddIdx < index
case _ => false
}
if (isAfterDoubleDash)
completer.postDoubleDash(stateOpt, remArgs)
.map { completer =>
if (value.startsWith("-"))
completer.optionName(value, stateOpt, remArgs)
else
completer.argument(value, stateOpt, remArgs)
}
.getOrElse(Nil)
else
Nil
case Some(step) =>
val shift = index - step.index
step match {
case Step.DoubleDash(_) =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.ErroredOption(_, _, _, _) if shift == 0 =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.ErroredOption(_, consumed, arg, _) if consumed == 2 && shift == 1 =>
completer.optionValue(arg, value, stateOpt)
completer.optionValue(arg, value, stateOpt, remArgs)
case Step.ErroredOption(_, _, _, _) =>
// should not happen
Nil
case Step.FirstUnrecognized(_, true) =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.FirstUnrecognized(_, false) =>
completer.argument(value, stateOpt)
completer.argument(value, stateOpt, remArgs)
case Step.IgnoredUnrecognized(_) =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.Unrecognized(_, _) =>
completer.optionName(value, stateOpt)
case Step.StandardArgument(idx) if args0(idx) == "-" =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.StandardArgument(idx) if value == "-" =>
completer.optionName(value, stateOpt, remArgs)
case Step.MatchedOption(_, consumed, arg) if shift == 0 =>
completer.optionName(value, stateOpt)
completer.optionName(value, stateOpt, remArgs)
case Step.MatchedOption(_, consumed, arg) if consumed == 2 && shift == 1 =>
completer.optionValue(arg, value, stateOpt)
completer.optionValue(arg, value, stateOpt, remArgs)
case Step.MatchedOption(_, _, _) =>
// should not happen
Nil
case Step.StandardArgument(_) =>
completer.argument(value, stateOpt)
completer.argument(value, stateOpt, remArgs)
}
}
}
Expand Down
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", "1acab44cf68aeebb575bd1c920f96397519a18d0")
Seq("git", "tag", "--merged", "HEAD^", "--contains", "c199a3037771d09af0a190a2b99fa8b287e6812f")
.!!
.linesIterator
.map(_.trim)
Expand Down
14 changes: 7 additions & 7 deletions tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ object CompletionDefinitions {
override def completer: Completer[Options] = {
val parent = super.completer
new Completer[Options] {
def optionName(prefix: String, state: Option[Options]) =
parent.optionName(prefix, state)
def optionValue(arg: Arg, prefix: String, state: Option[Options]) =
def optionName(prefix: String, state: Option[Options], args: RemainingArgs) =
parent.optionName(prefix, state, args)
def optionValue(arg: Arg, prefix: String, state: Option[Options], args: RemainingArgs) =
if (arg.name.name == "value")
state match {
case None => parent.optionValue(arg, prefix, state)
case None => parent.optionValue(arg, prefix, state, args)
case Some(state0) =>
(0 to 2)
.map(_ + state0.other * 1000)
.map(n => CompletionItem(n.toString))
.toList
}
else
parent.optionValue(arg, prefix, state)
def argument(prefix: String, state: Option[Options]) =
parent.argument(prefix, state)
parent.optionValue(arg, prefix, state, args)
def argument(prefix: String, state: Option[Options], args: RemainingArgs) =
parent.argument(prefix, state, args)
}
}
def run(options: Options, args: RemainingArgs): Unit = ???
Expand Down

0 comments on commit 2e8c201

Please sign in to comment.