JShellTool.java revision 3062:15bdc18525ff
1/*
2 * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.internal.jshell.tool;
27
28import java.io.BufferedWriter;
29import java.io.ByteArrayInputStream;
30import java.io.File;
31import java.io.FileNotFoundException;
32import java.io.FileReader;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.PrintStream;
36import java.io.Reader;
37import java.io.StringReader;
38import java.nio.charset.Charset;
39import java.nio.file.AccessDeniedException;
40import java.nio.file.FileSystems;
41import java.nio.file.Files;
42import java.nio.file.NoSuchFileException;
43import java.nio.file.Path;
44import java.nio.file.Paths;
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Collections;
48import java.util.Iterator;
49import java.util.LinkedHashMap;
50import java.util.LinkedHashSet;
51import java.util.List;
52import java.util.Map;
53import java.util.Map.Entry;
54import java.util.Scanner;
55import java.util.Set;
56import java.util.function.Consumer;
57import java.util.function.Predicate;
58import java.util.prefs.Preferences;
59import java.util.regex.Matcher;
60import java.util.regex.Pattern;
61import java.util.stream.Collectors;
62import java.util.stream.Stream;
63import java.util.stream.StreamSupport;
64
65import jdk.internal.jshell.debug.InternalDebugControl;
66import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
67import jdk.jshell.Diag;
68import jdk.jshell.EvalException;
69import jdk.jshell.JShell;
70import jdk.jshell.Snippet;
71import jdk.jshell.DeclarationSnippet;
72import jdk.jshell.TypeDeclSnippet;
73import jdk.jshell.MethodSnippet;
74import jdk.jshell.PersistentSnippet;
75import jdk.jshell.VarSnippet;
76import jdk.jshell.ExpressionSnippet;
77import jdk.jshell.Snippet.Status;
78import jdk.jshell.SourceCodeAnalysis;
79import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
80import jdk.jshell.SourceCodeAnalysis.Suggestion;
81import jdk.jshell.SnippetEvent;
82import jdk.jshell.UnresolvedReferenceException;
83import jdk.jshell.Snippet.SubKind;
84import jdk.jshell.JShell.Subscription;
85
86import static java.nio.file.StandardOpenOption.CREATE;
87import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
88import static java.nio.file.StandardOpenOption.WRITE;
89import java.util.MissingResourceException;
90import java.util.ResourceBundle;
91import static java.util.stream.Collectors.toList;
92
93/**
94 * Command line REPL tool for Java using the JShell API.
95 * @author Robert Field
96 */
97public class JShellTool {
98
99    private static final Pattern LINEBREAK = Pattern.compile("\\R");
100    private static final Pattern HISTORY_ALL_FILENAME = Pattern.compile(
101            "((?<cmd>(all|history))(\\z|\\p{javaWhitespace}+))?(?<filename>.*)");
102
103    final InputStream cmdin;
104    final PrintStream cmdout;
105    final PrintStream cmderr;
106    final PrintStream console;
107    final InputStream userin;
108    final PrintStream userout;
109    final PrintStream usererr;
110
111    /**
112     * The constructor for the tool (used by tool launch via main and by test
113     * harnesses to capture ins and outs.
114     * @param cmdin command line input -- snippets and commands
115     * @param cmdout command line output, feedback including errors
116     * @param cmderr start-up errors and debugging info
117     * @param console console control interaction
118     * @param userin code execution input (not yet functional)
119     * @param userout code execution output  -- System.out.printf("hi")
120     * @param usererr code execution error stream  -- System.err.printf("Oops")
121     */
122    public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
123            PrintStream console,
124            InputStream userin, PrintStream userout, PrintStream usererr) {
125        this.cmdin = cmdin;
126        this.cmdout = cmdout;
127        this.cmderr = cmderr;
128        this.console = console;
129        this.userin = userin;
130        this.userout = userout;
131        this.usererr = usererr;
132    }
133
134    private IOContext input = null;
135    private boolean regenerateOnDeath = true;
136    private boolean live = false;
137
138    SourceCodeAnalysis analysis;
139    JShell state = null;
140    Subscription shutdownSubscription = null;
141
142    private boolean debug = false;
143    private boolean displayPrompt = true;
144    public boolean testPrompt = false;
145    private Feedback feedback = Feedback.Default;
146    private String cmdlineClasspath = null;
147    private String cmdlineStartup = null;
148    private String editor = null;
149
150    static final Preferences PREFS = Preferences.userRoot().node("tool/REPL");
151
152    static final String STARTUP_KEY = "STARTUP";
153
154    static final String DEFAULT_STARTUP =
155            "\n" +
156            "import java.util.*;\n" +
157            "import java.io.*;\n" +
158            "import java.math.*;\n" +
159            "import java.net.*;\n" +
160            "import java.util.concurrent.*;\n" +
161            "import java.util.prefs.*;\n" +
162            "import java.util.regex.*;\n" +
163            "void printf(String format, Object... args) { System.out.printf(format, args); }\n";
164
165    // Tool id (tid) mapping
166    NameSpace mainNamespace;
167    NameSpace startNamespace;
168    NameSpace errorNamespace;
169    NameSpace currentNameSpace;
170    Map<Snippet,SnippetInfo> mapSnippet;
171
172    void debug(String format, Object... args) {
173        if (debug) {
174            cmderr.printf(format + "\n", args);
175        }
176    }
177
178    /**
179     * For more verbose feedback modes
180     * @param format printf format
181     * @param args printf args
182     */
183    void fluff(String format, Object... args) {
184        if (feedback() != Feedback.Off && feedback() != Feedback.Concise) {
185            hard(format, args);
186        }
187    }
188
189    /**
190     * For concise feedback mode only
191     * @param format printf format
192     * @param args printf args
193     */
194    void concise(String format, Object... args) {
195        if (feedback() == Feedback.Concise) {
196            hard(format, args);
197        }
198    }
199
200    /**
201     * For all feedback modes -- must show
202     * @param format printf format
203     * @param args printf args
204     */
205    void hard(String format, Object... args) {
206        cmdout.printf("|  " + format + "\n", args);
207    }
208
209    /**
210     * Trim whitespace off end of string
211     * @param s
212     * @return
213     */
214    static String trimEnd(String s) {
215        int last = s.length() - 1;
216        int i = last;
217        while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
218            --i;
219        }
220        if (i != last) {
221            return s.substring(0, i + 1);
222        } else {
223            return s;
224        }
225    }
226
227    /**
228     * Normal start entry point
229     * @param args
230     * @throws Exception
231     */
232    public static void main(String[] args) throws Exception {
233        new JShellTool(System.in, System.out, System.err, System.out,
234                 new ByteArrayInputStream(new byte[0]), System.out, System.err)
235                .start(args);
236    }
237
238    public void start(String[] args) throws Exception {
239        List<String> loadList = processCommandArgs(args);
240        if (loadList == null) {
241            // Abort
242            return;
243        }
244        try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
245            start(in, loadList);
246        }
247    }
248
249    private void start(IOContext in, List<String> loadList) {
250        resetState(); // Initialize
251
252        for (String loadFile : loadList) {
253            cmdOpen(loadFile);
254        }
255
256        if (regenerateOnDeath) {
257            fluff("Welcome to JShell -- Version %s", version());
258            fluff("Type /help for help");
259        }
260
261        try {
262            while (regenerateOnDeath) {
263                if (!live) {
264                    resetState();
265                }
266                run(in);
267            }
268        } finally {
269            closeState();
270        }
271    }
272
273    /**
274     * Process the command line arguments.
275     * Set options.
276     * @param args the command line arguments
277     * @return the list of files to be loaded
278     */
279    private List<String> processCommandArgs(String[] args) {
280        List<String> loadList = new ArrayList<>();
281        Iterator<String> ai = Arrays.asList(args).iterator();
282        while (ai.hasNext()) {
283            String arg = ai.next();
284            if (arg.startsWith("-")) {
285                switch (arg) {
286                    case "-classpath":
287                    case "-cp":
288                        if (cmdlineClasspath != null) {
289                            cmderr.printf("Conflicting -classpath option.\n");
290                            return null;
291                        }
292                        if (ai.hasNext()) {
293                            cmdlineClasspath = ai.next();
294                        } else {
295                            cmderr.printf("Argument to -classpath missing.\n");
296                            return null;
297                        }
298                        break;
299                    case "-help":
300                        printUsage();
301                        return null;
302                    case "-version":
303                        cmdout.printf("jshell %s\n", version());
304                        return null;
305                    case "-fullversion":
306                        cmdout.printf("jshell %s\n", fullVersion());
307                        return null;
308                    case "-startup":
309                        if (cmdlineStartup != null) {
310                            cmderr.printf("Conflicting -startup or -nostartup option.\n");
311                            return null;
312                        }
313                        if (ai.hasNext()) {
314                            String filename = ai.next();
315                            try {
316                                byte[] encoded = Files.readAllBytes(Paths.get(filename));
317                                cmdlineStartup = new String(encoded);
318                            } catch (AccessDeniedException e) {
319                                hard("File '%s' for start-up is not accessible.", filename);
320                            } catch (NoSuchFileException e) {
321                                hard("File '%s' for start-up is not found.", filename);
322                            } catch (Exception e) {
323                                hard("Exception while reading start-up file: %s", e);
324                            }
325                        } else {
326                            cmderr.printf("Argument to -startup missing.\n");
327                            return null;
328                        }
329                        break;
330                    case "-nostartup":
331                        if (cmdlineStartup != null && !cmdlineStartup.isEmpty()) {
332                            cmderr.printf("Conflicting -startup option.\n");
333                            return null;
334                        }
335                        cmdlineStartup = "";
336                        break;
337                    default:
338                        cmderr.printf("Unknown option: %s\n", arg);
339                        printUsage();
340                        return null;
341                }
342            } else {
343                loadList.add(arg);
344            }
345        }
346        return loadList;
347    }
348
349    private void printUsage() {
350        cmdout.printf("Usage:   jshell <options> <load files>\n");
351        cmdout.printf("where possible options include:\n");
352        cmdout.printf("  -classpath <path>          Specify where to find user class files\n");
353        cmdout.printf("  -cp <path>                 Specify where to find user class files\n");
354        cmdout.printf("  -startup <file>            One run replacement for the start-up definitions\n");
355        cmdout.printf("  -nostartup                 Do not run the start-up definitions\n");
356        cmdout.printf("  -help                      Print a synopsis of standard options\n");
357        cmdout.printf("  -version                   Version information\n");
358    }
359
360    private void resetState() {
361        closeState();
362
363        // Initialize tool id mapping
364        mainNamespace = new NameSpace("main", "");
365        startNamespace = new NameSpace("start", "s");
366        errorNamespace = new NameSpace("error", "e");
367        mapSnippet = new LinkedHashMap<>();
368        currentNameSpace = startNamespace;
369
370        state = JShell.builder()
371                .in(userin)
372                .out(userout)
373                .err(usererr)
374                .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext())
375                .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive)
376                        ? currentNameSpace.tid(sn)
377                        : errorNamespace.tid(sn))
378                .build();
379        analysis = state.sourceCodeAnalysis();
380        shutdownSubscription = state.onShutdown((JShell deadState) -> {
381            if (deadState == state) {
382                hard("State engine terminated.  See /history");
383                live = false;
384            }
385        });
386        live = true;
387
388        if (cmdlineClasspath != null) {
389            state.addToClasspath(cmdlineClasspath);
390        }
391
392
393        String start;
394        if (cmdlineStartup == null) {
395            start = PREFS.get(STARTUP_KEY, "<nada>");
396            if (start.equals("<nada>")) {
397                start = DEFAULT_STARTUP;
398                PREFS.put(STARTUP_KEY, DEFAULT_STARTUP);
399            }
400        } else {
401            start = cmdlineStartup;
402        }
403        try (IOContext suin = new FileScannerIOContext(new StringReader(start))) {
404            run(suin);
405        } catch (Exception ex) {
406            hard("Unexpected exception reading start-up: %s\n", ex);
407        }
408        currentNameSpace = mainNamespace;
409    }
410
411    private void closeState() {
412        live = false;
413        JShell oldState = state;
414        if (oldState != null) {
415            oldState.unsubscribe(shutdownSubscription); // No notification
416            oldState.close();
417        }
418    }
419
420    /**
421     * Main loop
422     * @param in the line input/editing context
423     */
424    private void run(IOContext in) {
425        IOContext oldInput = input;
426        input = in;
427        try {
428            String incomplete = "";
429            while (live) {
430                String prompt;
431                if (in.interactiveOutput() && displayPrompt) {
432                    prompt = testPrompt
433                                    ? incomplete.isEmpty()
434                                            ? "\u0005" //ENQ
435                                            : "\u0006" //ACK
436                                    : incomplete.isEmpty()
437                                            ? feedback() == Feedback.Concise
438                                                    ? "-> "
439                                                    : "\n-> "
440                                            : ">> "
441                    ;
442                } else {
443                    prompt = "";
444                }
445                String raw;
446                try {
447                    raw = in.readLine(prompt, incomplete);
448                } catch (InputInterruptedException ex) {
449                    //input interrupted - clearing current state
450                    incomplete = "";
451                    continue;
452                }
453                if (raw == null) {
454                    //EOF
455                    if (in.interactiveOutput()) {
456                        // End after user ctrl-D
457                        regenerateOnDeath = false;
458                    }
459                    break;
460                }
461                String trimmed = trimEnd(raw);
462                if (!trimmed.isEmpty()) {
463                    String line = incomplete + trimmed;
464
465                    // No commands in the middle of unprocessed source
466                    if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) {
467                        processCommand(line.trim());
468                    } else {
469                        incomplete = processSourceCatchingReset(line);
470                    }
471                }
472            }
473        } catch (IOException ex) {
474            hard("Unexpected exception: %s\n", ex);
475        } finally {
476            input = oldInput;
477        }
478    }
479
480    private String processSourceCatchingReset(String src) {
481        try {
482            input.beforeUserCode();
483            return processSource(src);
484        } catch (IllegalStateException ex) {
485            hard("Resetting...");
486            live = false; // Make double sure
487            return "";
488        } finally {
489            input.afterUserCode();
490        }
491    }
492
493    private void processCommand(String cmd) {
494        try {
495            //handle "/[number]"
496            cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1)));
497            return ;
498        } catch (NumberFormatException ex) {
499            //ignore
500        }
501        String arg = "";
502        int idx = cmd.indexOf(' ');
503        if (idx > 0) {
504            arg = cmd.substring(idx + 1).trim();
505            cmd = cmd.substring(0, idx);
506        }
507        Command command = commands.get(cmd);
508        if (command == null || command.kind == CommandKind.HELP_ONLY) {
509            hard("No such command: %s", cmd);
510            fluff("Type /help for help.");
511        } else {
512            command.run.accept(arg);
513        }
514    }
515
516    private static Path toPathResolvingUserHome(String pathString) {
517        if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
518            return Paths.get(System.getProperty("user.home"), pathString.substring(2));
519        else
520            return Paths.get(pathString);
521    }
522
523    static final class Command {
524        public final String[] aliases;
525        public final String params;
526        public final String description;
527        public final Consumer<String> run;
528        public final CompletionProvider completions;
529        public final CommandKind kind;
530
531        public Command(String command, String alias, String params, String description, Consumer<String> run, CompletionProvider completions) {
532            this(command, alias, params, description, run, completions, CommandKind.NORMAL);
533        }
534
535        public Command(String command, String alias, String params, String description, Consumer<String> run, CompletionProvider completions, CommandKind kind) {
536            this.aliases = alias != null ? new String[] {command, alias} : new String[] {command};
537            this.params = params;
538            this.description = description;
539            this.run = run;
540            this.completions = completions;
541            this.kind = kind;
542        }
543
544    }
545
546    interface CompletionProvider {
547        List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
548    }
549
550    enum CommandKind {
551        NORMAL,
552        HIDDEN,
553        HELP_ONLY;
554    }
555
556    static final class FixedCompletionProvider implements CompletionProvider {
557
558        private final String[] alternatives;
559
560        public FixedCompletionProvider(String... alternatives) {
561            this.alternatives = alternatives;
562        }
563
564        @Override
565        public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
566            List<Suggestion> result = new ArrayList<>();
567
568            for (String alternative : alternatives) {
569                if (alternative.startsWith(input)) {
570                    result.add(new Suggestion(alternative, false));
571                }
572            }
573
574            anchor[0] = 0;
575
576            return result;
577        }
578
579    }
580
581    private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
582    private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
583    private final Map<String, Command> commands = new LinkedHashMap<>();
584    private void registerCommand(Command cmd) {
585        for (String str : cmd.aliases) {
586            commands.put(str, cmd);
587        }
588    }
589    private static CompletionProvider fileCompletions(Predicate<Path> accept) {
590        return (code, cursor, anchor) -> {
591            int lastSlash = code.lastIndexOf('/');
592            String path = code.substring(0, lastSlash + 1);
593            String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
594            Path current = toPathResolvingUserHome(path);
595            List<Suggestion> result = new ArrayList<>();
596            try (Stream<Path> dir = Files.list(current)) {
597                dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
598                   .map(f -> new Suggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""), false))
599                   .forEach(result::add);
600            } catch (IOException ex) {
601                //ignore...
602            }
603            if (path.isEmpty()) {
604                StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
605                             .filter(root -> accept.test(root) && root.toString().startsWith(prefix))
606                             .map(root -> new Suggestion(root.toString(), false))
607                             .forEach(result::add);
608            }
609            anchor[0] = path.length();
610            return result;
611        };
612    }
613
614    private static CompletionProvider classPathCompletion() {
615        return fileCompletions(p -> Files.isDirectory(p) ||
616                                    p.getFileName().toString().endsWith(".zip") ||
617                                    p.getFileName().toString().endsWith(".jar"));
618    }
619
620    private CompletionProvider editCompletion() {
621        return (prefix, cursor, anchor) -> {
622            anchor[0] = 0;
623            return state.snippets()
624                        .stream()
625                        .flatMap(k -> (k instanceof DeclarationSnippet)
626                                ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name())
627                                : Stream.of(String.valueOf(k.id())))
628                        .filter(k -> k.startsWith(prefix))
629                        .map(k -> new Suggestion(k, false))
630                        .collect(Collectors.toList());
631        };
632    }
633
634    private static CompletionProvider saveCompletion() {
635        CompletionProvider keyCompletion = new FixedCompletionProvider("all ", "history ");
636        return (code, cursor, anchor) -> {
637            List<Suggestion> result = new ArrayList<>();
638            int space = code.indexOf(' ');
639            if (space == (-1)) {
640                result.addAll(keyCompletion.completionSuggestions(code, cursor, anchor));
641            }
642            result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
643            anchor[0] += space + 1;
644            return result;
645        };
646    }
647
648    // Table of commands -- with command forms, argument kinds, help message, implementation, ...
649
650    {
651        registerCommand(new Command("/list", "/l", "[all]", "list the source you have typed",
652                                    arg -> cmdList(arg),
653                                    new FixedCompletionProvider("all")));
654        registerCommand(new Command("/seteditor", null, "<executable>", "set the external editor command to use",
655                                    arg -> cmdSetEditor(arg),
656                                    EMPTY_COMPLETION_PROVIDER));
657        registerCommand(new Command("/edit", "/e", "<name or id>", "edit a source entry referenced by name or id",
658                                    arg -> cmdEdit(arg),
659                                    editCompletion()));
660        registerCommand(new Command("/drop", "/d", "<name or id>", "delete a source entry referenced by name or id",
661                                    arg -> cmdDrop(arg),
662                                    editCompletion()));
663        registerCommand(new Command("/save", "/s", "[all|history] <file>", "save the source you have typed",
664                                    arg -> cmdSave(arg),
665                                    saveCompletion()));
666        registerCommand(new Command("/open", "/o", "<file>", "open a file as source input",
667                                    arg -> cmdOpen(arg),
668                                    FILE_COMPLETION_PROVIDER));
669        registerCommand(new Command("/vars", "/v", null, "list the declared variables and their values",
670                                    arg -> cmdVars(),
671                                    EMPTY_COMPLETION_PROVIDER));
672        registerCommand(new Command("/methods", "/m", null, "list the declared methods and their signatures",
673                                    arg -> cmdMethods(),
674                                    EMPTY_COMPLETION_PROVIDER));
675        registerCommand(new Command("/classes", "/c", null, "list the declared classes",
676                                    arg -> cmdClasses(),
677                                    EMPTY_COMPLETION_PROVIDER));
678        registerCommand(new Command("/exit", "/x", null, "exit the REPL",
679                                    arg -> cmdExit(),
680                                    EMPTY_COMPLETION_PROVIDER));
681        registerCommand(new Command("/reset", "/r", null, "reset everything in the REPL",
682                                    arg -> cmdReset(),
683                                    EMPTY_COMPLETION_PROVIDER));
684        registerCommand(new Command("/feedback", "/f", "<level>", "feedback information: off, concise, normal, verbose, default, or ?",
685                                    arg -> cmdFeedback(arg),
686                                    new FixedCompletionProvider("off", "concise", "normal", "verbose", "default", "?")));
687        registerCommand(new Command("/prompt", "/p", null, "toggle display of a prompt",
688                                    arg -> cmdPrompt(),
689                                    EMPTY_COMPLETION_PROVIDER));
690        registerCommand(new Command("/classpath", "/cp", "<path>", "add a path to the classpath",
691                                    arg -> cmdClasspath(arg),
692                                    classPathCompletion()));
693        registerCommand(new Command("/history", "/h", null, "history of what you have typed",
694                                    arg -> cmdHistory(),
695                                    EMPTY_COMPLETION_PROVIDER));
696        registerCommand(new Command("/setstart", null, "<file>", "read file and set as the new start-up definitions",
697                                    arg -> cmdSetStart(arg),
698                                    FILE_COMPLETION_PROVIDER));
699        registerCommand(new Command("/savestart", null, "<file>", "save the default start-up definitions to the file",
700                                    arg -> cmdSaveStart(arg),
701                                    FILE_COMPLETION_PROVIDER));
702        registerCommand(new Command("/debug", "/db", "", "toggle debugging of the REPL",
703                                    arg -> cmdDebug(arg),
704                                    EMPTY_COMPLETION_PROVIDER,
705                                    CommandKind.HIDDEN));
706        registerCommand(new Command("/help", "/?", "", "this help message",
707                                    arg -> cmdHelp(),
708                                    EMPTY_COMPLETION_PROVIDER));
709        registerCommand(new Command("/!", null, "", "re-run last snippet",
710                                    arg -> cmdUseHistoryEntry(-1),
711                                    EMPTY_COMPLETION_PROVIDER));
712        registerCommand(new Command("/<n>", null, "", "re-run n-th snippet",
713                                    arg -> { throw new IllegalStateException(); },
714                                    EMPTY_COMPLETION_PROVIDER,
715                                    CommandKind.HELP_ONLY));
716        registerCommand(new Command("/-<n>", null, "", "re-run n-th previous snippet",
717                                    arg -> { throw new IllegalStateException(); },
718                                    EMPTY_COMPLETION_PROVIDER,
719                                    CommandKind.HELP_ONLY));
720    }
721
722    public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
723        String prefix = code.substring(0, cursor);
724        int space = prefix.indexOf(' ');
725        Stream<Suggestion> result;
726
727        if (space == (-1)) {
728            result = commands.values()
729                             .stream()
730                             .distinct()
731                             .filter(cmd -> cmd.kind != CommandKind.HIDDEN && cmd.kind != CommandKind.HELP_ONLY)
732                             .map(cmd -> cmd.aliases[0])
733                             .filter(key -> key.startsWith(prefix))
734                             .map(key -> new Suggestion(key + " ", false));
735            anchor[0] = 0;
736        } else {
737            String arg = prefix.substring(space + 1);
738            String cmd = prefix.substring(0, space);
739            Command command = commands.get(cmd);
740            if (command != null) {
741                result = command.completions.completionSuggestions(arg, cursor - space, anchor).stream();
742                anchor[0] += space + 1;
743            } else {
744                result = Stream.empty();
745            }
746        }
747
748        return result.sorted((s1, s2) -> s1.continuation.compareTo(s2.continuation))
749                     .collect(Collectors.toList());
750    }
751
752    public String commandDocumentation(String code, int cursor) {
753        code = code.substring(0, cursor);
754        int space = code.indexOf(' ');
755
756        if (space != (-1)) {
757            String cmd = code.substring(0, space);
758            Command command = commands.get(cmd);
759            if (command != null) {
760                return command.description;
761            }
762        }
763
764        return null;
765    }
766
767    // --- Command implementations ---
768
769    void cmdSetEditor(String arg) {
770        if (arg.isEmpty()) {
771            hard("/seteditor requires a path argument");
772        } else {
773            editor = arg;
774            fluff("Editor set to: %s", arg);
775        }
776    }
777
778    void cmdClasspath(String arg) {
779        if (arg.isEmpty()) {
780            hard("/classpath requires a path argument");
781        } else {
782            state.addToClasspath(toPathResolvingUserHome(arg).toString());
783            fluff("Path %s added to classpath", arg);
784        }
785    }
786
787    void cmdDebug(String arg) {
788        if (arg.isEmpty()) {
789            debug = !debug;
790            InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0);
791            fluff("Debugging %s", debug ? "on" : "off");
792        } else {
793            int flags = 0;
794            for (char ch : arg.toCharArray()) {
795                switch (ch) {
796                    case '0':
797                        flags = 0;
798                        debug = false;
799                        fluff("Debugging off");
800                        break;
801                    case 'r':
802                        debug = true;
803                        fluff("REPL tool debugging on");
804                        break;
805                    case 'g':
806                        flags |= InternalDebugControl.DBG_GEN;
807                        fluff("General debugging on");
808                        break;
809                    case 'f':
810                        flags |= InternalDebugControl.DBG_FMGR;
811                        fluff("File manager debugging on");
812                        break;
813                    case 'c':
814                        flags |= InternalDebugControl.DBG_COMPA;
815                        fluff("Completion analysis debugging on");
816                        break;
817                    case 'd':
818                        flags |= InternalDebugControl.DBG_DEP;
819                        fluff("Dependency debugging on");
820                        break;
821                    case 'e':
822                        flags |= InternalDebugControl.DBG_EVNT;
823                        fluff("Event debugging on");
824                        break;
825                    default:
826                        hard("Unknown debugging option: %c", ch);
827                        fluff("Use: 0 r g f c d");
828                        break;
829                }
830            }
831            InternalDebugControl.setDebugFlags(state, flags);
832        }
833    }
834
835    private void cmdExit() {
836        regenerateOnDeath = false;
837        live = false;
838        fluff("Goodbye\n");
839    }
840
841    private void cmdFeedback(String arg) {
842        switch (arg) {
843            case "":
844            case "d":
845            case "default":
846                feedback = Feedback.Default;
847                break;
848            case "o":
849            case "off":
850                feedback = Feedback.Off;
851                break;
852            case "c":
853            case "concise":
854                feedback = Feedback.Concise;
855                break;
856            case "n":
857            case "normal":
858                feedback = Feedback.Normal;
859                break;
860            case "v":
861            case "verbose":
862                feedback = Feedback.Verbose;
863                break;
864            default:
865                hard("Follow /feedback with of the following:");
866                hard("  off       (errors and critical output only)");
867                hard("  concise");
868                hard("  normal");
869                hard("  verbose");
870                hard("  default");
871                hard("You may also use just the first letter, for example: /f c");
872                hard("In interactive mode 'default' is the same as 'normal', from a file it is the same as 'off'");
873                return;
874        }
875        fluff("Feedback mode: %s", feedback.name().toLowerCase());
876    }
877
878    void cmdHelp() {
879        int synopsisLen = 0;
880        Map<String, String> synopsis2Description = new LinkedHashMap<>();
881        for (Command cmd : new LinkedHashSet<>(commands.values())) {
882            if (cmd.kind == CommandKind.HIDDEN)
883                continue;
884            StringBuilder synopsis = new StringBuilder();
885            if (cmd.aliases.length > 1) {
886                synopsis.append(String.format("%-3s or ", cmd.aliases[1]));
887            } else {
888                synopsis.append("       ");
889            }
890            synopsis.append(cmd.aliases[0]);
891            if (cmd.params != null)
892                synopsis.append(" ").append(cmd.params);
893            synopsis2Description.put(synopsis.toString(), cmd.description);
894            synopsisLen = Math.max(synopsisLen, synopsis.length());
895        }
896        cmdout.println("Type a Java language expression, statement, or declaration.");
897        cmdout.println("Or type one of the following commands:\n");
898        for (Entry<String, String> e : synopsis2Description.entrySet()) {
899            cmdout.print(String.format("%-" + synopsisLen + "s", e.getKey()));
900            cmdout.print(" -- ");
901            cmdout.println(e.getValue());
902        }
903        cmdout.println();
904        cmdout.println("Supported shortcuts include:");
905        cmdout.println("<tab>       -- show possible completions for the current text");
906        cmdout.println("Shift-<tab> -- for current method or constructor invocation, show a synopsis of the method/constructor");
907    }
908
909    private void cmdHistory() {
910        cmdout.println();
911        for (String s : input.currentSessionHistory()) {
912            // No number prefix, confusing with snippet ids
913            cmdout.printf("%s\n", s);
914        }
915    }
916
917    /**
918     * Convert a user argument to a list of snippets referenced by that
919     * argument (or lack of argument).
920     * @param arg The user's argument to the command
921     * @return a list of referenced snippets
922     */
923    private List<Snippet> argToSnippets(String arg) {
924        List<Snippet> snippets = new ArrayList<>();
925        if (arg.isEmpty()) {
926            // Default is all user snippets
927            for (Snippet sn : state.snippets()) {
928                if (notInStartUp(sn)) {
929                    snippets.add(sn);
930                }
931            }
932        } else {
933            // Look for all declarations with matching names
934            for (Snippet key : state.snippets()) {
935                switch (key.kind()) {
936                    case METHOD:
937                    case VAR:
938                    case TYPE_DECL:
939                        if (((DeclarationSnippet) key).name().equals(arg)) {
940                            snippets.add(key);
941                        }
942                        break;
943                }
944            }
945            // If no declarations found, look for an id of this name
946            if (snippets.isEmpty()) {
947                for (Snippet sn : state.snippets()) {
948                    if (sn.id().equals(arg)) {
949                        snippets.add(sn);
950                        break;
951                    }
952                }
953            }
954            // If still no matches found, give an error
955            if (snippets.isEmpty()) {
956                hard("No definition or id named %s found.  See /classes /methods /vars or /list", arg);
957                return null;
958            }
959        }
960        return snippets;
961    }
962
963    private void cmdDrop(String arg) {
964        if (arg.isEmpty()) {
965            hard("In the /drop argument, please specify an import, variable, method, or class to drop.");
966            hard("Specify by id or name. Use /list to see ids. Use /reset to reset all state.");
967            return;
968        }
969        List<Snippet> snippetSet = argToSnippets(arg);
970        if (snippetSet == null) {
971            return;
972        }
973        snippetSet = snippetSet.stream()
974                .filter(sn -> state.status(sn).isActive)
975                .collect(toList());
976        snippetSet.removeIf(sn -> !(sn instanceof PersistentSnippet));
977        if (snippetSet.isEmpty()) {
978            hard("The argument did not specify an import, variable, method, or class to drop.");
979            return;
980        }
981        if (snippetSet.size() > 1) {
982            hard("The argument references more than one import, variable, method, or class.");
983            hard("Try again with one of the ids below:");
984            for (Snippet sn : snippetSet) {
985                cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n       "));
986            }
987            return;
988        }
989        PersistentSnippet psn = (PersistentSnippet) snippetSet.iterator().next();
990        state.drop(psn).forEach(this::handleEvent);
991    }
992
993    private void cmdEdit(String arg) {
994        List<Snippet> snippetSet = argToSnippets(arg);
995        if (snippetSet == null) {
996            return;
997        }
998        Set<String> srcSet = new LinkedHashSet<>();
999        for (Snippet key : snippetSet) {
1000            String src = key.source();
1001            switch (key.subKind()) {
1002                case VAR_VALUE_SUBKIND:
1003                    break;
1004                case ASSIGNMENT_SUBKIND:
1005                case OTHER_EXPRESSION_SUBKIND:
1006                case TEMP_VAR_EXPRESSION_SUBKIND:
1007                    if (!src.endsWith(";")) {
1008                        src = src + ";";
1009                    }
1010                    srcSet.add(src);
1011                    break;
1012                default:
1013                    srcSet.add(src);
1014                    break;
1015            }
1016        }
1017        StringBuilder sb = new StringBuilder();
1018        for (String s : srcSet) {
1019            sb.append(s);
1020            sb.append('\n');
1021        }
1022        String src = sb.toString();
1023        Consumer<String> saveHandler = new SaveHandler(src, srcSet);
1024        Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
1025        if (editor == null) {
1026            EditPad.edit(errorHandler, src, saveHandler);
1027        } else {
1028            ExternalEditor.edit(editor, errorHandler, src, saveHandler, input);
1029        }
1030    }
1031    //where
1032    // receives editor requests to save
1033    private class SaveHandler implements Consumer<String> {
1034
1035        String src;
1036        Set<String> currSrcs;
1037
1038        SaveHandler(String src, Set<String> ss) {
1039            this.src = src;
1040            this.currSrcs = ss;
1041        }
1042
1043        @Override
1044        public void accept(String s) {
1045            if (!s.equals(src)) { // quick check first
1046                src = s;
1047                try {
1048                    Set<String> nextSrcs = new LinkedHashSet<>();
1049                    boolean failed = false;
1050                    while (true) {
1051                        CompletionInfo an = analysis.analyzeCompletion(s);
1052                        if (!an.completeness.isComplete) {
1053                            break;
1054                        }
1055                        String tsrc = trimNewlines(an.source);
1056                        if (!failed && !currSrcs.contains(tsrc)) {
1057                            failed = processCompleteSource(tsrc);
1058                        }
1059                        nextSrcs.add(tsrc);
1060                        if (an.remaining.isEmpty()) {
1061                            break;
1062                        }
1063                        s = an.remaining;
1064                    }
1065                    currSrcs = nextSrcs;
1066                } catch (IllegalStateException ex) {
1067                    hard("Resetting...");
1068                    resetState();
1069                    currSrcs = new LinkedHashSet<>(); // re-process everything
1070                }
1071            }
1072        }
1073
1074        private String trimNewlines(String s) {
1075            int b = 0;
1076            while (b < s.length() && s.charAt(b) == '\n') {
1077                ++b;
1078            }
1079            int e = s.length() -1;
1080            while (e >= 0 && s.charAt(e) == '\n') {
1081                --e;
1082            }
1083            return s.substring(b, e + 1);
1084        }
1085    }
1086
1087    private void cmdList(String arg) {
1088        boolean all = false;
1089        switch (arg) {
1090            case "all":
1091                all = true;
1092                break;
1093            case "history":
1094                cmdHistory();
1095                return;
1096            case "":
1097                break;
1098            default:
1099                hard("Invalid /list argument: %s", arg);
1100                return;
1101        }
1102        boolean hasOutput = false;
1103        for (Snippet sn : state.snippets()) {
1104            if (all || (notInStartUp(sn) && state.status(sn).isActive)) {
1105                if (!hasOutput) {
1106                    cmdout.println();
1107                    hasOutput = true;
1108                }
1109                cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n       "));
1110
1111            }
1112        }
1113    }
1114
1115    private void cmdOpen(String filename) {
1116        if (filename.isEmpty()) {
1117            hard("The /open command requires a filename argument.");
1118        } else {
1119            try {
1120                run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString()));
1121            } catch (FileNotFoundException e) {
1122                hard("File '%s' is not found: %s", filename, e.getMessage());
1123            } catch (Exception e) {
1124                hard("Exception while reading file: %s", e);
1125            }
1126        }
1127    }
1128
1129    private void cmdPrompt() {
1130        displayPrompt = !displayPrompt;
1131        fluff("Prompt will %sdisplay. Use /prompt to toggle.", displayPrompt ? "" : "NOT ");
1132        concise("Prompt: %s", displayPrompt ? "on" : "off");
1133    }
1134
1135    private void cmdReset() {
1136        live = false;
1137        fluff("Resetting state.");
1138    }
1139
1140    private void cmdSave(String arg_filename) {
1141        Matcher mat = HISTORY_ALL_FILENAME.matcher(arg_filename);
1142        if (!mat.find()) {
1143            hard("Malformed argument to the /save command: %s", arg_filename);
1144            return;
1145        }
1146        boolean useHistory = false;
1147        boolean saveAll = false;
1148        String cmd = mat.group("cmd");
1149        if (cmd != null) switch (cmd) {
1150            case "all":
1151                saveAll = true;
1152                break;
1153            case "history":
1154                useHistory = true;
1155                break;
1156        }
1157        String filename = mat.group("filename");
1158        if (filename == null ||filename.isEmpty()) {
1159            hard("The /save command requires a filename argument.");
1160            return;
1161        }
1162        try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
1163                Charset.defaultCharset(),
1164                CREATE, TRUNCATE_EXISTING, WRITE)) {
1165            if (useHistory) {
1166                for (String s : input.currentSessionHistory()) {
1167                    writer.write(s);
1168                    writer.write("\n");
1169                }
1170            } else {
1171                for (Snippet sn : state.snippets()) {
1172                    if (saveAll || notInStartUp(sn)) {
1173                        writer.write(sn.source());
1174                        writer.write("\n");
1175                    }
1176                }
1177            }
1178        } catch (FileNotFoundException e) {
1179            hard("File '%s' for save is not accessible: %s", filename, e.getMessage());
1180        } catch (Exception e) {
1181            hard("Exception while saving: %s", e);
1182        }
1183    }
1184
1185    private void cmdSetStart(String filename) {
1186        if (filename.isEmpty()) {
1187            hard("The /setstart command requires a filename argument.");
1188        } else {
1189            try {
1190                byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename));
1191                String init = new String(encoded);
1192                PREFS.put(STARTUP_KEY, init);
1193            } catch (AccessDeniedException e) {
1194                hard("File '%s' for /setstart is not accessible.", filename);
1195            } catch (NoSuchFileException e) {
1196                hard("File '%s' for /setstart is not found.", filename);
1197            } catch (Exception e) {
1198                hard("Exception while reading start set file: %s", e);
1199            }
1200        }
1201    }
1202
1203    private void cmdSaveStart(String filename) {
1204        if (filename.isEmpty()) {
1205            hard("The /savestart command requires a filename argument.");
1206        } else {
1207            try {
1208                Files.write(toPathResolvingUserHome(filename), DEFAULT_STARTUP.getBytes());
1209            } catch (AccessDeniedException e) {
1210                hard("File '%s' for /savestart is not accessible.", filename);
1211            } catch (NoSuchFileException e) {
1212                hard("File '%s' for /savestart cannot be located.", filename);
1213            } catch (Exception e) {
1214                hard("Exception while saving default startup file: %s", e);
1215            }
1216        }
1217    }
1218
1219    private void cmdVars() {
1220        for (VarSnippet vk : state.variables()) {
1221            String val = state.status(vk) == Status.VALID
1222                    ? state.varValue(vk)
1223                    : "(not-active)";
1224            hard("  %s %s = %s", vk.typeName(), vk.name(), val);
1225        }
1226    }
1227
1228    private void cmdMethods() {
1229        for (MethodSnippet mk : state.methods()) {
1230            hard("  %s %s", mk.name(), mk.signature());
1231        }
1232    }
1233
1234    private void cmdClasses() {
1235        for (TypeDeclSnippet ck : state.types()) {
1236            String kind;
1237            switch (ck.subKind()) {
1238                case INTERFACE_SUBKIND:
1239                    kind = "interface";
1240                    break;
1241                case CLASS_SUBKIND:
1242                    kind = "class";
1243                    break;
1244                case ENUM_SUBKIND:
1245                    kind = "enum";
1246                    break;
1247                case ANNOTATION_TYPE_SUBKIND:
1248                    kind = "@interface";
1249                    break;
1250                default:
1251                    assert false : "Wrong kind" + ck.subKind();
1252                    kind = "class";
1253                    break;
1254            }
1255            hard("  %s %s", kind, ck.name());
1256        }
1257    }
1258
1259    private void cmdUseHistoryEntry(int index) {
1260        List<Snippet> keys = state.snippets();
1261        if (index < 0)
1262            index += keys.size();
1263        else
1264            index--;
1265        if (index >= 0 && index < keys.size()) {
1266            String source = keys.get(index).source();
1267            cmdout.printf("%s\n", source);
1268            input.replaceLastHistoryEntry(source);
1269            processSourceCatchingReset(source);
1270        } else {
1271            hard("Cannot find snippet %d", index + 1);
1272        }
1273    }
1274
1275    /**
1276     * Filter diagnostics for only errors (no warnings, ...)
1277     * @param diagnostics input list
1278     * @return filtered list
1279     */
1280    List<Diag> errorsOnly(List<Diag> diagnostics) {
1281        return diagnostics.stream()
1282                .filter(d -> d.isError())
1283                .collect(toList());
1284    }
1285
1286    void printDiagnostics(String source, List<Diag> diagnostics, boolean embed) {
1287        String padding = embed? "    " : "";
1288        for (Diag diag : diagnostics) {
1289            //assert diag.getSource().equals(source);
1290
1291            if (!embed) {
1292                if (diag.isError()) {
1293                    hard("Error:");
1294                } else {
1295                    hard("Warning:");
1296                }
1297            }
1298
1299            for (String line : diag.getMessage(null).split("\\r?\\n")) {
1300                if (!line.trim().startsWith("location:")) {
1301                    hard("%s%s", padding, line);
1302                }
1303            }
1304
1305            int pstart = (int) diag.getStartPosition();
1306            int pend = (int) diag.getEndPosition();
1307            Matcher m = LINEBREAK.matcher(source);
1308            int pstartl = 0;
1309            int pendl = -2;
1310            while (m.find(pstartl)) {
1311                pendl = m.start();
1312                if (pendl >= pstart) {
1313                    break;
1314                } else {
1315                    pstartl = m.end();
1316                }
1317            }
1318            if (pendl < pstart) {
1319                pendl = source.length();
1320            }
1321            fluff("%s%s", padding, source.substring(pstartl, pendl));
1322
1323            StringBuilder sb = new StringBuilder();
1324            int start = pstart - pstartl;
1325            for (int i = 0; i < start; ++i) {
1326                sb.append(' ');
1327            }
1328            sb.append('^');
1329            boolean multiline = pend > pendl;
1330            int end = (multiline ? pendl : pend) - pstartl - 1;
1331            if (end > start) {
1332                for (int i = start + 1; i < end; ++i) {
1333                    sb.append('-');
1334                }
1335                if (multiline) {
1336                    sb.append("-...");
1337                } else {
1338                    sb.append('^');
1339                }
1340            }
1341            fluff("%s%s", padding, sb.toString());
1342
1343            debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
1344            debug("Code: %s", diag.getCode());
1345            debug("Pos: %d (%d - %d)", diag.getPosition(),
1346                    diag.getStartPosition(), diag.getEndPosition());
1347        }
1348    }
1349
1350    private String processSource(String srcInput) throws IllegalStateException {
1351        while (true) {
1352            CompletionInfo an = analysis.analyzeCompletion(srcInput);
1353            if (!an.completeness.isComplete) {
1354                return an.remaining;
1355            }
1356            boolean failed = processCompleteSource(an.source);
1357            if (failed || an.remaining.isEmpty()) {
1358                return "";
1359            }
1360            srcInput = an.remaining;
1361        }
1362    }
1363    //where
1364    private boolean processCompleteSource(String source) throws IllegalStateException {
1365        debug("Compiling: %s", source);
1366        boolean failed = false;
1367        List<SnippetEvent> events = state.eval(source);
1368        for (SnippetEvent e : events) {
1369            failed |= handleEvent(e);
1370        }
1371        return failed;
1372    }
1373
1374    private boolean handleEvent(SnippetEvent ste) {
1375        Snippet sn = ste.snippet();
1376        if (sn == null) {
1377            debug("Event with null key: %s", ste);
1378            return false;
1379        }
1380        List<Diag> diagnostics = state.diagnostics(sn);
1381        String source = sn.source();
1382        if (ste.causeSnippet() == null) {
1383            // main event
1384            printDiagnostics(source, diagnostics, false);
1385            if (ste.status().isActive) {
1386                if (ste.exception() != null) {
1387                    if (ste.exception() instanceof EvalException) {
1388                        printEvalException((EvalException) ste.exception());
1389                        return true;
1390                    } else if (ste.exception() instanceof UnresolvedReferenceException) {
1391                        printUnresolved((UnresolvedReferenceException) ste.exception());
1392                    } else {
1393                        hard("Unexpected execution exception: %s", ste.exception());
1394                        return true;
1395                    }
1396                } else {
1397                    displayDeclarationAndValue(ste, false, ste.value());
1398                }
1399            } else if (ste.status() == Status.REJECTED) {
1400                if (diagnostics.isEmpty()) {
1401                    hard("Failed.");
1402                }
1403                return true;
1404            }
1405        } else if (ste.status() == Status.REJECTED) {
1406            //TODO -- I don't believe updates can cause failures any more
1407            hard("Caused failure of dependent %s --", ((DeclarationSnippet) sn).name());
1408            printDiagnostics(source, diagnostics, true);
1409        } else {
1410            // Update
1411            SubKind subkind = sn.subKind();
1412            if (sn instanceof DeclarationSnippet
1413                    && (feedback() == Feedback.Verbose
1414                    || ste.status() == Status.OVERWRITTEN
1415                    || subkind == SubKind.VAR_DECLARATION_SUBKIND
1416                    || subkind == SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND)) {
1417                // Under the conditions, display update information
1418                displayDeclarationAndValue(ste, true, null);
1419                List<Diag> other = errorsOnly(diagnostics);
1420                if (other.size() > 0) {
1421                    printDiagnostics(source, other, true);
1422                }
1423            }
1424        }
1425        return false;
1426    }
1427
1428    @SuppressWarnings("fallthrough")
1429    private void displayDeclarationAndValue(SnippetEvent ste, boolean update, String value) {
1430        Snippet key = ste.snippet();
1431        String declared;
1432        Status status = ste.status();
1433        switch (status) {
1434            case VALID:
1435            case RECOVERABLE_DEFINED:
1436            case RECOVERABLE_NOT_DEFINED:
1437                if (ste.previousStatus().isActive) {
1438                    declared = ste.isSignatureChange()
1439                        ? "Replaced"
1440                        : "Modified";
1441                } else {
1442                    declared = "Added";
1443                }
1444                break;
1445            case OVERWRITTEN:
1446                declared = "Overwrote";
1447                break;
1448            case DROPPED:
1449                declared = "Dropped";
1450                break;
1451            case REJECTED:
1452                declared = "Rejected";
1453                break;
1454            case NONEXISTENT:
1455            default:
1456                // Should not occur
1457                declared = ste.previousStatus().toString() + "=>" + status.toString();
1458        }
1459        if (update) {
1460            declared = "  Update " + declared.toLowerCase();
1461        }
1462        String however;
1463        if (key instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) {
1464            String cannotUntil = (status == Status.RECOVERABLE_NOT_DEFINED)
1465                    ? " cannot be referenced until"
1466                    : " cannot be invoked until";
1467            however = (update? " which" : ", however, it") + cannotUntil + unresolved((DeclarationSnippet) key);
1468        } else {
1469            however = "";
1470        }
1471        switch (key.subKind()) {
1472            case CLASS_SUBKIND:
1473                fluff("%s class %s%s", declared, ((TypeDeclSnippet) key).name(), however);
1474                break;
1475            case INTERFACE_SUBKIND:
1476                fluff("%s interface %s%s", declared, ((TypeDeclSnippet) key).name(), however);
1477                break;
1478            case ENUM_SUBKIND:
1479                fluff("%s enum %s%s", declared, ((TypeDeclSnippet) key).name(), however);
1480                break;
1481            case ANNOTATION_TYPE_SUBKIND:
1482                fluff("%s annotation interface %s%s", declared, ((TypeDeclSnippet) key).name(), however);
1483                break;
1484            case METHOD_SUBKIND:
1485                fluff("%s method %s(%s)%s", declared, ((MethodSnippet) key).name(),
1486                        ((MethodSnippet) key).parameterTypes(), however);
1487                break;
1488            case VAR_DECLARATION_SUBKIND:
1489                if (!update) {
1490                    VarSnippet vk = (VarSnippet) key;
1491                    if (status == Status.RECOVERABLE_NOT_DEFINED) {
1492                        fluff("%s variable %s%s", declared, vk.name(), however);
1493                    } else {
1494                        fluff("%s variable %s of type %s%s", declared, vk.name(), vk.typeName(), however);
1495                    }
1496                    break;
1497                }
1498            // Fall through
1499            case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: {
1500                VarSnippet vk = (VarSnippet) key;
1501                if (status == Status.RECOVERABLE_NOT_DEFINED) {
1502                    if (!update) {
1503                        fluff("%s variable %s%s", declared, vk.name(), however);
1504                        break;
1505                    }
1506                } else if (update) {
1507                    if (ste.isSignatureChange()) {
1508                        hard("%s variable %s, reset to null", declared, vk.name());
1509                    }
1510                } else {
1511                    fluff("%s variable %s of type %s with initial value %s",
1512                            declared, vk.name(), vk.typeName(), value);
1513                    concise("%s : %s", vk.name(), value);
1514                }
1515                break;
1516            }
1517            case TEMP_VAR_EXPRESSION_SUBKIND: {
1518                VarSnippet vk = (VarSnippet) key;
1519                if (update) {
1520                    hard("%s temporary variable %s, reset to null", declared, vk.name());
1521                 } else {
1522                    fluff("Expression value is: %s", (value));
1523                    fluff("  assigned to temporary variable %s of type %s", vk.name(), vk.typeName());
1524                    concise("%s : %s", vk.name(), value);
1525                }
1526                break;
1527            }
1528            case OTHER_EXPRESSION_SUBKIND:
1529                fluff("Expression value is: %s", (value));
1530                break;
1531            case VAR_VALUE_SUBKIND: {
1532                ExpressionSnippet ek = (ExpressionSnippet) key;
1533                fluff("Variable %s of type %s has value %s", ek.name(), ek.typeName(), (value));
1534                concise("%s : %s", ek.name(), value);
1535                break;
1536            }
1537            case ASSIGNMENT_SUBKIND: {
1538                ExpressionSnippet ek = (ExpressionSnippet) key;
1539                fluff("Variable %s has been assigned the value %s", ek.name(), (value));
1540                concise("%s : %s", ek.name(), value);
1541                break;
1542            }
1543        }
1544    }
1545    //where
1546    void printStackTrace(StackTraceElement[] stes) {
1547        for (StackTraceElement ste : stes) {
1548            StringBuilder sb = new StringBuilder();
1549            String cn = ste.getClassName();
1550            if (!cn.isEmpty()) {
1551                int dot = cn.lastIndexOf('.');
1552                if (dot > 0) {
1553                    sb.append(cn.substring(dot + 1));
1554                } else {
1555                    sb.append(cn);
1556                }
1557                sb.append(".");
1558            }
1559            if (!ste.getMethodName().isEmpty()) {
1560                sb.append(ste.getMethodName());
1561                sb.append(" ");
1562            }
1563            String fileName = ste.getFileName();
1564            int lineNumber = ste.getLineNumber();
1565            String loc = ste.isNativeMethod()
1566                    ? "Native Method"
1567                    : fileName == null
1568                            ? "Unknown Source"
1569                            : lineNumber >= 0
1570                                    ? fileName + ":" + lineNumber
1571                                    : fileName;
1572            hard("      at %s(%s)", sb, loc);
1573
1574        }
1575    }
1576    //where
1577    void printUnresolved(UnresolvedReferenceException ex) {
1578        MethodSnippet corralled =  ex.getMethodSnippet();
1579        List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled));
1580        StringBuilder sb = new StringBuilder();
1581        if (otherErrors.size() > 0) {
1582            if (state.unresolvedDependencies(corralled).size() > 0) {
1583                sb.append(" and");
1584            }
1585            if (otherErrors.size() == 1) {
1586                sb.append(" this error is addressed --");
1587            } else {
1588                sb.append(" these errors are addressed --");
1589            }
1590        } else {
1591            sb.append(".");
1592        }
1593
1594        hard("Attempted to call %s which cannot be invoked until%s", corralled.name(),
1595                unresolved(corralled), sb.toString());
1596        if (otherErrors.size() > 0) {
1597            printDiagnostics(corralled.source(), otherErrors, true);
1598        }
1599    }
1600    //where
1601    void printEvalException(EvalException ex) {
1602        if (ex.getMessage() == null) {
1603            hard("%s thrown", ex.getExceptionClassName());
1604        } else {
1605            hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
1606        }
1607        printStackTrace(ex.getStackTrace());
1608    }
1609    //where
1610    String unresolved(DeclarationSnippet key) {
1611        List<String> unr = state.unresolvedDependencies(key);
1612        StringBuilder sb = new StringBuilder();
1613        int fromLast = unr.size();
1614        if (fromLast > 0) {
1615            sb.append(" ");
1616        }
1617        for (String u : unr) {
1618            --fromLast;
1619            sb.append(u);
1620            if (fromLast == 0) {
1621                // No suffix
1622            } else if (fromLast == 1) {
1623                sb.append(", and ");
1624            } else {
1625                sb.append(", ");
1626            }
1627        }
1628        switch (unr.size()) {
1629            case 0:
1630                break;
1631            case 1:
1632                sb.append(" is declared");
1633                break;
1634            default:
1635                sb.append(" are declared");
1636                break;
1637        }
1638        return sb.toString();
1639    }
1640
1641    enum Feedback {
1642        Default,
1643        Off,
1644        Concise,
1645        Normal,
1646        Verbose
1647    }
1648
1649    Feedback feedback() {
1650        if (feedback == Feedback.Default) {
1651            return input == null || input.interactiveOutput() ? Feedback.Normal : Feedback.Off;
1652        }
1653        return feedback;
1654    }
1655
1656    boolean notInStartUp(Snippet sn) {
1657        return mapSnippet.get(sn).space != startNamespace;
1658    }
1659
1660    /** The current version number as a string.
1661     */
1662    static String version() {
1663        return version("release");  // mm.nn.oo[-milestone]
1664    }
1665
1666    /** The current full version number as a string.
1667     */
1668    static String fullVersion() {
1669        return version("full"); // mm.mm.oo[-milestone]-build
1670    }
1671
1672    private static final String versionRBName = "jdk.internal.jshell.tool.resources.version";
1673    private static ResourceBundle versionRB;
1674
1675    private static String version(String key) {
1676        if (versionRB == null) {
1677            try {
1678                versionRB = ResourceBundle.getBundle(versionRBName);
1679            } catch (MissingResourceException e) {
1680                return "(version info not available)";
1681            }
1682        }
1683        try {
1684            return versionRB.getString(key);
1685        }
1686        catch (MissingResourceException e) {
1687            return "(version info not available)";
1688        }
1689    }
1690
1691    class NameSpace {
1692        final String spaceName;
1693        final String prefix;
1694        private int nextNum;
1695
1696        NameSpace(String spaceName, String prefix) {
1697            this.spaceName = spaceName;
1698            this.prefix = prefix;
1699            this.nextNum = 1;
1700        }
1701
1702        String tid(Snippet sn) {
1703            String tid = prefix + nextNum++;
1704            mapSnippet.put(sn, new SnippetInfo(sn, this, tid));
1705            return tid;
1706        }
1707
1708        String tidNext() {
1709            return prefix + nextNum;
1710        }
1711    }
1712
1713    static class SnippetInfo {
1714        final Snippet snippet;
1715        final NameSpace space;
1716        final String tid;
1717
1718        SnippetInfo(Snippet snippet, NameSpace space, String tid) {
1719            this.snippet = snippet;
1720            this.space = space;
1721            this.tid = tid;
1722        }
1723    }
1724}
1725
1726class ScannerIOContext extends IOContext {
1727
1728    private final Scanner scannerIn;
1729    private final PrintStream pStream;
1730
1731    public ScannerIOContext(Scanner scannerIn, PrintStream pStream) {
1732        this.scannerIn = scannerIn;
1733        this.pStream = pStream;
1734    }
1735
1736    @Override
1737    public String readLine(String prompt, String prefix) {
1738        if (pStream != null && prompt != null) {
1739            pStream.print(prompt);
1740        }
1741        if (scannerIn.hasNextLine()) {
1742            return scannerIn.nextLine();
1743        } else {
1744            return null;
1745        }
1746    }
1747
1748    @Override
1749    public boolean interactiveOutput() {
1750        return true;
1751    }
1752
1753    @Override
1754    public Iterable<String> currentSessionHistory() {
1755        return Collections.emptyList();
1756    }
1757
1758    @Override
1759    public void close() {
1760        scannerIn.close();
1761    }
1762
1763    @Override
1764    public boolean terminalEditorRunning() {
1765        return false;
1766    }
1767
1768    @Override
1769    public void suspend() {
1770    }
1771
1772    @Override
1773    public void resume() {
1774    }
1775
1776    @Override
1777    public void beforeUserCode() {
1778    }
1779
1780    @Override
1781    public void afterUserCode() {
1782    }
1783
1784    @Override
1785    public void replaceLastHistoryEntry(String source) {
1786    }
1787}
1788
1789class FileScannerIOContext extends ScannerIOContext {
1790
1791    public FileScannerIOContext(String fn) throws FileNotFoundException {
1792        this(new FileReader(fn));
1793    }
1794
1795    public FileScannerIOContext(Reader rdr) throws FileNotFoundException {
1796        super(new Scanner(rdr), null);
1797    }
1798
1799    @Override
1800    public boolean interactiveOutput() {
1801        return false;
1802    }
1803}
1804
1805