Skip to content

Commit 2e8c201

Browse files
Merge pull request #458 from alexarchambault/better-completer-api
Better Completer API
2 parents 65eb6bd + 6d7865b commit 2e8c201

File tree

5 files changed

+101
-64
lines changed

5 files changed

+101
-64
lines changed

core/shared/src/main/scala/caseapp/core/complete/Completer.scala

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
package caseapp.core.complete
22

3-
import caseapp.core.Arg
43
import caseapp.core.help.{WithFullHelp, WithHelp}
4+
import caseapp.core.{Arg, RemainingArgs}
55

66
trait Completer[-T] { self =>
7-
def optionName(prefix: String, state: Option[T]): List[CompletionItem]
8-
def optionValue(arg: Arg, prefix: String, state: Option[T]): List[CompletionItem]
9-
def argument(prefix: String, state: Option[T]): List[CompletionItem]
7+
def optionName(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem]
8+
def optionValue(
9+
arg: Arg,
10+
prefix: String,
11+
state: Option[T],
12+
args: RemainingArgs
13+
): List[CompletionItem]
14+
def argument(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem]
15+
16+
def postDoubleDash(state: Option[T], args: RemainingArgs): Option[Completer[T]] =
17+
None
1018

1119
def contramapOpt[U](f: U => Option[T]): Completer[U] =
1220
new Completer[U] {
13-
def optionName(prefix: String, state: Option[U]): List[CompletionItem] =
14-
self.optionName(prefix, state.flatMap(f))
15-
def optionValue(arg: Arg, prefix: String, state: Option[U]): List[CompletionItem] =
16-
self.optionValue(arg, prefix, state.flatMap(f))
17-
def argument(prefix: String, state: Option[U]): List[CompletionItem] =
18-
self.argument(prefix, state.flatMap(f))
21+
def optionName(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] =
22+
self.optionName(prefix, state.flatMap(f), args)
23+
def optionValue(
24+
arg: Arg,
25+
prefix: String,
26+
state: Option[U],
27+
args: RemainingArgs
28+
): List[CompletionItem] =
29+
self.optionValue(arg, prefix, state.flatMap(f), args)
30+
def argument(prefix: String, state: Option[U], args: RemainingArgs): List[CompletionItem] =
31+
self.argument(prefix, state.flatMap(f), args)
32+
33+
override def postDoubleDash(state: Option[U], args: RemainingArgs): Option[Completer[U]] =
34+
self.postDoubleDash(state.flatMap(f), args).map(_.contramapOpt(f))
1935
}
2036
def withHelp: Completer[WithHelp[T]] =
2137
contramapOpt(_.baseOrError.toOption)

core/shared/src/main/scala/caseapp/core/complete/HelpCompleter.scala

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package caseapp.core.complete
22

3-
import caseapp.core.Arg
43
import caseapp.core.help.Help
4+
import caseapp.core.{Arg, RemainingArgs}
55

66
class HelpCompleter[T](help: Help[T]) extends Completer[T] {
7-
def optionName(prefix: String, state: Option[T]): List[CompletionItem] =
7+
def optionName(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] =
88
help
99
.args
1010
.iterator
@@ -18,8 +18,13 @@ class HelpCompleter[T](help: Help[T]) extends Completer[T] {
1818
Iterator(CompletionItem(names.head, arg.helpMessage.map(_.message), names.tail))
1919
}
2020
.toList
21-
def optionValue(arg: Arg, prefix: String, state: Option[T]): List[CompletionItem] =
21+
def optionValue(
22+
arg: Arg,
23+
prefix: String,
24+
state: Option[T],
25+
args: RemainingArgs
26+
): List[CompletionItem] =
2227
Nil
23-
def argument(prefix: String, state: Option[T]): List[CompletionItem] =
28+
def argument(prefix: String, state: Option[T], args: RemainingArgs): List[CompletionItem] =
2429
Nil
2530
}

core/shared/src/main/scala/caseapp/core/parser/ParserMethods.scala

+58-42
Original file line numberDiff line numberDiff line change
@@ -125,23 +125,25 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
125125
stopAtFirstUnrecognized: Boolean,
126126
ignoreUnrecognized: Boolean
127127
): Either[Error, (T, RemainingArgs)] = {
128-
val (res, _) = scan(args, stopAtFirstUnrecognized, ignoreUnrecognized)
129-
res.left.map(_._1)
128+
val (res, remArgs, _) = scan(args, stopAtFirstUnrecognized, ignoreUnrecognized)
129+
res
130+
.left.map(_._1)
131+
.map((_, remArgs))
130132
}
131133

