This commit is contained in:
Stephan Schroevers
2023-04-21 22:11:22 +02:00
parent 943a409ec1
commit 89e49b118e
3 changed files with 119 additions and 452 deletions

View File

@@ -1,22 +1,18 @@
package tech.picnic.errorprone.openai;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSet;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.OptionalInt;
import org.fusesource.jansi.AnsiConsole;
import org.jline.builtins.ConfigurationPath;
import org.jline.console.SystemRegistry;
import org.jline.console.impl.Builtins;
import org.jline.console.impl.SystemRegistryImpl;
import org.jline.keymap.KeyMap;
import org.jline.reader.EndOfFileException;
@@ -31,95 +27,64 @@ import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.widget.TailTipWidgets;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.Model.PositionalParamSpec;
import picocli.CommandLine.Model.UsageMessageSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParentCommand;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.Parameters;
import picocli.shell.jline3.PicocliCommands;
import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory;
import tech.picnic.errorprone.openai.IssueExtractor.Issue;
// XXX: Review whether to enable a *subset* of JLine's built-ins. See
// https://github.com/remkop/picocli/tree/main/picocli-shell-jline3#jline-316-and-picocli-44-example.
public final class Cli {
public static void main(String... args) {
IssueResolutionController issueResolutionController =
new IssueResolutionController(ImmutableSetMultimap.of());
AnsiConsole.systemInstall();
try {
CommandSpec issues = command("issues", 'i', "List issues.");
CommandSpec submit =
command("submit", 's', "Submit issues to OpenAI.")
.addPositional(
PositionalParamSpec.builder()
.paramLabel("ISSUES")
.type(List.class)
.auxiliaryTypes(Integer.class)
.description("The subset of issues to submit (default: all)")
.build());
CommandSpec apply = command("apply", 'a', "Apply the changes suggested by OpenAI");
CommandSpec next = command("next", 'n', "Move to the next issue.");
CommandSpec previous = command("previous", 'p', "Move to the previous issue.");
try (Terminal terminal = TerminalBuilder.terminal()) {
IssueResolutionController issueResolutionController =
new IssueResolutionController(
terminal.writer(),
ImmutableSet.of(
new Issue<>(Path.of("xxx"), OptionalInt.of(1), OptionalInt.empty(), "msg"),
new Issue<>(Path.of("yyy"), OptionalInt.of(2), OptionalInt.empty(), "msg"),
new Issue<>(Path.of("xxx"), OptionalInt.of(3), OptionalInt.empty(), "msg")));
PicocliCommandsFactory factory = new PicocliCommandsFactory();
// Or, if you have your own factory, you can chain them like this:
// MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
// PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the
// factories
factory.setTerminal(terminal);
CommandLine cmd =
new CommandLine(
CommandSpec.create()
.name("")
.addSubcommand(null, issues)
.addSubcommand(null, submit)
.addSubcommand(null, apply)
.addSubcommand(null, next)
.addSubcommand(null, previous),
factory);
// XXX: Rename to `commands` if we don't enable the built-ins.
PicocliCommands commands = new PicocliCommands(cmd);
CommandLine commandLine = new CommandLine(issueResolutionController, factory);
PicocliCommands commands = new PicocliCommands(commandLine);
Parser parser = new DefaultParser();
// XXX: Check `TerminalBuilder.builder().build()` options.
try (Terminal terminal = TerminalBuilder.terminal()) {
SystemRegistry systemRegistry =
new SystemRegistryImpl(parser, terminal, () -> Path.of("").toAbsolutePath(), null);
systemRegistry.setCommandRegistries(commands);
systemRegistry.register("help", commands);
LineReader reader =
LineReaderBuilder.builder()
.terminal(terminal)
.completer(systemRegistry.completer())
.parser(parser)
.build();
cmd.setExecutionStrategy(
pr -> run(pr, reader.getTerminal().writer(), issueResolutionController));
factory.setTerminal(terminal);
new TailTipWidgets(
reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER)
.enable();
reader.getKeyMaps().get("main").bind(new Reference("tailtip-toggle"), KeyMap.ctrl('t'));
SystemRegistry systemRegistry =
new SystemRegistryImpl(parser, terminal, () -> Path.of("").toAbsolutePath(), null);
systemRegistry.setCommandRegistries(commands);
systemRegistry.register("help", commands);
while (true) {
try {
systemRegistry.cleanUp();
systemRegistry.execute(reader.readLine("prompt> ", null, (MaskingCallback) null, null));
} catch (UserInterruptException e) {
// XXX: Review whether indeed to ignore this.
} catch (EndOfFileException e) {
return;
} catch (Exception e) {
systemRegistry.trace(e);
}
LineReader reader =
LineReaderBuilder.builder()
.terminal(terminal)
.completer(systemRegistry.completer())
.parser(parser)
.build();
new TailTipWidgets(
reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER)
.enable();
reader.getKeyMaps().get("main").bind(new Reference("tailtip-toggle"), KeyMap.ctrl('t'));
while (true) {
try {
systemRegistry.cleanUp();
systemRegistry.execute(
reader.readLine(
issueResolutionController.prompt(), null, (MaskingCallback) null, null));
} catch (UserInterruptException e) {
// XXX: Review whether indeed to ignore this.
} catch (EndOfFileException e) {
return;
} catch (Exception e) {
systemRegistry.trace(e);
}
}
} catch (Throwable t) {
@@ -130,71 +95,99 @@ public final class Cli {
}
}
// XXX: Should we even support those `files.isEmpty()` cases?
// XXX: Allow issues to be dropped?
// XXX: Mark files modified/track modification count?
// XXX: List full diff over multiple rounds?
@Command(name = "")
static final class IssueResolutionController {
private final ImmutableList<ImmutableList<Issue<Path>>> issuesGroupedByPath;
private final PrintWriter out;
private final ImmutableList<FileIssues> files;
private int currentIndex = 0;
// XXX: Maybe shouldn't already group by path at call site?
IssueResolutionController(ImmutableSetMultimap<Path, Issue<Path>> issues) {
this.issuesGroupedByPath =
issues.asMap().values().stream().map(ImmutableList::copyOf).collect(toImmutableList());
IssueResolutionController(PrintWriter output, ImmutableSet<Issue<Path>> issues) {
this.out = output;
this.files =
issues.stream()
.collect(
collectingAndThen(
groupingBy(Issue::file),
m ->
m.entrySet().stream()
.map(
e ->
new FileIssues(
e.getKey(), ImmutableList.copyOf(e.getValue())))
.collect(toImmutableList())));
}
void list(PrintWriter out) {
if (currentIndex >= issuesGroupedByPath.size()) {
out.println("No more issues.");
@Command(aliases = "i", subcommands = HelpCommand.class, description = "List issues.")
void issues() {
if (files.isEmpty()) {
out.println("No issues.");
return;
}
ImmutableList<Issue<Path>> issues = issuesGroupedByPath.get(currentIndex);
out.printf("Issues for %s:%n", issues.get(0).file());
FileIssues fileIssues = files.get(currentIndex);
out.printf("Issues for %s:%n", fileIssues.file());
ImmutableList<Issue<Path>> issues = fileIssues.issues();
for (int i = 0; i < issues.size(); i++) {
out.printf(Locale.ROOT, "%02d. %s%n", i, issues.get(i).description());
out.printf(Locale.ROOT, "%2d. %s%n", i, issues.get(i).description());
}
}
void next(PrintWriter out) {
currentIndex++;
list(out);
@Command(
aliases = "s",
subcommands = HelpCommand.class,
description = "Submit issues to OpenAI.")
void submit(
@Parameters(arity = "0..*", description = "The subset of issues to submit (default: all)")
List<Integer> issues) {
// XXX: Validate indices.
// XXX: Implement.
}
void previous(PrintWriter out) {
currentIndex--;
list(out);
}
}
static int run(ParseResult pr, PrintWriter out, IssueResolutionController controller) {
Integer helpExitCode = CommandLine.executeHelpRequest(pr);
if (helpExitCode != null) {
return helpExitCode;
@Command(
aliases = "a",
subcommands = HelpCommand.class,
description = "Apply the changes suggested by OpenAI")
void apply() {
// XXX: Implement.
}
ParseResult subcommand = Objects.requireNonNull(pr.subcommand(), "subcommand");
switch (subcommand.commandSpec().name()) {
case "issues" -> controller.list(out);
case "submit" -> out.println("submit");
case "apply" -> out.println("apply");
case "next" -> controller.next(out);
case "previous" -> controller.previous(out);
default -> throw new IllegalStateException(
"Unknown command: " + subcommand.commandSpec().name());
@Command(
aliases = "n",
subcommands = HelpCommand.class,
description = "Move to the next issue.")
void next() {
if (currentIndex < files.size() - 1) {
currentIndex++;
issues();
} else {
out.println("No next issues.");
}
}
return 0;
}
private static CommandSpec command(String name, char alias, String description) {
return CommandSpec.create()
.name(name)
.aliases(Character.toString(alias))
.usageMessage(new UsageMessageSpec().description(description))
.addOption(helpOption());
}
@Command(
aliases = "p",
subcommands = HelpCommand.class,
description = "Move to the previous issue.")
void previous() {
if (currentIndex > 0) {
currentIndex--;
issues();
} else {
out.println("No previous issues.");
}
}
private static OptionSpec helpOption() {
return OptionSpec.builder("-h", "--help")
.usageHelp(true)
.description("Show this help message and exit.")
.build();
String prompt() {
return files.isEmpty()
? "No issues>"
: String.format(
"File %s (%s/%s)>", files.get(currentIndex).file(), currentIndex + 1, files.size());
}
record FileIssues(Path file, ImmutableList<Issue<Path>> issues) {}
}
}

View File

@@ -1,247 +0,0 @@
package tech.picnic.errorprone.openai;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.fusesource.jansi.AnsiConsole;
import org.jline.console.SystemRegistry;
import org.jline.console.impl.SystemRegistryImpl;
import org.jline.keymap.KeyMap;
import org.jline.reader.Binding;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
import org.jline.reader.Parser;
import org.jline.reader.Reference;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.widget.TailTipWidgets;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParentCommand;
import picocli.shell.jline3.PicocliCommands;
import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory;
/**
* Example that demonstrates how to build an interactive shell with JLine3 and picocli. This example
* requires JLine 3.16+ and picocli 4.4+.
*
* <p>The built-in {@code PicocliCommands.ClearScreen} command was introduced in picocli 4.6.
*/
// XXX: Drop this!!
public class Example {
/** Top-level command that just prints help. */
@Command(
name = "",
description = {
"Example interactive shell with completion and autosuggestions. "
+ "Hit @|magenta <TAB>|@ to see available commands.",
"Hit @|magenta ALT-S|@ to toggle tailtips.",
""
},
footer = {"", "Press Ctrl-D to exit."},
subcommands = {
MyCommand.class,
PicocliCommands.ClearScreen.class,
CommandLine.HelpCommand.class
})
static class CliCommands implements Runnable {
PrintWriter out;
CliCommands() {}
public void setReader(LineReader reader) {
out = reader.getTerminal().writer();
}
public void run() {
out.println(new CommandLine(this).getUsageMessage());
}
}
/** A command with some options to demonstrate completion. */
@Command(
name = "cmd",
mixinStandardHelpOptions = true,
version = "1.0",
description = {
"Command with some options to demonstrate TAB-completion.",
" (Note that enum values also get completed.)"
},
subcommands = {Nested.class, CommandLine.HelpCommand.class})
static class MyCommand implements Runnable {
@Option(
names = {"-v", "--verbose"},
description = {
"Specify multiple -v options to increase verbosity.",
"For example, `-v -v -v` or `-vvv`"
})
private boolean[] verbosity = {};
@ArgGroup(exclusive = false)
private MyDuration myDuration = new MyDuration();
static class MyDuration {
@Option(
names = {"-d", "--duration"},
description = "The duration quantity.",
required = true)
private int amount;
@Option(
names = {"-u", "--timeUnit"},
description = "The duration time unit.",
required = true)
private TimeUnit unit;
}
@ParentCommand CliCommands parent;
public void run() {
if (verbosity.length > 0) {
parent.out.printf("Hi there. You asked for %d %s.%n", myDuration.amount, myDuration.unit);
} else {
parent.out.println("hi!");
}
}
}
@Command(
name = "nested",
mixinStandardHelpOptions = true,
subcommands = {CommandLine.HelpCommand.class},
description = "Hosts more sub-subcommands")
static class Nested implements Runnable {
public void run() {
System.out.println("I'm a nested subcommand. I don't do much, but I have sub-subcommands!");
}
@Command(
mixinStandardHelpOptions = true,
subcommands = {CommandLine.HelpCommand.class},
description = "Multiplies two numbers.")
public void multiply(
@Option(
names = {"-l", "--left"},
required = true)
int left,
@Option(
names = {"-r", "--right"},
required = true)
int right) {
System.out.printf("%d * %d = %d%n", left, right, left * right);
}
@Command(
mixinStandardHelpOptions = true,
subcommands = {CommandLine.HelpCommand.class},
description = "Adds two numbers.")
public void add(
@Option(
names = {"-l", "--left"},
required = true)
int left,
@Option(
names = {"-r", "--right"},
required = true)
int right) {
System.out.printf("%d + %d = %d%n", left, right, left + right);
}
@Command(
mixinStandardHelpOptions = true,
subcommands = {CommandLine.HelpCommand.class},
description = "Subtracts two numbers.")
public void subtract(
@Option(
names = {"-l", "--left"},
required = true)
int left,
@Option(
names = {"-r", "--right"},
required = true)
int right) {
System.out.printf("%d - %d = %d%n", left, right, left - right);
}
}
public static void main(String[] args) {
AnsiConsole.systemInstall();
try {
Supplier<Path> workDir = () -> Paths.get(System.getProperty("user.dir"));
// XXX: Review whether to enable JLine built-in commands.
// Builtins builtins = new Builtins(workDir, new ConfigurationPath(null, null), null);
// builtins.rename(Builtins.Command.TTOP, "top");
// set up picocli commands
CliCommands commands = new CliCommands();
PicocliCommandsFactory factory = new PicocliCommandsFactory();
// Or, if you have your own factory, you can chain them like this:
// MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
// PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the
// factories
CommandLine cmd = new CommandLine(commands, factory);
PicocliCommands picocliCommands = new PicocliCommands(cmd);
Parser parser = new DefaultParser();
// XXX: Check `TerminalBuilder.builder().build()` options.
try (Terminal terminal = TerminalBuilder.terminal()) {
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, null);
// XXX: Or if we enable the built-ins:
// systemRegistry.setCommandRegistries(builtins, picocliCommands);
systemRegistry.setCommandRegistries(picocliCommands);
systemRegistry.register("help", picocliCommands);
LineReader reader =
LineReaderBuilder.builder()
.terminal(terminal)
.completer(systemRegistry.completer())
.parser(parser)
.variable(LineReader.LIST_MAX, 50)
.build();
// XXX: And if we enable the built-ins:
// builtins.setLineReader(reader);
commands.setReader(reader);
factory.setTerminal(terminal);
TailTipWidgets widgets =
new TailTipWidgets(
reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
widgets.enable();
KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
String prompt = "prompt> ";
String rightPrompt = null;
String line;
while (true) {
try {
systemRegistry.cleanUp();
line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
systemRegistry.execute(line);
} catch (UserInterruptException e) {
// XXX: Review whether indeed to ignore this.
} catch (EndOfFileException e) {
return;
} catch (Exception e) {
systemRegistry.trace(e);
}
}
}
} catch (Throwable t) {
// XXX: Review!
t.printStackTrace();
} finally {
AnsiConsole.systemUninstall();
}
}
}

View File

@@ -3,18 +3,12 @@ package tech.picnic.errorprone.openai;
import static org.assertj.core.api.Assertions.assertThat;
import static tech.picnic.errorprone.openai.OpenAi.OPENAI_TOKEN_VARIABLE;
import java.util.List;
import java.util.Objects;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.PositionalParamSpec;
import picocli.CommandLine.ParseResult;
// XXX: Drop this code.
@TestInstance(Lifecycle.PER_CLASS)
@@ -70,77 +64,4 @@ final class PlayGroundTest {
assertThat(openAi.requestChatCompletion(input)).isEqualTo("XXX");
}
@Test
void cliTest() {
// OptionSpec help = OptionSpec.builder("help", "h")
// .usageHelp(true).build();
// CommandSpec help =
// CommandSpec.create()
// .name("help")
// .aliases("h")
// .addPositional(
// PositionalParamSpec.builder().paramLabel("COMMAND").arity("0..1").build());
CommandSpec issues = CommandSpec.create().name("issues").aliases("i");
CommandSpec submit =
CommandSpec.create()
.name("submit")
.aliases("s")
.addPositional(
PositionalParamSpec.builder()
.paramLabel("ISSUES")
.type(List.class)
.auxiliaryTypes(Integer.class)
.description("The subset of issues to submit (default: all)")
.build());
CommandSpec apply = CommandSpec.create().name("apply").aliases("a");
CommandSpec next = CommandSpec.create().name("next").aliases("n");
CommandSpec previous = CommandSpec.create().name("previous").aliases("prev", "p");
CommandSpec quit = CommandSpec.create().name("quit").aliases("q");
CommandSpec commands =
CommandSpec.create()
.name("")
// .addSubcommand(null, help)
.addSubcommand(null, issues)
.addSubcommand(null, submit)
.addSubcommand(null, apply)
.addSubcommand(null, next)
.addSubcommand(null, previous)
.addSubcommand(null, quit)
// .addOption(help)
;
CommandLine commandLine = new CommandLine(commands);
// set an execution strategy (the run(ParseResult) method) that will be called
// by CommandLine.execute(args) when user input was valid
commandLine.setExecutionStrategy(PlayGroundTest::run);
// int exitCode = commandLine.execute("-c", "4", "file1", "file2");
// int exitCode = commandLine.execute("issues");
// int exitCode = commandLine.execute("submit", "4");
int exitCode = commandLine.execute("submit", "-h");
assertThat(exitCode).isEqualTo(0);
}
static int run(ParseResult pr) {
// handle requests for help or version information
Integer helpExitCode = CommandLine.executeHelpRequest(pr);
if (helpExitCode != null) {
return helpExitCode;
}
ParseResult subcommand = Objects.requireNonNull(pr.subcommand(), "subcommand");
switch (subcommand.commandSpec().name()) {
case "help":
PositionalParamSpec x = subcommand.matchedPositional(0);
}
// implement the business logic
// int count = pr.matchedOptionValue('c', 1);
// List<File> files = pr.matchedPositionalValue(0, Collections.<File>emptyList());
return 0;
}
}