88 import static java.nio.file.StandardOpenOption.WRITE; |
89 import static java.nio.file.StandardOpenOption.WRITE; |
89 import java.util.MissingResourceException; |
90 import java.util.MissingResourceException; |
90 import java.util.Optional; |
91 import java.util.Optional; |
91 import java.util.ResourceBundle; |
92 import java.util.ResourceBundle; |
92 import java.util.Spliterators; |
93 import java.util.Spliterators; |
|
94 import java.util.function.Function; |
93 import java.util.function.Supplier; |
95 import java.util.function.Supplier; |
94 import static java.util.stream.Collectors.toList; |
96 import static java.util.stream.Collectors.toList; |
|
97 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; |
95 |
98 |
96 /** |
99 /** |
97 * Command line REPL tool for Java using the JShell API. |
100 * Command line REPL tool for Java using the JShell API. |
98 * @author Robert Field |
101 * @author Robert Field |
99 */ |
102 */ |
100 public class JShellTool { |
103 public class JShellTool { |
101 |
104 |
102 private static final Pattern LINEBREAK = Pattern.compile("\\R"); |
105 private static final Pattern LINEBREAK = Pattern.compile("\\R"); |
103 private static final Pattern HISTORY_ALL_START_FILENAME = Pattern.compile( |
106 private static final Pattern HISTORY_ALL_START_FILENAME = Pattern.compile( |
104 "((?<cmd>(all|history|start))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)"); |
107 "((?<cmd>(all|history|start))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)"); |
|
108 private static final String RECORD_SEPARATOR = "\u241E"; |
105 |
109 |
106 final InputStream cmdin; |
110 final InputStream cmdin; |
107 final PrintStream cmdout; |
111 final PrintStream cmdout; |
108 final PrintStream cmderr; |
112 final PrintStream cmderr; |
109 final PrintStream console; |
113 final PrintStream console; |
148 private Feedback feedback = Feedback.Default; |
152 private Feedback feedback = Feedback.Default; |
149 private String cmdlineClasspath = null; |
153 private String cmdlineClasspath = null; |
150 private String cmdlineStartup = null; |
154 private String cmdlineStartup = null; |
151 private String editor = null; |
155 private String editor = null; |
152 |
156 |
153 static final Preferences PREFS = Preferences.userRoot().node("tool/REPL"); |
157 // Commands and snippets which should be replayed |
|
158 private List<String> replayableHistory; |
|
159 private List<String> replayableHistoryPrevious; |
|
160 |
|
161 static final Preferences PREFS = Preferences.userRoot().node("tool/JShell"); |
154 |
162 |
155 static final String STARTUP_KEY = "STARTUP"; |
163 static final String STARTUP_KEY = "STARTUP"; |
|
164 static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; |
156 |
165 |
157 static final String DEFAULT_STARTUP = |
166 static final String DEFAULT_STARTUP = |
158 "\n" + |
167 "\n" + |
159 "import java.util.*;\n" + |
168 "import java.util.*;\n" + |
160 "import java.io.*;\n" + |
169 "import java.io.*;\n" + |
163 "import java.util.concurrent.*;\n" + |
172 "import java.util.concurrent.*;\n" + |
164 "import java.util.prefs.*;\n" + |
173 "import java.util.prefs.*;\n" + |
165 "import java.util.regex.*;\n" + |
174 "import java.util.regex.*;\n" + |
166 "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; |
175 "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; |
167 |
176 |
168 // Tool id (tid) mapping |
177 // Tool id (tid) mapping: the three name spaces |
169 NameSpace mainNamespace; |
178 NameSpace mainNamespace; |
170 NameSpace startNamespace; |
179 NameSpace startNamespace; |
171 NameSpace errorNamespace; |
180 NameSpace errorNamespace; |
|
181 |
|
182 // Tool id (tid) mapping: the current name spaces |
172 NameSpace currentNameSpace; |
183 NameSpace currentNameSpace; |
|
184 |
173 Map<Snippet,SnippetInfo> mapSnippet; |
185 Map<Snippet,SnippetInfo> mapSnippet; |
174 |
186 |
175 void debug(String format, Object... args) { |
187 void debug(String format, Object... args) { |
176 if (debug) { |
188 if (debug) { |
177 cmderr.printf(format + "\n", args); |
189 cmderr.printf(format + "\n", args); |
368 startNamespace = new NameSpace("start", "s"); |
386 startNamespace = new NameSpace("start", "s"); |
369 errorNamespace = new NameSpace("error", "e"); |
387 errorNamespace = new NameSpace("error", "e"); |
370 mapSnippet = new LinkedHashMap<>(); |
388 mapSnippet = new LinkedHashMap<>(); |
371 currentNameSpace = startNamespace; |
389 currentNameSpace = startNamespace; |
372 |
390 |
|
391 // Reset the replayable history, saving the old for restore |
|
392 replayableHistoryPrevious = replayableHistory; |
|
393 replayableHistory = new ArrayList<>(); |
|
394 |
373 state = JShell.builder() |
395 state = JShell.builder() |
374 .in(userin) |
396 .in(userin) |
375 .out(userout) |
397 .out(userout) |
376 .err(usererr) |
398 .err(usererr) |
377 .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext()) |
399 .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext()) |
380 : errorNamespace.tid(sn)) |
402 : errorNamespace.tid(sn)) |
381 .build(); |
403 .build(); |
382 analysis = state.sourceCodeAnalysis(); |
404 analysis = state.sourceCodeAnalysis(); |
383 shutdownSubscription = state.onShutdown((JShell deadState) -> { |
405 shutdownSubscription = state.onShutdown((JShell deadState) -> { |
384 if (deadState == state) { |
406 if (deadState == state) { |
385 hard("State engine terminated. See /history"); |
407 hard("State engine terminated."); |
|
408 hard("Restore definitions with: /reload restore"); |
386 live = false; |
409 live = false; |
387 } |
410 } |
388 }); |
411 }); |
389 live = true; |
412 live = true; |
390 |
413 |
391 if (cmdlineClasspath != null) { |
414 if (cmdlineClasspath != null) { |
392 state.addToClasspath(cmdlineClasspath); |
415 state.addToClasspath(cmdlineClasspath); |
393 } |
416 } |
394 |
|
395 |
417 |
396 String start; |
418 String start; |
397 if (cmdlineStartup == null) { |
419 if (cmdlineStartup == null) { |
398 start = PREFS.get(STARTUP_KEY, "<nada>"); |
420 start = PREFS.get(STARTUP_KEY, "<nada>"); |
399 if (start.equals("<nada>")) { |
421 if (start.equals("<nada>")) { |
514 if (!rerunHistoryEntryById(cmd.substring(1))) { |
542 if (!rerunHistoryEntryById(cmd.substring(1))) { |
515 hard("No such command or snippet id: %s", cmd); |
543 hard("No such command or snippet id: %s", cmd); |
516 fluff("Type /help for help."); |
544 fluff("Type /help for help."); |
517 } |
545 } |
518 } else if (candidates.length == 1) { |
546 } else if (candidates.length == 1) { |
519 candidates[0].run.accept(arg); |
547 Command command = candidates[0]; |
|
548 |
|
549 // If comand was successful and is of a replayable kind, add it the replayable history |
|
550 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { |
|
551 addToReplayHistory((command.command + " " + arg).trim()); |
|
552 } |
520 } else { |
553 } else { |
521 hard("Command: %s is ambiguous: %s", cmd, Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); |
554 hard("Command: %s is ambiguous: %s", cmd, Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); |
522 fluff("Type /help for help."); |
555 fluff("Type /help for help."); |
523 } |
556 } |
524 } |
557 } |
544 |
577 |
545 static final class Command { |
578 static final class Command { |
546 public final String command; |
579 public final String command; |
547 public final String params; |
580 public final String params; |
548 public final String description; |
581 public final String description; |
549 public final Consumer<String> run; |
582 public final Function<String,Boolean> run; |
550 public final CompletionProvider completions; |
583 public final CompletionProvider completions; |
551 public final CommandKind kind; |
584 public final CommandKind kind; |
552 |
585 |
553 public Command(String command, String params, String description, Consumer<String> run, CompletionProvider completions) { |
586 public Command(String command, String params, String description, Function<String,Boolean> run, CompletionProvider completions) { |
554 this(command, params, description, run, completions, CommandKind.NORMAL); |
587 this(command, params, description, run, completions, CommandKind.NORMAL); |
555 } |
588 } |
556 |
589 |
557 public Command(String command, String params, String description, Consumer<String> run, CompletionProvider completions, CommandKind kind) { |
590 public Command(String command, String params, String description, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { |
558 this.command = command; |
591 this.command = command; |
559 this.params = params; |
592 this.params = params; |
560 this.description = description; |
593 this.description = description; |
561 this.run = run; |
594 this.run = run; |
562 this.completions = completions; |
595 this.completions = completions; |
600 |
634 |
601 } |
635 } |
602 |
636 |
603 private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); |
637 private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); |
604 private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("all ", "start ", "history "); |
638 private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("all ", "start ", "history "); |
|
639 private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("restore", "quiet"); |
605 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); |
640 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); |
606 private final Map<String, Command> commands = new LinkedHashMap<>(); |
641 private final Map<String, Command> commands = new LinkedHashMap<>(); |
607 private void registerCommand(Command cmd) { |
642 private void registerCommand(Command cmd) { |
608 commands.put(cmd.command, cmd); |
643 commands.put(cmd.command, cmd); |
609 } |
644 } |
672 anchor[0] += space + 1; |
707 anchor[0] += space + 1; |
673 return result; |
708 return result; |
674 }; |
709 }; |
675 } |
710 } |
676 |
711 |
|
712 private static CompletionProvider reloadCompletion() { |
|
713 return (code, cursor, anchor) -> { |
|
714 List<Suggestion> result = new ArrayList<>(); |
|
715 int pastSpace = code.indexOf(' ') + 1; // zero if no space |
|
716 result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor)); |
|
717 anchor[0] += pastSpace; |
|
718 return result; |
|
719 }; |
|
720 } |
|
721 |
677 // Table of commands -- with command forms, argument kinds, help message, implementation, ... |
722 // Table of commands -- with command forms, argument kinds, help message, implementation, ... |
678 |
723 |
679 { |
724 { |
680 registerCommand(new Command("/list", "[all|start|history|<name or id>]", "list the source you have typed", |
725 registerCommand(new Command("/list", "[all|start|history|<name or id>]", "list the source you have typed", |
681 arg -> cmdList(arg), |
726 arg -> cmdList(arg), |
686 registerCommand(new Command("/edit", "<name or id>", "edit a source entry referenced by name or id", |
731 registerCommand(new Command("/edit", "<name or id>", "edit a source entry referenced by name or id", |
687 arg -> cmdEdit(arg), |
732 arg -> cmdEdit(arg), |
688 editCompletion())); |
733 editCompletion())); |
689 registerCommand(new Command("/drop", "<name or id>", "delete a source entry referenced by name or id", |
734 registerCommand(new Command("/drop", "<name or id>", "delete a source entry referenced by name or id", |
690 arg -> cmdDrop(arg), |
735 arg -> cmdDrop(arg), |
691 editCompletion())); |
736 editCompletion(), |
|
737 CommandKind.REPLAY)); |
692 registerCommand(new Command("/save", "[all|history|start] <file>", "save: <none> - current source;\n" + |
738 registerCommand(new Command("/save", "[all|history|start] <file>", "save: <none> - current source;\n" + |
693 " all - source including overwritten, failed, and start-up code;\n" + |
739 " all - source including overwritten, failed, and start-up code;\n" + |
694 " history - editing history;\n" + |
740 " history - editing history;\n" + |
695 " start - default start-up definitions", |
741 " start - default start-up definitions", |
696 arg -> cmdSave(arg), |
742 arg -> cmdSave(arg), |
714 arg -> cmdExit(), |
760 arg -> cmdExit(), |
715 EMPTY_COMPLETION_PROVIDER)); |
761 EMPTY_COMPLETION_PROVIDER)); |
716 registerCommand(new Command("/reset", null, "reset everything in the REPL", |
762 registerCommand(new Command("/reset", null, "reset everything in the REPL", |
717 arg -> cmdReset(), |
763 arg -> cmdReset(), |
718 EMPTY_COMPLETION_PROVIDER)); |
764 EMPTY_COMPLETION_PROVIDER)); |
|
765 registerCommand(new Command("/reload", "[restore] [quiet]", "reset and replay relevant history -- current or previous (restore)", |
|
766 arg -> cmdReload(arg), |
|
767 reloadCompletion())); |
719 registerCommand(new Command("/feedback", "<level>", "feedback information: off, concise, normal, verbose, default, or ?", |
768 registerCommand(new Command("/feedback", "<level>", "feedback information: off, concise, normal, verbose, default, or ?", |
720 arg -> cmdFeedback(arg), |
769 arg -> cmdFeedback(arg), |
721 new FixedCompletionProvider("off", "concise", "normal", "verbose", "default", "?"))); |
770 new FixedCompletionProvider("off", "concise", "normal", "verbose", "default", "?"))); |
722 registerCommand(new Command("/prompt", null, "toggle display of a prompt", |
771 registerCommand(new Command("/prompt", null, "toggle display of a prompt", |
723 arg -> cmdPrompt(), |
772 arg -> cmdPrompt(), |
724 EMPTY_COMPLETION_PROVIDER)); |
773 EMPTY_COMPLETION_PROVIDER)); |
725 registerCommand(new Command("/classpath", "<path>", "add a path to the classpath", |
774 registerCommand(new Command("/classpath", "<path>", "add a path to the classpath", |
726 arg -> cmdClasspath(arg), |
775 arg -> cmdClasspath(arg), |
727 classPathCompletion())); |
776 classPathCompletion(), |
|
777 CommandKind.REPLAY)); |
728 registerCommand(new Command("/history", null, "history of what you have typed", |
778 registerCommand(new Command("/history", null, "history of what you have typed", |
729 arg -> cmdHistory(), |
779 arg -> cmdHistory(), |
730 EMPTY_COMPLETION_PROVIDER)); |
780 EMPTY_COMPLETION_PROVIDER)); |
731 registerCommand(new Command("/setstart", "<file>", "read file and set as the new start-up definitions", |
781 registerCommand(new Command("/setstart", "<file>", "read file and set as the new start-up definitions", |
732 arg -> cmdSetStart(arg), |
782 arg -> cmdSetStart(arg), |
799 return null; |
849 return null; |
800 } |
850 } |
801 |
851 |
802 // --- Command implementations --- |
852 // --- Command implementations --- |
803 |
853 |
804 void cmdSetEditor(String arg) { |
854 boolean cmdSetEditor(String arg) { |
805 if (arg.isEmpty()) { |
855 if (arg.isEmpty()) { |
806 hard("/seteditor requires a path argument"); |
856 hard("/seteditor requires a path argument"); |
|
857 return false; |
807 } else { |
858 } else { |
808 editor = arg; |
859 editor = arg; |
809 fluff("Editor set to: %s", arg); |
860 fluff("Editor set to: %s", arg); |
810 } |
861 return true; |
811 } |
862 } |
812 |
863 } |
813 void cmdClasspath(String arg) { |
864 |
|
865 boolean cmdClasspath(String arg) { |
814 if (arg.isEmpty()) { |
866 if (arg.isEmpty()) { |
815 hard("/classpath requires a path argument"); |
867 hard("/classpath requires a path argument"); |
|
868 return false; |
816 } else { |
869 } else { |
817 state.addToClasspath(toPathResolvingUserHome(arg).toString()); |
870 state.addToClasspath(toPathResolvingUserHome(arg).toString()); |
818 fluff("Path %s added to classpath", arg); |
871 fluff("Path %s added to classpath", arg); |
819 } |
872 return true; |
820 } |
873 } |
821 |
874 } |
822 void cmdDebug(String arg) { |
875 |
|
876 boolean cmdDebug(String arg) { |
823 if (arg.isEmpty()) { |
877 if (arg.isEmpty()) { |
824 debug = !debug; |
878 debug = !debug; |
825 InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0); |
879 InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0); |
826 fluff("Debugging %s", debug ? "on" : "off"); |
880 fluff("Debugging %s", debug ? "on" : "off"); |
827 } else { |
881 } else { |
858 fluff("Event debugging on"); |
912 fluff("Event debugging on"); |
859 break; |
913 break; |
860 default: |
914 default: |
861 hard("Unknown debugging option: %c", ch); |
915 hard("Unknown debugging option: %c", ch); |
862 fluff("Use: 0 r g f c d"); |
916 fluff("Use: 0 r g f c d"); |
863 break; |
917 return false; |
864 } |
918 } |
865 } |
919 } |
866 InternalDebugControl.setDebugFlags(state, flags); |
920 InternalDebugControl.setDebugFlags(state, flags); |
867 } |
921 } |
868 } |
922 return true; |
869 |
923 } |
870 private void cmdExit() { |
924 |
|
925 private boolean cmdExit() { |
871 regenerateOnDeath = false; |
926 regenerateOnDeath = false; |
872 live = false; |
927 live = false; |
|
928 if (!replayableHistory.isEmpty()) { |
|
929 PREFS.put(REPLAY_RESTORE_KEY, replayableHistory.stream().reduce( |
|
930 (a, b) -> a + RECORD_SEPARATOR + b).get()); |
|
931 } |
873 fluff("Goodbye\n"); |
932 fluff("Goodbye\n"); |
874 } |
933 return true; |
875 |
934 } |
876 private void cmdFeedback(String arg) { |
935 |
|
936 private boolean cmdFeedback(String arg) { |
877 switch (arg) { |
937 switch (arg) { |
878 case "": |
938 case "": |
879 case "d": |
939 case "d": |
880 case "default": |
940 case "default": |
881 feedback = Feedback.Default; |
941 feedback = Feedback.Default; |
903 hard(" normal"); |
963 hard(" normal"); |
904 hard(" verbose"); |
964 hard(" verbose"); |
905 hard(" default"); |
965 hard(" default"); |
906 hard("You may also use just the first letter, for example: /f c"); |
966 hard("You may also use just the first letter, for example: /f c"); |
907 hard("In interactive mode 'default' is the same as 'normal', from a file it is the same as 'off'"); |
967 hard("In interactive mode 'default' is the same as 'normal', from a file it is the same as 'off'"); |
908 return; |
968 return false; |
909 } |
969 } |
910 fluff("Feedback mode: %s", feedback.name().toLowerCase()); |
970 fluff("Feedback mode: %s", feedback.name().toLowerCase()); |
911 } |
971 return true; |
912 |
972 } |
913 void cmdHelp() { |
973 |
|
974 boolean cmdHelp() { |
914 int synopsisLen = 0; |
975 int synopsisLen = 0; |
915 Map<String, String> synopsis2Description = new LinkedHashMap<>(); |
976 Map<String, String> synopsis2Description = new LinkedHashMap<>(); |
916 for (Command cmd : new LinkedHashSet<>(commands.values())) { |
977 for (Command cmd : new LinkedHashSet<>(commands.values())) { |
917 if (cmd.kind == CommandKind.HIDDEN) |
978 if (cmd.kind == CommandKind.HIDDEN) |
918 continue; |
979 continue; |
934 } |
995 } |
935 cmdout.println(); |
996 cmdout.println(); |
936 cmdout.println("Supported shortcuts include:"); |
997 cmdout.println("Supported shortcuts include:"); |
937 cmdout.println("<tab> -- show possible completions for the current text"); |
998 cmdout.println("<tab> -- show possible completions for the current text"); |
938 cmdout.println("Shift-<tab> -- for current method or constructor invocation, show a synopsis of the method/constructor"); |
999 cmdout.println("Shift-<tab> -- for current method or constructor invocation, show a synopsis of the method/constructor"); |
939 } |
1000 return true; |
940 |
1001 } |
941 private void cmdHistory() { |
1002 |
|
1003 private boolean cmdHistory() { |
942 cmdout.println(); |
1004 cmdout.println(); |
943 for (String s : input.currentSessionHistory()) { |
1005 for (String s : input.currentSessionHistory()) { |
944 // No number prefix, confusing with snippet ids |
1006 // No number prefix, confusing with snippet ids |
945 cmdout.printf("%s\n", s); |
1007 cmdout.printf("%s\n", s); |
946 } |
1008 } |
|
1009 return true; |
947 } |
1010 } |
948 |
1011 |
949 /** |
1012 /** |
950 * Avoid parameterized varargs possible heap pollution warning. |
1013 * Avoid parameterized varargs possible heap pollution warning. |
951 */ |
1014 */ |
1008 ); |
1071 ); |
1009 return result; |
1072 return result; |
1010 } |
1073 } |
1011 } |
1074 } |
1012 |
1075 |
1013 private void cmdDrop(String arg) { |
1076 private boolean cmdDrop(String arg) { |
1014 if (arg.isEmpty()) { |
1077 if (arg.isEmpty()) { |
1015 hard("In the /drop argument, please specify an import, variable, method, or class to drop."); |
1078 hard("In the /drop argument, please specify an import, variable, method, or class to drop."); |
1016 hard("Specify by id or name. Use /list to see ids. Use /reset to reset all state."); |
1079 hard("Specify by id or name. Use /list to see ids. Use /reset to reset all state."); |
1017 return; |
1080 return false; |
1018 } |
1081 } |
1019 Stream<Snippet> stream = argToSnippets(arg, false); |
1082 Stream<Snippet> stream = argToSnippets(arg, false); |
1020 if (stream == null) { |
1083 if (stream == null) { |
1021 hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
1084 hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
1022 return; |
1085 return false; |
1023 } |
1086 } |
1024 List<Snippet> snippets = stream |
1087 List<Snippet> snippets = stream |
1025 .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet) |
1088 .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet) |
1026 .collect(toList()); |
1089 .collect(toList()); |
1027 if (snippets.isEmpty()) { |
1090 if (snippets.isEmpty()) { |
1028 hard("The argument did not specify an active import, variable, method, or class to drop."); |
1091 hard("The argument did not specify an active import, variable, method, or class to drop."); |
1029 return; |
1092 return false; |
1030 } |
1093 } |
1031 if (snippets.size() > 1) { |
1094 if (snippets.size() > 1) { |
1032 hard("The argument references more than one import, variable, method, or class."); |
1095 hard("The argument references more than one import, variable, method, or class."); |
1033 hard("Try again with one of the ids below:"); |
1096 hard("Try again with one of the ids below:"); |
1034 for (Snippet sn : snippets) { |
1097 for (Snippet sn : snippets) { |
1035 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
1098 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
1036 } |
1099 } |
1037 return; |
1100 return false; |
1038 } |
1101 } |
1039 PersistentSnippet psn = (PersistentSnippet) snippets.get(0); |
1102 PersistentSnippet psn = (PersistentSnippet) snippets.get(0); |
1040 state.drop(psn).forEach(this::handleEvent); |
1103 state.drop(psn).forEach(this::handleEvent); |
1041 } |
1104 return true; |
1042 |
1105 } |
1043 private void cmdEdit(String arg) { |
1106 |
|
1107 private boolean cmdEdit(String arg) { |
1044 Stream<Snippet> stream = argToSnippets(arg, true); |
1108 Stream<Snippet> stream = argToSnippets(arg, true); |
1045 if (stream == null) { |
1109 if (stream == null) { |
1046 hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
1110 hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); |
1047 return; |
1111 return false; |
1048 } |
1112 } |
1049 Set<String> srcSet = new LinkedHashSet<>(); |
1113 Set<String> srcSet = new LinkedHashSet<>(); |
1050 stream.forEachOrdered(sn -> { |
1114 stream.forEachOrdered(sn -> { |
1051 String src = sn.source(); |
1115 String src = sn.source(); |
1052 switch (sn.subKind()) { |
1116 switch (sn.subKind()) { |
1076 if (editor == null) { |
1140 if (editor == null) { |
1077 EditPad.edit(errorHandler, src, saveHandler); |
1141 EditPad.edit(errorHandler, src, saveHandler); |
1078 } else { |
1142 } else { |
1079 ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); |
1143 ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); |
1080 } |
1144 } |
|
1145 return true; |
1081 } |
1146 } |
1082 //where |
1147 //where |
1083 // receives editor requests to save |
1148 // receives editor requests to save |
1084 private class SaveHandler implements Consumer<String> { |
1149 private class SaveHandler implements Consumer<String> { |
1085 |
1150 |
1133 } |
1198 } |
1134 return s.substring(b, e + 1); |
1199 return s.substring(b, e + 1); |
1135 } |
1200 } |
1136 } |
1201 } |
1137 |
1202 |
1138 private void cmdList(String arg) { |
1203 private boolean cmdList(String arg) { |
1139 if (arg.equals("history")) { |
1204 if (arg.equals("history")) { |
1140 cmdHistory(); |
1205 return cmdHistory(); |
1141 return; |
|
1142 } |
1206 } |
1143 Stream<Snippet> stream = argToSnippets(arg, true); |
1207 Stream<Snippet> stream = argToSnippets(arg, true); |
1144 if (stream == null) { |
1208 if (stream == null) { |
1145 // Check if there are any definitions at all |
1209 // Check if there are any definitions at all |
1146 if (argToSnippets("", false).iterator().hasNext()) { |
1210 if (argToSnippets("", false).iterator().hasNext()) { |
1147 hard("No definition or id named %s found. Try /list without arguments.", arg); |
1211 hard("No definition or id named %s found. Try /list without arguments.", arg); |
1148 } else { |
1212 } else { |
1149 hard("No definition or id named %s found. There are no active definitions.", arg); |
1213 hard("No definition or id named %s found. There are no active definitions.", arg); |
1150 } |
1214 } |
1151 return; |
1215 return false; |
1152 } |
1216 } |
1153 |
1217 |
1154 // prevent double newline on empty list |
1218 // prevent double newline on empty list |
1155 boolean[] hasOutput = new boolean[1]; |
1219 boolean[] hasOutput = new boolean[1]; |
1156 stream.forEachOrdered(sn -> { |
1220 stream.forEachOrdered(sn -> { |
1158 cmdout.println(); |
1222 cmdout.println(); |
1159 hasOutput[0] = true; |
1223 hasOutput[0] = true; |
1160 } |
1224 } |
1161 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
1225 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); |
1162 }); |
1226 }); |
1163 } |
1227 return true; |
1164 |
1228 } |
1165 private void cmdOpen(String filename) { |
1229 |
|
1230 private boolean cmdOpen(String filename) { |
1166 if (filename.isEmpty()) { |
1231 if (filename.isEmpty()) { |
1167 hard("The /open command requires a filename argument."); |
1232 hard("The /open command requires a filename argument."); |
|
1233 return false; |
1168 } else { |
1234 } else { |
1169 try { |
1235 try { |
1170 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); |
1236 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); |
1171 } catch (FileNotFoundException e) { |
1237 } catch (FileNotFoundException e) { |
1172 hard("File '%s' is not found: %s", filename, e.getMessage()); |
1238 hard("File '%s' is not found: %s", filename, e.getMessage()); |
|
1239 return false; |
1173 } catch (Exception e) { |
1240 } catch (Exception e) { |
1174 hard("Exception while reading file: %s", e); |
1241 hard("Exception while reading file: %s", e); |
1175 } |
1242 return false; |
1176 } |
1243 } |
1177 } |
1244 } |
1178 |
1245 return true; |
1179 private void cmdPrompt() { |
1246 } |
|
1247 |
|
1248 private boolean cmdPrompt() { |
1180 displayPrompt = !displayPrompt; |
1249 displayPrompt = !displayPrompt; |
1181 fluff("Prompt will %sdisplay. Use /prompt to toggle.", displayPrompt ? "" : "NOT "); |
1250 fluff("Prompt will %sdisplay. Use /prompt to toggle.", displayPrompt ? "" : "NOT "); |
1182 concise("Prompt: %s", displayPrompt ? "on" : "off"); |
1251 concise("Prompt: %s", displayPrompt ? "on" : "off"); |
1183 } |
1252 return true; |
1184 |
1253 } |
1185 private void cmdReset() { |
1254 |
|
1255 private boolean cmdReset() { |
1186 live = false; |
1256 live = false; |
1187 fluff("Resetting state."); |
1257 fluff("Resetting state."); |
1188 } |
1258 return true; |
1189 |
1259 } |
1190 private void cmdSave(String arg_filename) { |
1260 |
|
1261 private boolean cmdReload(String arg) { |
|
1262 Iterable<String> history = replayableHistory; |
|
1263 boolean echo = true; |
|
1264 if (arg.length() > 0) { |
|
1265 if ("restore".startsWith(arg)) { |
|
1266 if (replayableHistoryPrevious == null) { |
|
1267 hard("No previous history to restore\n", arg); |
|
1268 return false; |
|
1269 } |
|
1270 history = replayableHistoryPrevious; |
|
1271 } else if ("quiet".startsWith(arg)) { |
|
1272 echo = false; |
|
1273 } else { |
|
1274 hard("Invalid argument to reload command: %s\nUse 'restore', 'quiet', or no argument\n", arg); |
|
1275 return false; |
|
1276 } |
|
1277 } |
|
1278 fluff("Restarting and restoring %s.", |
|
1279 history == replayableHistoryPrevious |
|
1280 ? "from previous state" |
|
1281 : "state"); |
|
1282 resetState(); |
|
1283 run(new ReloadIOContext(history, |
|
1284 echo? cmdout : null)); |
|
1285 return true; |
|
1286 } |
|
1287 |
|
1288 private boolean cmdSave(String arg_filename) { |
1191 Matcher mat = HISTORY_ALL_START_FILENAME.matcher(arg_filename); |
1289 Matcher mat = HISTORY_ALL_START_FILENAME.matcher(arg_filename); |
1192 if (!mat.find()) { |
1290 if (!mat.find()) { |
1193 hard("Malformed argument to the /save command: %s", arg_filename); |
1291 hard("Malformed argument to the /save command: %s", arg_filename); |
1194 return; |
1292 return false; |
1195 } |
1293 } |
1196 boolean useHistory = false; |
1294 boolean useHistory = false; |
1197 String saveAll = ""; |
1295 String saveAll = ""; |
1198 boolean saveStart = false; |
1296 boolean saveStart = false; |
1199 String cmd = mat.group("cmd"); |
1297 String cmd = mat.group("cmd"); |
1209 break; |
1307 break; |
1210 } |
1308 } |
1211 String filename = mat.group("filename"); |
1309 String filename = mat.group("filename"); |
1212 if (filename == null ||filename.isEmpty()) { |
1310 if (filename == null ||filename.isEmpty()) { |
1213 hard("The /save command requires a filename argument."); |
1311 hard("The /save command requires a filename argument."); |
1214 return; |
1312 return false; |
1215 } |
1313 } |
1216 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), |
1314 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), |
1217 Charset.defaultCharset(), |
1315 Charset.defaultCharset(), |
1218 CREATE, TRUNCATE_EXISTING, WRITE)) { |
1316 CREATE, TRUNCATE_EXISTING, WRITE)) { |
1219 if (useHistory) { |
1317 if (useHistory) { |
1232 } |
1330 } |
1233 } |
1331 } |
1234 } |
1332 } |
1235 } catch (FileNotFoundException e) { |
1333 } catch (FileNotFoundException e) { |
1236 hard("File '%s' for save is not accessible: %s", filename, e.getMessage()); |
1334 hard("File '%s' for save is not accessible: %s", filename, e.getMessage()); |
|
1335 return false; |
1237 } catch (Exception e) { |
1336 } catch (Exception e) { |
1238 hard("Exception while saving: %s", e); |
1337 hard("Exception while saving: %s", e); |
1239 } |
1338 return false; |
1240 } |
1339 } |
1241 |
1340 return true; |
1242 private void cmdSetStart(String filename) { |
1341 } |
|
1342 |
|
1343 private boolean cmdSetStart(String filename) { |
1243 if (filename.isEmpty()) { |
1344 if (filename.isEmpty()) { |
1244 hard("The /setstart command requires a filename argument."); |
1345 hard("The /setstart command requires a filename argument."); |
1245 } else { |
1346 } else { |
1246 try { |
1347 try { |
1247 byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename)); |
1348 byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename)); |
1248 String init = new String(encoded); |
1349 String init = new String(encoded); |
1249 PREFS.put(STARTUP_KEY, init); |
1350 PREFS.put(STARTUP_KEY, init); |
1250 } catch (AccessDeniedException e) { |
1351 } catch (AccessDeniedException e) { |
1251 hard("File '%s' for /setstart is not accessible.", filename); |
1352 hard("File '%s' for /setstart is not accessible.", filename); |
|
1353 return false; |
1252 } catch (NoSuchFileException e) { |
1354 } catch (NoSuchFileException e) { |
1253 hard("File '%s' for /setstart is not found.", filename); |
1355 hard("File '%s' for /setstart is not found.", filename); |
|
1356 return false; |
1254 } catch (Exception e) { |
1357 } catch (Exception e) { |
1255 hard("Exception while reading start set file: %s", e); |
1358 hard("Exception while reading start set file: %s", e); |
1256 } |
1359 return false; |
1257 } |
1360 } |
1258 } |
1361 } |
1259 |
1362 return true; |
1260 private void cmdVars() { |
1363 } |
|
1364 |
|
1365 private boolean cmdVars() { |
1261 for (VarSnippet vk : state.variables()) { |
1366 for (VarSnippet vk : state.variables()) { |
1262 String val = state.status(vk) == Status.VALID |
1367 String val = state.status(vk) == Status.VALID |
1263 ? state.varValue(vk) |
1368 ? state.varValue(vk) |
1264 : "(not-active)"; |
1369 : "(not-active)"; |
1265 hard(" %s %s = %s", vk.typeName(), vk.name(), val); |
1370 hard(" %s %s = %s", vk.typeName(), vk.name(), val); |
1266 } |
1371 } |
1267 } |
1372 return true; |
1268 |
1373 } |
1269 private void cmdMethods() { |
1374 |
|
1375 private boolean cmdMethods() { |
1270 for (MethodSnippet mk : state.methods()) { |
1376 for (MethodSnippet mk : state.methods()) { |
1271 hard(" %s %s", mk.name(), mk.signature()); |
1377 hard(" %s %s", mk.name(), mk.signature()); |
1272 } |
1378 } |
1273 } |
1379 return true; |
1274 |
1380 } |
1275 private void cmdClasses() { |
1381 |
|
1382 private boolean cmdClasses() { |
1276 for (TypeDeclSnippet ck : state.types()) { |
1383 for (TypeDeclSnippet ck : state.types()) { |
1277 String kind; |
1384 String kind; |
1278 switch (ck.subKind()) { |
1385 switch (ck.subKind()) { |
1279 case INTERFACE_SUBKIND: |
1386 case INTERFACE_SUBKIND: |
1280 kind = "interface"; |
1387 kind = "interface"; |
1293 kind = "class"; |
1400 kind = "class"; |
1294 break; |
1401 break; |
1295 } |
1402 } |
1296 hard(" %s %s", kind, ck.name()); |
1403 hard(" %s %s", kind, ck.name()); |
1297 } |
1404 } |
1298 } |
1405 return true; |
1299 |
1406 } |
1300 private void cmdImports() { |
1407 |
|
1408 private boolean cmdImports() { |
1301 state.imports().forEach(ik -> { |
1409 state.imports().forEach(ik -> { |
1302 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); |
1410 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); |
1303 }); |
1411 }); |
1304 } |
1412 return true; |
1305 |
1413 } |
1306 private void cmdUseHistoryEntry(int index) { |
1414 |
|
1415 private boolean cmdUseHistoryEntry(int index) { |
1307 List<Snippet> keys = state.snippets(); |
1416 List<Snippet> keys = state.snippets(); |
1308 if (index < 0) |
1417 if (index < 0) |
1309 index += keys.size(); |
1418 index += keys.size(); |
1310 else |
1419 else |
1311 index--; |
1420 index--; |
1312 if (index >= 0 && index < keys.size()) { |
1421 if (index >= 0 && index < keys.size()) { |
1313 rerunSnippet(keys.get(index)); |
1422 rerunSnippet(keys.get(index)); |
1314 } else { |
1423 } else { |
1315 hard("Cannot find snippet %d", index + 1); |
1424 hard("Cannot find snippet %d", index + 1); |
1316 } |
1425 return false; |
|
1426 } |
|
1427 return true; |
1317 } |
1428 } |
1318 |
1429 |
1319 private boolean rerunHistoryEntryById(String id) { |
1430 private boolean rerunHistoryEntryById(String id) { |
1320 Optional<Snippet> snippet = state.snippets().stream() |
1431 Optional<Snippet> snippet = state.snippets().stream() |
1321 .filter(s -> s.id().equals(id)) |
1432 .filter(s -> s.id().equals(id)) |
1423 } |
1534 } |
1424 //where |
1535 //where |
1425 private boolean processCompleteSource(String source) throws IllegalStateException { |
1536 private boolean processCompleteSource(String source) throws IllegalStateException { |
1426 debug("Compiling: %s", source); |
1537 debug("Compiling: %s", source); |
1427 boolean failed = false; |
1538 boolean failed = false; |
|
1539 boolean isActive = false; |
1428 List<SnippetEvent> events = state.eval(source); |
1540 List<SnippetEvent> events = state.eval(source); |
1429 for (SnippetEvent e : events) { |
1541 for (SnippetEvent e : events) { |
|
1542 // Report the event, recording failure |
1430 failed |= handleEvent(e); |
1543 failed |= handleEvent(e); |
1431 } |
1544 |
|
1545 // If any main snippet is active, this should be replayable |
|
1546 // also ignore var value queries |
|
1547 isActive |= e.causeSnippet() == null && |
|
1548 e.status().isActive && |
|
1549 e.snippet().subKind() != VAR_VALUE_SUBKIND; |
|
1550 } |
|
1551 // If this is an active snippet and it didn't cause the backend to die, |
|
1552 // add it to the replayable history |
|
1553 if (isActive && live) { |
|
1554 addToReplayHistory(source); |
|
1555 } |
|
1556 |
1432 return failed; |
1557 return failed; |
1433 } |
1558 } |
1434 |
1559 |
1435 private boolean handleEvent(SnippetEvent ste) { |
1560 private boolean handleEvent(SnippetEvent ste) { |
1436 Snippet sn = ste.snippet(); |
1561 Snippet sn = ste.snippet(); |
1782 this.tid = tid; |
1907 this.tid = tid; |
1783 } |
1908 } |
1784 } |
1909 } |
1785 } |
1910 } |
1786 |
1911 |
1787 class ScannerIOContext extends IOContext { |
1912 abstract class NonInteractiveIOContext extends IOContext { |
1788 |
1913 |
|
1914 @Override |
|
1915 public boolean interactiveOutput() { |
|
1916 return false; |
|
1917 } |
|
1918 |
|
1919 @Override |
|
1920 public Iterable<String> currentSessionHistory() { |
|
1921 return Collections.emptyList(); |
|
1922 } |
|
1923 |
|
1924 @Override |
|
1925 public boolean terminalEditorRunning() { |
|
1926 return false; |
|
1927 } |
|
1928 |
|
1929 @Override |
|
1930 public void suspend() { |
|
1931 } |
|
1932 |
|
1933 @Override |
|
1934 public void resume() { |
|
1935 } |
|
1936 |
|
1937 @Override |
|
1938 public void beforeUserCode() { |
|
1939 } |
|
1940 |
|
1941 @Override |
|
1942 public void afterUserCode() { |
|
1943 } |
|
1944 |
|
1945 @Override |
|
1946 public void replaceLastHistoryEntry(String source) { |
|
1947 } |
|
1948 } |
|
1949 |
|
1950 class ScannerIOContext extends NonInteractiveIOContext { |
1789 private final Scanner scannerIn; |
1951 private final Scanner scannerIn; |
1790 private final PrintStream pStream; |
1952 |
1791 |
1953 ScannerIOContext(Scanner scannerIn) { |
1792 public ScannerIOContext(Scanner scannerIn, PrintStream pStream) { |
|
1793 this.scannerIn = scannerIn; |
1954 this.scannerIn = scannerIn; |
1794 this.pStream = pStream; |
|
1795 } |
1955 } |
1796 |
1956 |
1797 @Override |
1957 @Override |
1798 public String readLine(String prompt, String prefix) { |
1958 public String readLine(String prompt, String prefix) { |
1799 if (pStream != null && prompt != null) { |
|
1800 pStream.print(prompt); |
|
1801 } |
|
1802 if (scannerIn.hasNextLine()) { |
1959 if (scannerIn.hasNextLine()) { |
1803 return scannerIn.nextLine(); |
1960 return scannerIn.nextLine(); |
1804 } else { |
1961 } else { |
1805 return null; |
1962 return null; |
1806 } |
1963 } |
1807 } |
1964 } |
1808 |
1965 |
1809 @Override |
1966 @Override |
1810 public boolean interactiveOutput() { |
1967 public void close() { |
1811 return true; |
1968 scannerIn.close(); |
|
1969 } |
|
1970 } |
|
1971 |
|
1972 class FileScannerIOContext extends ScannerIOContext { |
|
1973 |
|
1974 FileScannerIOContext(String fn) throws FileNotFoundException { |
|
1975 this(new FileReader(fn)); |
|
1976 } |
|
1977 |
|
1978 FileScannerIOContext(Reader rdr) throws FileNotFoundException { |
|
1979 super(new Scanner(rdr)); |
|
1980 } |
|
1981 } |
|
1982 |
|
1983 class ReloadIOContext extends NonInteractiveIOContext { |
|
1984 private final Iterator<String> it; |
|
1985 private final PrintStream echoStream; |
|
1986 |
|
1987 ReloadIOContext(Iterable<String> history, PrintStream echoStream) { |
|
1988 this.it = history.iterator(); |
|
1989 this.echoStream = echoStream; |
1812 } |
1990 } |
1813 |
1991 |
1814 @Override |
1992 @Override |
1815 public Iterable<String> currentSessionHistory() { |
1993 public String readLine(String prompt, String prefix) { |
1816 return Collections.emptyList(); |
1994 String s = it.hasNext() |
|
1995 ? it.next() |
|
1996 : null; |
|
1997 if (echoStream != null && s != null) { |
|
1998 String p = "-: "; |
|
1999 String p2 = "\n "; |
|
2000 echoStream.printf("%s%s\n", p, s.replace("\n", p2)); |
|
2001 } |
|
2002 return s; |
1817 } |
2003 } |
1818 |
2004 |
1819 @Override |
2005 @Override |
1820 public void close() { |
2006 public void close() { |
1821 scannerIn.close(); |
|
1822 } |
|
1823 |
|
1824 @Override |
|
1825 public boolean terminalEditorRunning() { |
|
1826 return false; |
|
1827 } |
|
1828 |
|
1829 @Override |
|
1830 public void suspend() { |
|
1831 } |
|
1832 |
|
1833 @Override |
|
1834 public void resume() { |
|
1835 } |
|
1836 |
|
1837 @Override |
|
1838 public void beforeUserCode() { |
|
1839 } |
|
1840 |
|
1841 @Override |
|
1842 public void afterUserCode() { |
|
1843 } |
|
1844 |
|
1845 @Override |
|
1846 public void replaceLastHistoryEntry(String source) { |
|
1847 } |
2007 } |
1848 } |
2008 } |
1849 |
|
1850 class FileScannerIOContext extends ScannerIOContext { |
|
1851 |
|
1852 public FileScannerIOContext(String fn) throws FileNotFoundException { |
|
1853 this(new FileReader(fn)); |
|
1854 } |
|
1855 |
|
1856 public FileScannerIOContext(Reader rdr) throws FileNotFoundException { |
|
1857 super(new Scanner(rdr), null); |
|
1858 } |
|
1859 |
|
1860 @Override |
|
1861 public boolean interactiveOutput() { |
|
1862 return false; |
|
1863 } |
|
1864 } |
|
1865 |
|