132134
final def scan(
133135
args: Seq[String],
134136
stopAtFirstUnrecognized: Boolean,
135137
ignoreUnrecognized: Boolean
136-
): (Either[(Error, Either[D, T]), (T, RemainingArgs)], List[Step]) = {
138+
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) = {
137139

138140
def runHelper(
139141
current: D,
140142
args: List[String],
141143
extraArgsReverse: List[Indexed[String]],
142144
reverseSteps: List[Step],
143145
index: Int
144-
): (Either[(Error, Either[D, T]), (T, RemainingArgs)], List[Step]) =
146+
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) =
145147
helper(current, args, extraArgsReverse, reverseSteps, index)
146148

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

156158
def done = {
159+
val remArgs = RemainingArgs(extraArgsReverse.reverse, Nil)
157160
val res = get(current)
158161
.left.map((_, Left(current)))
159-
.map((_, RemainingArgs(extraArgsReverse.reverse, Nil)))
160-
(res, reverseSteps.reverse)
162+
(res, remArgs, reverseSteps.reverse)
161163
}
162164

163165
def stopParsing(tailArgs: List[String]) = {
166+
val remArgs =
167+
if (stopAtFirstUnrecognized)
168+
// extraArgsReverse should be empty anyway here
169+
RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
170+
else
171+
RemainingArgs(extraArgsReverse.reverse, Indexed.seq(tailArgs, index + 1))
164172
val res = get(current)
165173
.left.map((_, Left(current)))
166-
.map { t =>
167-
if (stopAtFirstUnrecognized)
168-
// extraArgsReverse should be empty anyway here
169-
(t, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil))
170-
else
171-
(t, RemainingArgs(extraArgsReverse.reverse, Indexed.seq(tailArgs, index + 1)))
172-
}
173174
val reverseSteps0 = Step.DoubleDash(index) :: reverseSteps.reverse
174-
(res, reverseSteps0.reverse)
175+
(res, remArgs, reverseSteps0.reverse)
175176
}
176177

177178
def unrecognized(headArg: String, tailArgs: List[String]) =
178179
if (stopAtFirstUnrecognized) {
180+
// extraArgsReverse should be empty anyway here
181+
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
179182
val res = get(current)
180183
.left.map((_, Left(current)))
181-
// extraArgsReverse should be empty anyway here
182-
.map((_, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)))
183184
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = true) :: reverseSteps
184-
(res, reverseSteps0.reverse)
185+
(res, remArgs, reverseSteps0.reverse)
185186
}
186187
else {
187188
val err = Error.UnrecognizedArgument(headArg)
188-
val (remaining, steps) = runHelper(
189+
val (remaining, remArgs, steps) = runHelper(
189190
current,
190191
tailArgs,
191192
extraArgsReverse,
@@ -194,18 +195,18 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
194195
)
195196
val res = Left((
196197
remaining.fold(t => err.append(t._1), _ => err),
197-
remaining.fold(_._2, t => Right(t._1))
198+
remaining.fold(_._2, Right(_))
198199
))
199-
(res, steps)
200+
(res, remArgs, steps)
200201
}
201202

202203
def stoppingAtUnrecognized = {
204+
// extraArgsReverse should be empty anyway here
205+
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
203206
val res = get(current)
204207
.left.map((_, Left(current)))
205-
// extraArgsReverse should be empty anyway here
206-
.map((_, RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)))
207208
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = false) :: reverseSteps
208-
(res, reverseSteps0.reverse)
209+
(res, remArgs, reverseSteps0.reverse)
209210
}
210211

211212
args match {
@@ -257,7 +258,7 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
257258
case Left((msg, matchedArg, rem)) =>
258259
val consumed0 = Parser.consumed(args, rem)
259260
assert(consumed0 > 0)
260-
val (remaining, steps) = runHelper(
261+
val (remaining, remArgs, steps) = runHelper(
261262
current,
262263
rem,
263264
extraArgsReverse,
@@ -266,9 +267,9 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
266267
)
267268
val res = Left((
268269
remaining.fold(errs => msg.append(errs._1), _ => msg),
269-
remaining.fold(_._2, t => Right(t._1))
270+
remaining.fold(_._2, Right(_))
270271
))
271-
(res, steps)
272+
(res, remArgs, steps)
272273
}
273274
}
274275
}
@@ -286,11 +287,11 @@ trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
286287

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

289-
val (res, steps) = scan(args0, stopAtFirstUnrecognized, ignoreUnrecognized)
290+
val (res, remArgs, steps) = scan(args0, stopAtFirstUnrecognized, ignoreUnrecognized)
290291
lazy val stateOpt = res match {
291292
case Left((_, Left(state))) => get(state).toOption
292293
case Left((_, Right(t))) => Some(t)
293-
case Right((t, _)) => Some(t)
294+
case Right(t) => Some(t)
294295
}
295296

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

