Skip to content

Commit 3666228

Browse files
committed
Add completion candidates
1 parent 4cf0d61 commit 3666228

File tree

12 files changed

+175
-74
lines changed

12 files changed

+175
-74
lines changed

docs/modules/pkl-cli/pages/index.adoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,9 +1157,16 @@ Like with Pkl's own CLI, <<command-shell-completion, shell completions>> can be
11571157
./my-tool.pkl shell-completion zsh
11581158
----
11591159

1160-
==== Defining Completion Candidates
1160+
==== Customizing Completion Candidates
11611161

1162-
TODO
1162+
`@Flag` and `@Argument` annotations may specify the `completionCandidates` to improve generated shell completions.
1163+
1164+
Valid values include:
1165+
1166+
* A `Listing<String>` of literal string values to offer for completion.
1167+
* The literal string `"path"`, which offers local file paths for completion.
1168+
1169+
Options with a string literal union type implicitly offer the members of the union as completion candidates.
11631170

11641171
=== Flag name ambiguities
11651172

pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.pkl.cli
1717

18+
import com.github.ajalt.clikt.completion.CompletionCandidates
1819
import com.github.ajalt.clikt.completion.CompletionCommand
1920
import com.github.ajalt.clikt.core.*
2021
import com.github.ajalt.clikt.parameters.arguments.*
@@ -36,6 +37,7 @@ import org.pkl.core.CommandSpec
3637
import org.pkl.core.EvaluatorBuilder
3738
import org.pkl.core.FileOutput
3839
import org.pkl.core.ModuleSource.uri
40+
import org.pkl.core.PklBugException
3941
import org.pkl.core.PklException
4042
import org.pkl.core.util.IoUtils
4143

@@ -153,6 +155,7 @@ constructor(
153155
help = opt.helpText ?: "",
154156
metavar = opt.metavar,
155157
hidden = opt.hidden,
158+
completionCandidates = opt.completionCandidates?.toClikt(),
156159
)
157160
.convert {
158161
try {
@@ -251,3 +254,10 @@ constructor(
251254
}
252255
}
253256
}
257+
258+
fun CommandSpec.CompletionCandidates.toClikt(): CompletionCandidates =
259+
when (this) {
260+
CommandSpec.CompletionCandidates.PATH -> CompletionCandidates.Path
261+
is CommandSpec.CompletionCandidates.Fixed -> CompletionCandidates.Fixed(values)
262+
else -> throw PklBugException.unreachableCode()
263+
}

pkl-cli/src/test/kotlin/org/pkl/cli/CliCommandRunnerTest.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ package org.pkl.cli
1717

1818
import com.github.ajalt.clikt.core.CliktError
1919
import com.github.ajalt.clikt.core.MissingOption
20-
import com.github.ajalt.clikt.core.PrintHelpMessage
20+
import com.github.ajalt.clikt.core.PrintCompletionMessage
2121
import com.github.tomakehurst.wiremock.junit5.WireMockTest
2222
import java.io.ByteArrayOutputStream
2323
import java.net.URI
@@ -1093,4 +1093,46 @@ class CliCommandRunnerTest {
10931093
.trimIndent()
10941094
)
10951095
}
1096+
1097+
@Test
1098+
fun `completion candidates`() {
1099+
val moduleUri =
1100+
writePklFile(
1101+
"cmd.pkl",
1102+
renderOptions +
1103+
"""
1104+
class Options {
1105+
none: String?
1106+
enum: *"a" | "b" | "c"
1107+
@Flag { completionCandidates = "path" }
1108+
path: String?
1109+
@Flag { completionCandidates { "foo"; "bar"; "baz" } }
1110+
explicit: String?
1111+
}
1112+
"""
1113+
.trimIndent(),
1114+
)
1115+
val exc =
1116+
assertThrows<PrintCompletionMessage> {
1117+
runToStdout(
1118+
CliBaseOptions(sourceModules = listOf(moduleUri)),
1119+
listOf("shell-completion", "bash"),
1120+
)
1121+
}
1122+
assertThat(exc.message)
1123+
.contains(
1124+
"""
1125+
"--none")
1126+
;;
1127+
"--enum")
1128+
COMPREPLY=($(compgen -W 'a b c' -- "${'$'}{word}"))
1129+
;;
1130+
"--path")
1131+
__complete_files "${'$'}{word}"
1132+
;;
1133+
"--explicit")
1134+
COMPREPLY=($(compgen -W 'bar baz foo' -- "${'$'}{word}"))
1135+
;;"""
1136+
)
1137+
}
10961138
}

pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/extensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

pkl-core/src/main/java/org/pkl/core/CommandSpec.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.net.URI;
1919
import java.util.List;
2020
import java.util.Map;
21+
import java.util.Set;
2122
import java.util.function.BiFunction;
2223
import java.util.function.Function;
2324
import org.pkl.core.util.Nullable;
@@ -65,12 +66,31 @@ public static BadValue invalidChoice(String value, String choice) {
6566
}
6667
}
6768