307308
stepOpt match {
308-
case None => Nil
309+
case None =>
310+
val isAfterDoubleDash = steps.lastOption.exists {
311+
case Step.DoubleDash(ddIdx) => ddIdx < index
312+
case _ => false
313+
}
314+
if (isAfterDoubleDash)
315+
completer.postDoubleDash(stateOpt, remArgs)
316+
.map { completer =>
317+
if (value.startsWith("-"))
318+
completer.optionName(value, stateOpt, remArgs)
319+
else
320+
completer.argument(value, stateOpt, remArgs)
321+
}
322+
.getOrElse(Nil)
323+
else
324+
Nil
309325
case Some(step) =>
310326
val shift = index - step.index
311327
step match {
312328
case Step.DoubleDash(_) =>
313-
completer.optionName(value, stateOpt)
329+
completer.optionName(value, stateOpt, remArgs)
314330
case Step.ErroredOption(_, _, _, _) if shift == 0 =>
315-
completer.optionName(value, stateOpt)
331+
completer.optionName(value, stateOpt, remArgs)
316332
case Step.ErroredOption(_, consumed, arg, _) if consumed == 2 && shift == 1 =>
317-
completer.optionValue(arg, value, stateOpt)
333+
completer.optionValue(arg, value, stateOpt, remArgs)
318334
case Step.ErroredOption(_, _, _, _) =>
319335
// should not happen
320336
Nil
321337
case Step.FirstUnrecognized(_, true) =>
322-
completer.optionName(value, stateOpt)
338+
completer.optionName(value, stateOpt, remArgs)
323339
case Step.FirstUnrecognized(_, false) =>
324-
completer.argument(value, stateOpt)
340+
completer.argument(value, stateOpt, remArgs)
325341
case Step.IgnoredUnrecognized(_) =>
326-
completer.optionName(value, stateOpt)
342+
completer.optionName(value, stateOpt, remArgs)
327343
case Step.Unrecognized(_, _) =>
328-
completer.optionName(value, stateOpt)
329-
case Step.StandardArgument(idx) if args0(idx) == "-" =>
330-
completer.optionName(value, stateOpt)
344+
completer.optionName(value, stateOpt, remArgs)
345+
case Step.StandardArgument(idx) if value == "-" =>
346+
completer.optionName(value, stateOpt, remArgs)
331347
case Step.MatchedOption(_, consumed, arg) if shift == 0 =>
332-
completer.optionName(value, stateOpt)
348+
completer.optionName(value, stateOpt, remArgs)
333349
case Step.MatchedOption(_, consumed, arg) if consumed == 2 && shift == 1 =>
334-
completer.optionValue(arg, value, stateOpt)
350+
completer.optionValue(arg, value, stateOpt, remArgs)
335351
case Step.MatchedOption(_, _, _) =>
336352
// should not happen
337353
Nil
338354
case Step.StandardArgument(_) =>
339-
completer.argument(value, stateOpt)
355+
completer.argument(value, stateOpt, remArgs)
340356
}
341357
}
342358
}

project/Mima.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import scala.sys.process._
88
object Mima {
99

1010
def binaryCompatibilityVersions: Set[String] =
11-
Seq("git", "tag", "--merged", "HEAD^", "--contains", "1acab44cf68aeebb575bd1c920f96397519a18d0")
11+
Seq("git", "tag", "--merged", "HEAD^", "--contains", "c199a3037771d09af0a190a2b99fa8b287e6812f")
1212
.!!
1313
.linesIterator
1414
.map(_.trim)

tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ object CompletionDefinitions {
2828
override def completer: Completer[Options] = {
2929
val parent = super.completer
3030
new Completer[Options] {
31-
def optionName(prefix: String, state: Option[Options]) =
32-
parent.optionName(prefix, state)
33-
def optionValue(arg: Arg, prefix: String, state: Option[Options]) =
31+
def optionName(prefix: String, state: Option[Options], args: RemainingArgs) =
32+
parent.optionName(prefix, state, args)
33+
def optionValue(arg: Arg, prefix: String, state: Option[Options], args: RemainingArgs) =
3434
if (arg.name.name == "value")
3535
state match {
36-
case None => parent.optionValue(arg, prefix, state)
36+
case None => parent.optionValue(arg, prefix, state, args)
3737
case Some(state0) =>
3838
(0 to 2)
3939
.map(_ + state0.other * 1000)
4040
.map(n => CompletionItem(n.toString))
4141
.toList
4242
}
4343
else
44-
parent.optionValue(arg, prefix, state)
45-
def argument(prefix: String, state: Option[Options]) =
46-
parent.argument(prefix, state)
44+
parent.optionValue(arg, prefix, state, args)
45+
def argument(prefix: String, state: Option[Options], args: RemainingArgs) =
46+
parent.argument(prefix, state, args)
4747
}
4848
}
4949
def run(options: Options, args: RemainingArgs): Unit = ???

0 commit comments

Comments
 (0)