69+
public abstract static sealed class CompletionCandidates {
70+
public static final CompletionCandidates PATH = new StaticCompletionCandidates();
71+
72+
public static final class Fixed extends CompletionCandidates {
73+
private final Set<String> values;
74+
75+
public Fixed(Set<String> values) {
76+
this.values = values;
77+
}
78+
79+
public Set<String> getValues() {
80+
return values;
81+
}
82+
}
83+
84+
private static final class StaticCompletionCandidates extends CompletionCandidates {}
85+
}
86+
6887
public record Flag(
6988
String name,
7089
@Nullable String helpText,
7190
boolean showAsRequired,
7291
BiFunction<String, URI, Object> transformEach,
7392
Function<List<Object>, Object> transformAll,
93+
@Nullable CompletionCandidates completionCandidates,
7494
@Nullable String shortName,
7595
String metavar,
7696
boolean hidden,
@@ -115,6 +135,7 @@ public record Argument(
115135
@Nullable String helpText,
116136
BiFunction<String, URI, Object> transformEach,
117137
Function<List<Object>, Object> transformAll,
138+
@Nullable CompletionCandidates completionCandidates,
118139
boolean repeated)
119140
implements Option {
120141
@Override

pkl-core/src/main/java/org/pkl/core/PClassInfo.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public final class PClassInfo<T> implements Serializable {
3939
public static final URI pklSemverUri = URI.create("pkl:semver");
4040
public static final URI pklSettingsUri = URI.create("pkl:settings");
4141
public static final URI pklProjectUri = URI.create("pkl:Project");
42-
public static final URI pklCommandUri = URI.create("pkl:Command");
4342

4443
public static final PClassInfo<Void> Any = pklBaseClassInfo("Any", Void.class);
4544
public static final PClassInfo<PNull> Null = pklBaseClassInfo("Null", PNull.class);
@@ -86,8 +85,6 @@ public final class PClassInfo<T> implements Serializable {
8685
new PClassInfo<>("pkl.Project", "ModuleClass", PObject.class, pklProjectUri);
8786
public static final PClassInfo<PObject> Settings =
8887
new PClassInfo<>("pkl.settings", "ModuleClass", PObject.class, pklSettingsUri);
89-
public static final PClassInfo<PObject> Command =
90-
new PClassInfo<>("pkl.Command", "ModuleClass", PObject.class, pklCommandUri);
9188

9289
public static final PClassInfo<Object> Unavailable =
9390
new PClassInfo<>("unavailable", "unavailable", Object.class, URI.create("pkl:unavailable"));

pkl-core/src/main/java/org/pkl/core/ast/member/ClassMember.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,31 @@ public final List<VmTyped> getAnnotations() {
5454
return annotations;
5555
}
5656

57-
/**
58-
* Return all annotations starting from the declaring class and traversing up the class hierarchy
59-
*/
60-
public final List<VmTyped> getAllAnnotations() {
61-
assert name != null;
62-
var allAnnotations = new ArrayList<>(annotations);
63-
var clazz = getDeclaringClass().getSuperclass();
64-
while (clazz != null) {
65-
var prop = clazz.getProperty(name);
66-
if (prop != null) {
67-
allAnnotations.addAll(prop.getAnnotations());
57+
public List<VmTyped> getAllAnnotations(boolean ascending) {
58+
var annotations = new ArrayList<VmTyped>();
59+
60+
if (ascending) {
61+
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
62+
var p = clazz.getDeclaredProperty(getName());
63+
if (p != null) {
64+
annotations.addAll(p.getAnnotations());
65+
}
6866
}
69-
clazz = clazz.getSuperclass();
67+
} else {
68+
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
69+
}
70+
71+
return annotations;
72+
}
73+
74+
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
75+
if (clazz.getSuperclass() != null) {
76+
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
77+
}
78+
var p = clazz.getDeclaredProperty(getName());
79+
if (p != null) {
80+
annotations.addAll(p.getAnnotations());
7081
}
71-
return allAnnotations;
7282
}
7383

7484
/** Returns the prototype of the class that declares this member. */

pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,33 +54,6 @@ public ClassProperty(
5454
this.initializer = initializer;
5555
}
5656

57-
public List<VmTyped> getAllAnnotations(boolean ascending) {
58-
var annotations = new ArrayList<VmTyped>();
59-
60-
if (ascending) {
61-
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
62-
var p = clazz.getDeclaredProperty(getName());
63-
if (p != null) {
64-
annotations.addAll(p.getAnnotations());
65-
}
66-
}
67-
} else {
68-
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
69-
}
70-
71-
return annotations;
72-
}
73-
74-
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
75-
if (clazz.getSuperclass() != null) {
76-
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
77-
}
78-
var p = clazz.getDeclaredProperty(getName());
79-
if (p != null) {
80-
annotations.addAll(p.getAnnotations());
81-
}
82-
}
83-
8457
public VmSet getAllModifierMirrors() {
8558
var mods = 0;
8659
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {

pkl-core/src/main/java/org/pkl/core/runtime/CommandModule.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
*/
1616
package org.pkl.core.runtime;
1717

18-
import static org.pkl.core.PClassInfo.pklCommandUri;
19-
2018
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
19+
import java.net.URI;
2120

2221
public final class CommandModule extends StdLibModule {
2322
static final VmTyped instance = VmUtils.createEmptyModule();
2423

2524
static {
26-
loadModule(pklCommandUri, instance);
25+
loadModule(URI.create("pkl:Command"), instance);
2726
}
2827

2928
private CommandModule() {}

0 commit comments

Comments
 (0)