rev 48074 : 8189102: All tools should support -?, -h and --help
Reviewed-by: kvn, jjg, weijun, alanb, rfield, ksrini
1 /*
2 * Copyright (c) 2014, 2017, 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
26 package jdk.internal.jshell.tool;
27
28 import java.io.BufferedReader;
29 import java.io.BufferedWriter;
30 import java.io.EOFException;
31 import java.io.File;
32 import java.io.FileNotFoundException;
33 import java.io.FileReader;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.InputStreamReader;
37 import java.io.PrintStream;
38 import java.io.Reader;
39 import java.io.StringReader;
40 import java.lang.module.ModuleDescriptor;
41 import java.lang.module.ModuleFinder;
42 import java.lang.module.ModuleReference;
43 import java.nio.charset.Charset;
44 import java.nio.file.FileSystems;
45 import java.nio.file.Files;
46 import java.nio.file.Path;
47 import java.nio.file.Paths;
48 import java.text.MessageFormat;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collection;
52 import java.util.Collections;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.Iterator;
56 import java.util.LinkedHashMap;
57 import java.util.LinkedHashSet;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.Map;
61 import java.util.Map.Entry;
62 import java.util.Optional;
63 import java.util.Scanner;
64 import java.util.Set;
65 import java.util.function.Consumer;
66 import java.util.function.Predicate;
67 import java.util.prefs.Preferences;
68 import java.util.regex.Matcher;
69 import java.util.regex.Pattern;
70 import java.util.stream.Collectors;
71 import java.util.stream.Stream;
72 import java.util.stream.StreamSupport;
73
74 import jdk.internal.jshell.debug.InternalDebugControl;
75 import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
76 import jdk.jshell.DeclarationSnippet;
77 import jdk.jshell.Diag;
78 import jdk.jshell.EvalException;
79 import jdk.jshell.ExpressionSnippet;
80 import jdk.jshell.ImportSnippet;
81 import jdk.jshell.JShell;
82 import jdk.jshell.JShell.Subscription;
83 import jdk.jshell.MethodSnippet;
84 import jdk.jshell.Snippet;
85 import jdk.jshell.Snippet.Kind;
86 import jdk.jshell.Snippet.Status;
87 import jdk.jshell.SnippetEvent;
88 import jdk.jshell.SourceCodeAnalysis;
89 import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
90 import jdk.jshell.SourceCodeAnalysis.Completeness;
91 import jdk.jshell.SourceCodeAnalysis.Suggestion;
92 import jdk.jshell.TypeDeclSnippet;
93 import jdk.jshell.UnresolvedReferenceException;
94 import jdk.jshell.VarSnippet;
95
96 import static java.nio.file.StandardOpenOption.CREATE;
97 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
98 import static java.nio.file.StandardOpenOption.WRITE;
99 import java.util.AbstractMap.SimpleEntry;
100 import java.util.MissingResourceException;
101 import java.util.ResourceBundle;
102 import java.util.ServiceLoader;
103 import java.util.Spliterators;
104 import java.util.function.Function;
105 import java.util.function.Supplier;
106 import jdk.internal.joptsimple.*;
107 import jdk.internal.jshell.tool.Feedback.FormatAction;
108 import jdk.internal.jshell.tool.Feedback.FormatCase;
109 import jdk.internal.jshell.tool.Feedback.FormatErrors;
110 import jdk.internal.jshell.tool.Feedback.FormatResolve;
111 import jdk.internal.jshell.tool.Feedback.FormatUnresolved;
112 import jdk.internal.jshell.tool.Feedback.FormatWhen;
113 import jdk.internal.editor.spi.BuildInEditorProvider;
114 import jdk.internal.editor.external.ExternalEditor;
115 import static java.util.Arrays.asList;
116 import static java.util.Arrays.stream;
117 import static java.util.stream.Collectors.joining;
118 import static java.util.stream.Collectors.toList;
119 import static jdk.jshell.Snippet.SubKind.TEMP_VAR_EXPRESSION_SUBKIND;
120 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND;
121 import static java.util.stream.Collectors.toMap;
122 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA;
123 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP;
124 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
125 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR;
126 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
127 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP;
128 import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER;
129
130 /**
131 * Command line REPL tool for Java using the JShell API.
132 * @author Robert Field
133 */
134 public class JShellTool implements MessageHandler {
135
136 private static final Pattern LINEBREAK = Pattern.compile("\\R");
137 private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?");
138 static final String RECORD_SEPARATOR = "\u241E";
139 private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources";
140 private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version";
141 private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n";
142
143 final InputStream cmdin;
144 final PrintStream cmdout;
145 final PrintStream cmderr;
146 final PrintStream console;
147 final InputStream userin;
148 final PrintStream userout;
149 final PrintStream usererr;
150 final PersistentStorage prefs;
151 final Map<String, String> envvars;
152 final Locale locale;
153
154 final Feedback feedback = new Feedback();
155
156 /**
157 * The complete constructor for the tool (used by test harnesses).
158 * @param cmdin command line input -- snippets and commands
159 * @param cmdout command line output, feedback including errors
160 * @param cmderr start-up errors and debugging info
161 * @param console console control interaction
162 * @param userin code execution input, or null to use IOContext
163 * @param userout code execution output -- System.out.printf("hi")
164 * @param usererr code execution error stream -- System.err.printf("Oops")
165 * @param prefs persistence implementation to use
166 * @param envvars environment variable mapping to use
167 * @param locale locale to use
168 */
169 JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
170 PrintStream console,
171 InputStream userin, PrintStream userout, PrintStream usererr,
172 PersistentStorage prefs, Map<String, String> envvars, Locale locale) {
173 this.cmdin = cmdin;
174 this.cmdout = cmdout;
175 this.cmderr = cmderr;
176 this.console = console;
177 this.userin = userin != null ? userin : new InputStream() {
178 @Override
179 public int read() throws IOException {
180 return input.readUserInput();
181 }
182 };
183 this.userout = userout;
184 this.usererr = usererr;
185 this.prefs = prefs;
186 this.envvars = envvars;
187 this.locale = locale;
188 }
189
190 private ResourceBundle versionRB = null;
191 private ResourceBundle outputRB = null;
192
193 private IOContext input = null;
194 private boolean regenerateOnDeath = true;
195 private boolean live = false;
196 private boolean interactiveModeBegun = false;
197 private Options options;
198
199 SourceCodeAnalysis analysis;
200 private JShell state = null;
201 Subscription shutdownSubscription = null;
202
203 static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false);
204
205 private boolean debug = false;
206 public boolean testPrompt = false;
207 private Startup startup = null;
208 private boolean isCurrentlyRunningStartup = false;
209 private String executionControlSpec = null;
210 private EditorSetting editor = BUILT_IN_EDITOR;
211 private int exitCode = 0;
212
213 private static final String[] EDITOR_ENV_VARS = new String[] {
214 "JSHELLEDITOR", "VISUAL", "EDITOR"};
215
216 // Commands and snippets which can be replayed
217 private ReplayableHistory replayableHistory;
218 private ReplayableHistory replayableHistoryPrevious;
219
220 static final String STARTUP_KEY = "STARTUP";
221 static final String EDITOR_KEY = "EDITOR";
222 static final String FEEDBACK_KEY = "FEEDBACK";
223 static final String MODE_KEY = "MODE";
224 static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE";
225
226 static final Pattern BUILTIN_FILE_PATTERN = Pattern.compile("\\w+");
227 static final String BUILTIN_FILE_PATH_FORMAT = "/jdk/jshell/tool/resources/%s.jsh";
228 static final String INT_PREFIX = "int $$exit$$ = ";
229
230 // match anything followed by whitespace
231 private static final Pattern OPTION_PRE_PATTERN =
232 Pattern.compile("\\s*(\\S+\\s+)*?");
233 // match a (possibly incomplete) option flag with optional double-dash and/or internal dashes
234 private static final Pattern OPTION_PATTERN =
235 Pattern.compile(OPTION_PRE_PATTERN.pattern() + "(?<dd>-??)(?<flag>-([a-z][a-z\\-]*)?)");
236 // match an option flag and a (possibly missing or incomplete) value
237 private static final Pattern OPTION_VALUE_PATTERN =
238 Pattern.compile(OPTION_PATTERN.pattern() + "\\s+(?<val>\\S*)");
239
240 // Tool id (tid) mapping: the three name spaces
241 NameSpace mainNamespace;
242 NameSpace startNamespace;
243 NameSpace errorNamespace;
244
245 // Tool id (tid) mapping: the current name spaces
246 NameSpace currentNameSpace;
247
248 Map<Snippet, SnippetInfo> mapSnippet;
249
250 // Kinds of compiler/runtime init options
251 private enum OptionKind {
252 CLASS_PATH("--class-path", true),
253 MODULE_PATH("--module-path", true),
254 ADD_MODULES("--add-modules", false),
255 ADD_EXPORTS("--add-exports", false),
256 TO_COMPILER("-C", false, false, true, false),
257 TO_REMOTE_VM("-R", false, false, false, true),;
258 final String optionFlag;
259 final boolean onlyOne;
260 final boolean passFlag;
261 final boolean toCompiler;
262 final boolean toRemoteVm;
263
264 private OptionKind(String optionFlag, boolean onlyOne) {
265 this(optionFlag, onlyOne, true, true, true);
266 }
267
268 private OptionKind(String optionFlag, boolean onlyOne, boolean passFlag,
269 boolean toCompiler, boolean toRemoteVm) {
270 this.optionFlag = optionFlag;
271 this.onlyOne = onlyOne;
272 this.passFlag = passFlag;
273 this.toCompiler = toCompiler;
274 this.toRemoteVm = toRemoteVm;
275 }
276
277 }
278
279 // compiler/runtime init option values
280 private static class Options {
281
282 private final Map<OptionKind, List<String>> optMap;
283
284 // New blank Options
285 Options() {
286 optMap = new HashMap<>();
287 }
288
289 // Options as a copy
290 private Options(Options opts) {
291 optMap = new HashMap<>(opts.optMap);
292 }
293
294 private String[] selectOptions(Predicate<Entry<OptionKind, List<String>>> pred) {
295 return optMap.entrySet().stream()
296 .filter(pred)
297 .flatMap(e -> e.getValue().stream())
298 .toArray(String[]::new);
299 }
300
301 String[] remoteVmOptions() {
302 return selectOptions(e -> e.getKey().toRemoteVm);
303 }
304
305 String[] compilerOptions() {
306 return selectOptions(e -> e.getKey().toCompiler);
307 }
308
309 String[] commonOptions() {
310 return selectOptions(e -> e.getKey().passFlag);
311 }
312
313 void addAll(OptionKind kind, Collection<String> vals) {
314 optMap.computeIfAbsent(kind, k -> new ArrayList<>())
315 .addAll(vals);
316 }
317
318 // return a new Options, with parameter options overriding receiver options
319 Options override(Options newer) {
320 Options result = new Options(this);
321 newer.optMap.entrySet().stream()
322 .forEach(e -> {
323 if (e.getKey().onlyOne) {
324 // Only one allowed, override last
325 result.optMap.put(e.getKey(), e.getValue());
326 } else {
327 // Additive
328 result.addAll(e.getKey(), e.getValue());
329 }
330 });
331 return result;
332 }
333 }
334
335 // base option parsing of /env, /reload, and /reset and command-line options
336 private class OptionParserBase {
337
338 final OptionParser parser = new OptionParser();
339 private final OptionSpec<String> argClassPath = parser.accepts("class-path").withRequiredArg();
340 private final OptionSpec<String> argModulePath = parser.accepts("module-path").withRequiredArg();
341 private final OptionSpec<String> argAddModules = parser.accepts("add-modules").withRequiredArg();
342 private final OptionSpec<String> argAddExports = parser.accepts("add-exports").withRequiredArg();
343 private final NonOptionArgumentSpec<String> argNonOptions = parser.nonOptions();
344
345 private Options opts = new Options();
346 private List<String> nonOptions;
347 private boolean failed = false;
348
349 List<String> nonOptions() {
350 return nonOptions;
351 }
352
353 void msg(String key, Object... args) {
354 errormsg(key, args);
355 }
356
357 Options parse(String[] args) throws OptionException {
358 try {
359 OptionSet oset = parser.parse(args);
360 nonOptions = oset.valuesOf(argNonOptions);
361 return parse(oset);
362 } catch (OptionException ex) {
363 if (ex.options().isEmpty()) {
364 msg("jshell.err.opt.invalid", stream(args).collect(joining(", ")));
365 } else {
366 boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next());
367 msg(isKnown
368 ? "jshell.err.opt.arg"
369 : "jshell.err.opt.unknown",
370 ex.options()
371 .stream()
372 .collect(joining(", ")));
373 }
374 exitCode = 1;
375 return null;
376 }
377 }
378
379 // check that the supplied string represent valid class/module paths
380 // converting any ~/ to user home
381 private Collection<String> validPaths(Collection<String> vals, String context, boolean isModulePath) {
382 Stream<String> result = vals.stream()
383 .map(s -> Arrays.stream(s.split(File.pathSeparator))
384 .map(sp -> toPathResolvingUserHome(sp))
385 .filter(p -> checkValidPathEntry(p, context, isModulePath))
386 .map(p -> p.toString())
387 .collect(Collectors.joining(File.pathSeparator)));
388 if (failed) {
389 return Collections.emptyList();
390 } else {
391 return result.collect(toList());
392 }
393 }
394
395 // Adapted from compiler method Locations.checkValidModulePathEntry
396 private boolean checkValidPathEntry(Path p, String context, boolean isModulePath) {
397 if (!Files.exists(p)) {
398 msg("jshell.err.file.not.found", context, p);
399 failed = true;
400 return false;
401 }
402 if (Files.isDirectory(p)) {
403 // if module-path, either an exploded module or a directory of modules
404 return true;
405 }
406
407 String name = p.getFileName().toString();
408 int lastDot = name.lastIndexOf(".");
409 if (lastDot > 0) {
410 switch (name.substring(lastDot)) {
411 case ".jar":
412 return true;
413 case ".jmod":
414 if (isModulePath) {
415 return true;
416 }
417 }
418 }
419 msg("jshell.err.arg", context, p);
420 failed = true;
421 return false;
422 }
423
424 Options parse(OptionSet options) {
425 addOptions(OptionKind.CLASS_PATH,
426 validPaths(options.valuesOf(argClassPath), "--class-path", false));
427 addOptions(OptionKind.MODULE_PATH,
428 validPaths(options.valuesOf(argModulePath), "--module-path", true));
429 addOptions(OptionKind.ADD_MODULES, options.valuesOf(argAddModules));
430 addOptions(OptionKind.ADD_EXPORTS, options.valuesOf(argAddExports).stream()
431 .map(mp -> mp.contains("=") ? mp : mp + "=ALL-UNNAMED")
432 .collect(toList())
433 );
434
435 if (failed) {
436 exitCode = 1;
437 return null;
438 } else {
439 return opts;
440 }
441 }
442
443 void addOptions(OptionKind kind, Collection<String> vals) {
444 if (!vals.isEmpty()) {
445 if (kind.onlyOne && vals.size() > 1) {
446 msg("jshell.err.opt.one", kind.optionFlag);
447 failed = true;
448 return;
449 }
450 if (kind.passFlag) {
451 vals = vals.stream()
452 .flatMap(mp -> Stream.of(kind.optionFlag, mp))
453 .collect(toList());
454 }
455 opts.addAll(kind, vals);
456 }
457 }
458 }
459
460 // option parsing for /reload (adds -restore -quiet)
461 private class OptionParserReload extends OptionParserBase {
462
463 private final OptionSpecBuilder argRestore = parser.accepts("restore");
464 private final OptionSpecBuilder argQuiet = parser.accepts("quiet");
465
466 private boolean restore = false;
467 private boolean quiet = false;
468
469 boolean restore() {
470 return restore;
471 }
472
473 boolean quiet() {
474 return quiet;
475 }
476
477 @Override
478 Options parse(OptionSet options) {
479 if (options.has(argRestore)) {
480 restore = true;
481 }
482 if (options.has(argQuiet)) {
483 quiet = true;
484 }
485 return super.parse(options);
486 }
487 }
488
489 // option parsing for command-line
490 private class OptionParserCommandLine extends OptionParserBase {
491
492 private final OptionSpec<String> argStart = parser.accepts("startup").withRequiredArg();
493 private final OptionSpecBuilder argNoStart = parser.acceptsAll(asList("n", "no-startup"));
494 private final OptionSpec<String> argFeedback = parser.accepts("feedback").withRequiredArg();
495 private final OptionSpec<String> argExecution = parser.accepts("execution").withRequiredArg();
496 private final OptionSpecBuilder argQ = parser.accepts("q");
497 private final OptionSpecBuilder argS = parser.accepts("s");
498 private final OptionSpecBuilder argV = parser.accepts("v");
499 private final OptionSpec<String> argR = parser.accepts("R").withRequiredArg();
500 private final OptionSpec<String> argC = parser.accepts("C").withRequiredArg();
501 private final OptionSpecBuilder argHelp = parser.acceptsAll(asList("h", "help"));
502 private final OptionSpecBuilder argVersion = parser.accepts("version");
503 private final OptionSpecBuilder argFullVersion = parser.accepts("full-version");
504 private final OptionSpecBuilder argShowVersion = parser.accepts("show-version");
505 private final OptionSpecBuilder argHelpExtra = parser.acceptsAll(asList("X", "help-extra"));
506
507 private String feedbackMode = null;
508 private Startup initialStartup = null;
509
510 String feedbackMode() {
511 return feedbackMode;
512 }
513
514 Startup startup() {
515 return initialStartup;
516 }
517
518 @Override
519 void msg(String key, Object... args) {
520 errormsg(key, args);
521 }
522
523 /**
524 * Parse the command line options.
525 * @return the options as an Options object, or null if error
526 */
527 @Override
528 Options parse(OptionSet options) {
529 if (options.has(argHelp)) {
530 printUsage();
531 return null;
532 }
533 if (options.has(argHelpExtra)) {
534 printUsageX();
535 return null;
536 }
537 if (options.has(argVersion)) {
538 cmdout.printf("jshell %s\n", version());
539 return null;
540 }
541 if (options.has(argFullVersion)) {
542 cmdout.printf("jshell %s\n", fullVersion());
543 return null;
544 }
545 if (options.has(argShowVersion)) {
546 cmdout.printf("jshell %s\n", version());
547 }
548 if ((options.valuesOf(argFeedback).size() +
549 (options.has(argQ) ? 1 : 0) +
550 (options.has(argS) ? 1 : 0) +
551 (options.has(argV) ? 1 : 0)) > 1) {
552 msg("jshell.err.opt.feedback.one");
553 exitCode = 1;
554 return null;
555 } else if (options.has(argFeedback)) {
556 feedbackMode = options.valueOf(argFeedback);
557 } else if (options.has("q")) {
558 feedbackMode = "concise";
559 } else if (options.has("s")) {
560 feedbackMode = "silent";
561 } else if (options.has("v")) {
562 feedbackMode = "verbose";
563 }
564 if (options.has(argStart)) {
565 List<String> sts = options.valuesOf(argStart);
566 if (options.has("no-startup")) {
567 msg("jshell.err.opt.startup.conflict");
568 exitCode = 1;
569 return null;
570 }
571 initialStartup = Startup.fromFileList(sts, "--startup", new InitMessageHandler());
572 if (initialStartup == null) {
573 exitCode = 1;
574 return null;
575 }
576 } else if (options.has(argNoStart)) {
577 initialStartup = Startup.noStartup();
578 } else {
579 String packedStartup = prefs.get(STARTUP_KEY);
580 initialStartup = Startup.unpack(packedStartup, new InitMessageHandler());
581 }
582 if (options.has(argExecution)) {
583 executionControlSpec = options.valueOf(argExecution);
584 }
585 addOptions(OptionKind.TO_REMOTE_VM, options.valuesOf(argR));
586 addOptions(OptionKind.TO_COMPILER, options.valuesOf(argC));
587 return super.parse(options);
588 }
589 }
590
591 /**
592 * Encapsulate a history of snippets and commands which can be replayed.
593 */
594 private static class ReplayableHistory {
595
596 // the history
597 private List<String> hist;
598
599 // the length of the history as of last save
600 private int lastSaved;
601
602 private ReplayableHistory(List<String> hist) {
603 this.hist = hist;
604 this.lastSaved = 0;
605 }
606
607 // factory for empty histories
608 static ReplayableHistory emptyHistory() {
609 return new ReplayableHistory(new ArrayList<>());
610 }
611
612 // factory for history stored in persistent storage
613 static ReplayableHistory fromPrevious(PersistentStorage prefs) {
614 // Read replay history from last jshell session
615 String prevReplay = prefs.get(REPLAY_RESTORE_KEY);
616 if (prevReplay == null) {
617 return null;
618 } else {
619 return new ReplayableHistory(Arrays.asList(prevReplay.split(RECORD_SEPARATOR)));
620 }
621
622 }
623
624 // store the history in persistent storage
625 void storeHistory(PersistentStorage prefs) {
626 if (hist.size() > lastSaved) {
627 // Prevent history overflow by calculating what will fit, starting
628 // with most recent
629 int sepLen = RECORD_SEPARATOR.length();
630 int length = 0;
631 int first = hist.size();
632 while (length < Preferences.MAX_VALUE_LENGTH && --first >= 0) {
633 length += hist.get(first).length() + sepLen;
634 }
635 if (first >= 0) {
636 hist = hist.subList(first + 1, hist.size());
637 }
638 String shist = String.join(RECORD_SEPARATOR, hist);
639 prefs.put(REPLAY_RESTORE_KEY, shist);
640 markSaved();
641 }
642 prefs.flush();
643 }
644
645 // add a snippet or command to the history
646 void add(String s) {
647 hist.add(s);
648 }
649
650 // return history to reloaded
651 Iterable<String> iterable() {
652 return hist;
653 }
654
655 // mark that persistent storage and current history are in sync
656 void markSaved() {
657 lastSaved = hist.size();
658 }
659 }
660
661 /**
662 * Is the input/output currently interactive
663 *
664 * @return true if console
665 */
666 boolean interactive() {
667 return input != null && input.interactiveOutput();
668 }
669
670 void debug(String format, Object... args) {
671 if (debug) {
672 cmderr.printf(format + "\n", args);
673 }
674 }
675
676 /**
677 * Must show command output
678 *
679 * @param format printf format
680 * @param args printf args
681 */
682 @Override
683 public void hard(String format, Object... args) {
684 cmdout.printf(prefix(format), args);
685 }
686
687 /**
688 * Error command output
689 *
690 * @param format printf format
691 * @param args printf args
692 */
693 void error(String format, Object... args) {
694 (interactiveModeBegun? cmdout : cmderr).printf(prefixError(format), args);
695 }
696
697 /**
698 * Should optional informative be displayed?
699 * @return true if they should be displayed
700 */
701 @Override
702 public boolean showFluff() {
703 return feedback.shouldDisplayCommandFluff() && interactive();
704 }
705
706 /**
707 * Optional output
708 *
709 * @param format printf format
710 * @param args printf args
711 */
712 @Override
713 public void fluff(String format, Object... args) {
714 if (showFluff()) {
715 hard(format, args);
716 }
717 }
718
719 /**
720 * Resource bundle look-up
721 *
722 * @param key the resource key
723 */
724 String getResourceString(String key) {
725 if (outputRB == null) {
726 try {
727 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale);
728 } catch (MissingResourceException mre) {
729 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale);
730 return "";
731 }
732 }
733 String s;
734 try {
735 s = outputRB.getString(key);
736 } catch (MissingResourceException mre) {
737 error("Missing resource: %s in %s", key, L10N_RB_NAME);
738 return "";
739 }
740 return s;
741 }
742
743 /**
744 * Add normal prefixing/postfixing to embedded newlines in a string,
745 * bracketing with normal prefix/postfix
746 *
747 * @param s the string to prefix
748 * @return the pre/post-fixed and bracketed string
749 */
750 String prefix(String s) {
751 return prefix(s, feedback.getPre(), feedback.getPost());
752 }
753
754 /**
755 * Add error prefixing/postfixing to embedded newlines in a string,
756 * bracketing with error prefix/postfix
757 *
758 * @param s the string to prefix
759 * @return the pre/post-fixed and bracketed string
760 */
761 String prefixError(String s) {
762 return prefix(s, feedback.getErrorPre(), feedback.getErrorPost());
763 }
764
765 /**
766 * Add prefixing/postfixing to embedded newlines in a string,
767 * bracketing with prefix/postfix. No prefixing when non-interactive.
768 * Result is expected to be the format for a printf.
769 *
770 * @param s the string to prefix
771 * @param pre the string to prepend to each line
772 * @param post the string to append to each line (replacing newline)
773 * @return the pre/post-fixed and bracketed string
774 */
775 String prefix(String s, String pre, String post) {
776 if (s == null) {
777 return "";
778 }
779 if (!interactiveModeBegun) {
780 // messages expect to be new-line terminated (even when not prefixed)
781 return s + "%n";
782 }
783 String pp = s.replaceAll("\\R", post + pre);
784 if (pp.endsWith(post + pre)) {
785 // prevent an extra prefix char and blank line when the string
786 // already terminates with newline
787 pp = pp.substring(0, pp.length() - (post + pre).length());
788 }
789 return pre + pp + post;
790 }
791
792 /**
793 * Print using resource bundle look-up and adding prefix and postfix
794 *
795 * @param key the resource key
796 */
797 void hardrb(String key) {
798 hard(getResourceString(key));
799 }
800
801 /**
802 * Format using resource bundle look-up using MessageFormat
803 *
804 * @param key the resource key
805 * @param args
806 */
807 String messageFormat(String key, Object... args) {
808 String rs = getResourceString(key);
809 return MessageFormat.format(rs, args);
810 }
811
812 /**
813 * Print using resource bundle look-up, MessageFormat, and add prefix and
814 * postfix
815 *
816 * @param key the resource key
817 * @param args
818 */
819 @Override
820 public void hardmsg(String key, Object... args) {
821 hard(messageFormat(key, args));
822 }
823
824 /**
825 * Print error using resource bundle look-up, MessageFormat, and add prefix
826 * and postfix
827 *
828 * @param key the resource key
829 * @param args
830 */
831 @Override
832 public void errormsg(String key, Object... args) {
833 error(messageFormat(key, args));
834 }
835
836 /**
837 * Print (fluff) using resource bundle look-up, MessageFormat, and add
838 * prefix and postfix
839 *
840 * @param key the resource key
841 * @param args
842 */
843 @Override
844 public void fluffmsg(String key, Object... args) {
845 if (showFluff()) {
846 hardmsg(key, args);
847 }
848 }
849
850 <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
851 Map<String, String> a2b = stream.collect(toMap(a, b,
852 (m1, m2) -> m1,
853 LinkedHashMap::new));
854 for (Entry<String, String> e : a2b.entrySet()) {
855 hard("%s", e.getKey());
856 cmdout.printf(prefix(e.getValue(), feedback.getPre() + "\t", feedback.getPost()));
857 }
858 }
859
860 /**
861 * Trim whitespace off end of string
862 *
863 * @param s
864 * @return
865 */
866 static String trimEnd(String s) {
867 int last = s.length() - 1;
868 int i = last;
869 while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
870 --i;
871 }
872 if (i != last) {
873 return s.substring(0, i + 1);
874 } else {
875 return s;
876 }
877 }
878
879 /**
880 * The entry point into the JShell tool.
881 *
882 * @param args the command-line arguments
883 * @throws Exception catastrophic fatal exception
884 * @return the exit code
885 */
886 public int start(String[] args) throws Exception {
887 OptionParserCommandLine commandLineArgs = new OptionParserCommandLine();
888 options = commandLineArgs.parse(args);
889 if (options == null) {
890 // A null means end immediately, this may be an error or because
891 // of options like --version. Exit code has been set.
892 return exitCode;
893 }
894 startup = commandLineArgs.startup();
895 // initialize editor settings
896 configEditor();
897 // initialize JShell instance
898 try {
899 resetState();
900 } catch (IllegalStateException ex) {
901 // Display just the cause (not a exception backtrace)
902 cmderr.println(ex.getMessage());
903 //abort
904 return 1;
905 }
906 // Read replay history from last jshell session into previous history
907 replayableHistoryPrevious = ReplayableHistory.fromPrevious(prefs);
908 // load snippet/command files given on command-line
909 for (String loadFile : commandLineArgs.nonOptions()) {
910 if (!runFile(loadFile, "jshell")) {
911 // Load file failed -- abort
912 return 1;
913 }
914 }
915 // if we survived that...
916 if (regenerateOnDeath) {
917 // initialize the predefined feedback modes
918 initFeedback(commandLineArgs.feedbackMode());
919 }
920 // check again, as feedback setting could have failed
921 if (regenerateOnDeath) {
922 // if we haven't died, and the feedback mode wants fluff, print welcome
923 interactiveModeBegun = true;
924 if (feedback.shouldDisplayCommandFluff()) {
925 hardmsg("jshell.msg.welcome", version());
926 }
927 // Be sure history is always saved so that user code isn't lost
928 Thread shutdownHook = new Thread() {
929 @Override
930 public void run() {
931 replayableHistory.storeHistory(prefs);
932 }
933 };
934 Runtime.getRuntime().addShutdownHook(shutdownHook);
935 // execute from user input
936 try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
937 while (regenerateOnDeath) {
938 if (!live) {
939 resetState();
940 }
941 run(in);
942 }
943 } finally {
944 replayableHistory.storeHistory(prefs);
945 closeState();
946 try {
947 Runtime.getRuntime().removeShutdownHook(shutdownHook);
948 } catch (Exception ex) {
949 // ignore, this probably caused by VM aready being shutdown
950 // and this is the last act anyhow
951 }
952 }
953 }
954 closeState();
955 return exitCode;
956 }
957
958 private EditorSetting configEditor() {
959 // Read retained editor setting (if any)
960 editor = EditorSetting.fromPrefs(prefs);
961 if (editor != null) {
962 return editor;
963 }
964 // Try getting editor setting from OS environment variables
965 for (String envvar : EDITOR_ENV_VARS) {
966 String v = envvars.get(envvar);
967 if (v != null) {
968 return editor = new EditorSetting(v.split("\\s+"), false);
969 }
970 }
971 // Default to the built-in editor
972 return editor = BUILT_IN_EDITOR;
973 }
974
975 private void printUsage() {
976 cmdout.print(getResourceString("help.usage"));
977 }
978
979 private void printUsageX() {
980 cmdout.print(getResourceString("help.usage.x"));
981 }
982
983 /**
984 * Message handler to use during initial start-up.
985 */
986 private class InitMessageHandler implements MessageHandler {
987
988 @Override
989 public void fluff(String format, Object... args) {
990 //ignore
991 }
992
993 @Override
994 public void fluffmsg(String messageKey, Object... args) {
995 //ignore
996 }
997
998 @Override
999 public void hard(String format, Object... args) {
1000 //ignore
1001 }
1002
1003 @Override
1004 public void hardmsg(String messageKey, Object... args) {
1005 //ignore
1006 }
1007
1008 @Override
1009 public void errormsg(String messageKey, Object... args) {
1010 JShellTool.this.errormsg(messageKey, args);
1011 }
1012
1013 @Override
1014 public boolean showFluff() {
1015 return false;
1016 }
1017 }
1018
1019 private void resetState() {
1020 closeState();
1021
1022 // Initialize tool id mapping
1023 mainNamespace = new NameSpace("main", "");
1024 startNamespace = new NameSpace("start", "s");
1025 errorNamespace = new NameSpace("error", "e");
1026 mapSnippet = new LinkedHashMap<>();
1027 currentNameSpace = startNamespace;
1028
1029 // Reset the replayable history, saving the old for restore
1030 replayableHistoryPrevious = replayableHistory;
1031 replayableHistory = ReplayableHistory.emptyHistory();
1032 JShell.Builder builder =
1033 JShell.builder()
1034 .in(userin)
1035 .out(userout)
1036 .err(usererr)
1037 .tempVariableNameGenerator(() -> "$" + currentNameSpace.tidNext())
1038 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive())
1039 ? currentNameSpace.tid(sn)
1040 : errorNamespace.tid(sn))
1041 .remoteVMOptions(options.remoteVmOptions())
1042 .compilerOptions(options.compilerOptions());
1043 if (executionControlSpec != null) {
1044 builder.executionEngine(executionControlSpec);
1045 }
1046 state = builder.build();
1047 shutdownSubscription = state.onShutdown((JShell deadState) -> {
1048 if (deadState == state) {
1049 hardmsg("jshell.msg.terminated");
1050 fluffmsg("jshell.msg.terminated.restore");
1051 live = false;
1052 }
1053 });
1054 analysis = state.sourceCodeAnalysis();
1055 live = true;
1056
1057 // Run the start-up script.
1058 // Avoid an infinite loop running start-up while running start-up.
1059 // This could, otherwise, occur when /env /reset or /reload commands are
1060 // in the start-up script.
1061 if (!isCurrentlyRunningStartup) {
1062 try {
1063 isCurrentlyRunningStartup = true;
1064 startUpRun(startup.toString());
1065 } finally {
1066 isCurrentlyRunningStartup = false;
1067 }
1068 }
1069 // Record subsequent snippets in the main namespace.
1070 currentNameSpace = mainNamespace;
1071 }
1072
1073 //where -- one-time per run initialization of feedback modes
1074 private void initFeedback(String initMode) {
1075 // No fluff, no prefix, for init failures
1076 MessageHandler initmh = new InitMessageHandler();
1077 // Execute the feedback initialization code in the resource file
1078 startUpRun(getResourceString("startup.feedback"));
1079 // These predefined modes are read-only
1080 feedback.markModesReadOnly();
1081 // Restore user defined modes retained on previous run with /set mode -retain
1082 String encoded = prefs.get(MODE_KEY);
1083 if (encoded != null && !encoded.isEmpty()) {
1084 if (!feedback.restoreEncodedModes(initmh, encoded)) {
1085 // Catastrophic corruption -- remove the retained modes
1086 prefs.remove(MODE_KEY);
1087 }
1088 }
1089 if (initMode != null) {
1090 // The feedback mode to use was specified on the command line, use it
1091 if (!setFeedback(initmh, new ArgTokenizer("--feedback", initMode))) {
1092 regenerateOnDeath = false;
1093 exitCode = 1;
1094 }
1095 } else {
1096 String fb = prefs.get(FEEDBACK_KEY);
1097 if (fb != null) {
1098 // Restore the feedback mode to use that was retained
1099 // on a previous run with /set feedback -retain
1100 setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb));
1101 }
1102 }
1103 }
1104
1105 //where
1106 private void startUpRun(String start) {
1107 try (IOContext suin = new ScannerIOContext(new StringReader(start))) {
1108 run(suin);
1109 } catch (Exception ex) {
1110 errormsg("jshell.err.startup.unexpected.exception", ex);
1111 ex.printStackTrace(cmderr);
1112 }
1113 }
1114
1115 private void closeState() {
1116 live = false;
1117 JShell oldState = state;
1118 if (oldState != null) {
1119 state = null;
1120 analysis = null;
1121 oldState.unsubscribe(shutdownSubscription); // No notification
1122 oldState.close();
1123 }
1124 }
1125
1126 /**
1127 * Main loop
1128 *
1129 * @param in the line input/editing context
1130 */
1131 private void run(IOContext in) {
1132 IOContext oldInput = input;
1133 input = in;
1134 try {
1135 // remaining is the source left after one snippet is evaluated
1136 String remaining = "";
1137 while (live) {
1138 // Get a line(s) of input
1139 String src = getInput(remaining);
1140 // Process the snippet or command, returning the remaining source
1141 remaining = processInput(src);
1142 }
1143 } catch (EOFException ex) {
1144 // Just exit loop
1145 } catch (IOException ex) {
1146 errormsg("jshell.err.unexpected.exception", ex);
1147 } finally {
1148 input = oldInput;
1149 }
1150 }
1151
1152 /**
1153 * Process an input command or snippet.
1154 *
1155 * @param src the source to process
1156 * @return any remaining input to processed
1157 */
1158 private String processInput(String src) {
1159 if (isCommand(src)) {
1160 // It is a command
1161 processCommand(src.trim());
1162 // No remaining input after a command
1163 return "";
1164 } else {
1165 // It is a snipet. Separate the source from the remaining. Evaluate
1166 // the source
1167 CompletionInfo an = analysis.analyzeCompletion(src);
1168 if (processSourceCatchingReset(trimEnd(an.source()))) {
1169 // Snippet was successful use any leftover source
1170 return an.remaining();
1171 } else {
1172 // Snippet failed, throw away any remaining source
1173 return "";
1174 }
1175 }
1176 }
1177
1178 /**
1179 * Get the input line (or, if incomplete, lines).
1180 *
1181 * @param initial leading input (left over after last snippet)
1182 * @return the complete input snippet or command
1183 * @throws IOException on unexpected I/O error
1184 */
1185 private String getInput(String initial) throws IOException{
1186 String src = initial;
1187 while (live) { // loop while incomplete (and live)
1188 if (!src.isEmpty()) {
1189 // We have some source, see if it is complete, if so, use it
1190 String check;
1191
1192 if (isCommand(src)) {
1193 // A command can only be incomplete if it is a /exit with
1194 // an argument
1195 int sp = src.indexOf(" ");
1196 if (sp < 0) return src;
1197 check = src.substring(sp).trim();
1198 if (check.isEmpty()) return src;
1199 String cmd = src.substring(0, sp);
1200 Command[] match = findCommand(cmd, c -> c.kind.isRealCommand);
1201 if (match.length != 1 || !match[0].command.equals("/exit")) {
1202 // A command with no snippet arg, so no multi-line input
1203 return src;
1204 }
1205 } else {
1206 // For a snippet check the whole source
1207 check = src;
1208 }
1209 Completeness comp = analysis.analyzeCompletion(check).completeness();
1210 if (comp.isComplete() || comp == Completeness.EMPTY) {
1211 return src;
1212 }
1213 }
1214 String prompt = interactive()
1215 ? testPrompt
1216 ? src.isEmpty()
1217 ? "\u0005" //ENQ -- test prompt
1218 : "\u0006" //ACK -- test continuation prompt
1219 : src.isEmpty()
1220 ? feedback.getPrompt(currentNameSpace.tidNext())
1221 : feedback.getContinuationPrompt(currentNameSpace.tidNext())
1222 : "" // Non-interactive -- no prompt
1223 ;
1224 String line;
1225 try {
1226 line = input.readLine(prompt, src);
1227 } catch (InputInterruptedException ex) {
1228 //input interrupted - clearing current state
1229 src = "";
1230 continue;
1231 }
1232 if (line == null) {
1233 //EOF
1234 if (input.interactiveOutput()) {
1235 // End after user ctrl-D
1236 regenerateOnDeath = false;
1237 }
1238 throw new EOFException(); // no more input
1239 }
1240 src = src.isEmpty()
1241 ? line
1242 : src + "\n" + line;
1243 }
1244 throw new EOFException(); // not longer live
1245 }
1246
1247 private boolean isCommand(String line) {
1248 return line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*");
1249 }
1250
1251 private void addToReplayHistory(String s) {
1252 if (!isCurrentlyRunningStartup) {
1253 replayableHistory.add(s);
1254 }
1255 }
1256
1257 /**
1258 * Process a source snippet.
1259 *
1260 * @param src the snippet source to process
1261 * @return true on success, false on failure
1262 */
1263 private boolean processSourceCatchingReset(String src) {
1264 try {
1265 input.beforeUserCode();
1266 return processSource(src);
1267 } catch (IllegalStateException ex) {
1268 hard("Resetting...");
1269 live = false; // Make double sure
1270 return false;
1271 } finally {
1272 input.afterUserCode();
1273 }
1274 }
1275
1276 /**
1277 * Process a command (as opposed to a snippet) -- things that start with
1278 * slash.
1279 *
1280 * @param input
1281 */
1282 private void processCommand(String input) {
1283 if (input.startsWith("/-")) {
1284 try {
1285 //handle "/-[number]"
1286 cmdUseHistoryEntry(Integer.parseInt(input.substring(1)));
1287 return ;
1288 } catch (NumberFormatException ex) {
1289 //ignore
1290 }
1291 }
1292 String cmd;
1293 String arg;
1294 int idx = input.indexOf(' ');
1295 if (idx > 0) {
1296 arg = input.substring(idx + 1).trim();
1297 cmd = input.substring(0, idx);
1298 } else {
1299 cmd = input;
1300 arg = "";
1301 }
1302 // find the command as a "real command", not a pseudo-command or doc subject
1303 Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand);
1304 switch (candidates.length) {
1305 case 0:
1306 // not found, it is either a snippet command or an error
1307 if (ID.matcher(cmd.substring(1)).matches()) {
1308 // it is in the form of a snipppet id, see if it is a valid history reference
1309 rerunHistoryEntriesById(input);
1310 } else {
1311 errormsg("jshell.err.invalid.command", cmd);
1312 fluffmsg("jshell.msg.help.for.help");
1313 }
1314 break;
1315 case 1:
1316 Command command = candidates[0];
1317 // If comand was successful and is of a replayable kind, add it the replayable history
1318 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) {
1319 addToReplayHistory((command.command + " " + arg).trim());
1320 }
1321 break;
1322 default:
1323 // command if too short (ambigous), show the possibly matches
1324 errormsg("jshell.err.command.ambiguous", cmd,
1325 Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
1326 fluffmsg("jshell.msg.help.for.help");
1327 break;
1328 }
1329 }
1330
1331 private Command[] findCommand(String cmd, Predicate<Command> filter) {
1332 Command exact = commands.get(cmd);
1333 if (exact != null)
1334 return new Command[] {exact};
1335
1336 return commands.values()
1337 .stream()
1338 .filter(filter)
1339 .filter(command -> command.command.startsWith(cmd))
1340 .toArray(Command[]::new);
1341 }
1342
1343 static Path toPathResolvingUserHome(String pathString) {
1344 if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
1345 return Paths.get(System.getProperty("user.home"), pathString.substring(2));
1346 else
1347 return Paths.get(pathString);
1348 }
1349
1350 static final class Command {
1351 public final String command;
1352 public final String helpKey;
1353 public final Function<String,Boolean> run;
1354 public final CompletionProvider completions;
1355 public final CommandKind kind;
1356
1357 // NORMAL Commands
1358 public Command(String command, Function<String,Boolean> run, CompletionProvider completions) {
1359 this(command, run, completions, CommandKind.NORMAL);
1360 }
1361
1362 // Special kinds of Commands
1363 public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
1364 this(command, "help." + command.substring(1),
1365 run, completions, kind);
1366 }
1367
1368 // Documentation pseudo-commands
1369 public Command(String command, String helpKey, CommandKind kind) {
1370 this(command, helpKey,
1371 arg -> { throw new IllegalStateException(); },
1372 EMPTY_COMPLETION_PROVIDER,
1373 kind);
1374 }
1375
1376 public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
1377 this.command = command;
1378 this.helpKey = helpKey;
1379 this.run = run;
1380 this.completions = completions;
1381 this.kind = kind;
1382 }
1383
1384 }
1385
1386 interface CompletionProvider {
1387 List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
1388
1389 }
1390
1391 enum CommandKind {
1392 NORMAL(true, true, true),
1393 REPLAY(true, true, true),
1394 HIDDEN(true, false, false),
1395 HELP_ONLY(false, true, false),
1396 HELP_SUBJECT(false, false, false);
1397
1398 final boolean isRealCommand;
1399 final boolean showInHelp;
1400 final boolean shouldSuggestCompletions;
1401 private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) {
1402 this.isRealCommand = isRealCommand;
1403 this.showInHelp = showInHelp;
1404 this.shouldSuggestCompletions = shouldSuggestCompletions;
1405 }
1406 }
1407
1408 static final class FixedCompletionProvider implements CompletionProvider {
1409
1410 private final String[] alternatives;
1411
1412 public FixedCompletionProvider(String... alternatives) {
1413 this.alternatives = alternatives;
1414 }
1415
1416 // Add more options to an existing provider
1417 public FixedCompletionProvider(FixedCompletionProvider base, String... alternatives) {
1418 List<String> l = new ArrayList<>(Arrays.asList(base.alternatives));
1419 l.addAll(Arrays.asList(alternatives));
1420 this.alternatives = l.toArray(new String[l.size()]);
1421 }
1422
1423 @Override
1424 public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
1425 List<Suggestion> result = new ArrayList<>();
1426
1427 for (String alternative : alternatives) {
1428 if (alternative.startsWith(input)) {
1429 result.add(new ArgSuggestion(alternative));
1430 }
1431 }
1432
1433 anchor[0] = 0;
1434
1435 return result;
1436 }
1437
1438 }
1439
1440 static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
1441 private static final CompletionProvider SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start ", "-history");
1442 private static final CompletionProvider SAVE_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history ");
1443 private static final CompletionProvider SNIPPET_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start " );
1444 private static final FixedCompletionProvider COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
1445 "-class-path ", "-module-path ", "-add-modules ", "-add-exports ");
1446 private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
1447 COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER,
1448 "-restore ", "-quiet ");
1449 private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete");
1450 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
1451 private static final Map<String, CompletionProvider> ARG_OPTIONS = new HashMap<>();
1452 static {
1453 ARG_OPTIONS.put("-class-path", classPathCompletion());
1454 ARG_OPTIONS.put("-module-path", fileCompletions(Files::isDirectory));
1455 ARG_OPTIONS.put("-add-modules", EMPTY_COMPLETION_PROVIDER);
1456 ARG_OPTIONS.put("-add-exports", EMPTY_COMPLETION_PROVIDER);
1457 }
1458 private final Map<String, Command> commands = new LinkedHashMap<>();
1459 private void registerCommand(Command cmd) {
1460 commands.put(cmd.command, cmd);
1461 }
1462
1463 private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) {
1464 return (input, cursor, anchor) -> {
1465 List<Suggestion> result = Collections.emptyList();
1466
1467 int space = input.indexOf(' ');
1468 if (space != -1) {
1469 String rest = input.substring(space + 1);
1470 result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor);
1471 anchor[0] += space + 1;
1472 }
1473
1474 return result;
1475 };
1476 }
1477
1478 private static CompletionProvider fileCompletions(Predicate<Path> accept) {
1479 return (code, cursor, anchor) -> {
1480 int lastSlash = code.lastIndexOf('/');
1481 String path = code.substring(0, lastSlash + 1);
1482 String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
1483 Path current = toPathResolvingUserHome(path);
1484 List<Suggestion> result = new ArrayList<>();
1485 try (Stream<Path> dir = Files.list(current)) {
1486 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
1487 .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : "")))
1488 .forEach(result::add);
1489 } catch (IOException ex) {
1490 //ignore...
1491 }
1492 if (path.isEmpty()) {
1493 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
1494 .filter(root -> Files.exists(root))
1495 .filter(root -> accept.test(root) && root.toString().startsWith(prefix))
1496 .map(root -> new ArgSuggestion(root.toString()))
1497 .forEach(result::add);
1498 }
1499 anchor[0] = path.length();
1500 return result;
1501 };
1502 }
1503
1504 private static CompletionProvider classPathCompletion() {
1505 return fileCompletions(p -> Files.isDirectory(p) ||
1506 p.getFileName().toString().endsWith(".zip") ||
1507 p.getFileName().toString().endsWith(".jar"));
1508 }
1509
1510 // Completion based on snippet supplier
1511 private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) {
1512 return (prefix, cursor, anchor) -> {
1513 anchor[0] = 0;
1514 int space = prefix.lastIndexOf(' ');
1515 Set<String> prior = new HashSet<>(Arrays.asList(prefix.split(" ")));
1516 if (prior.contains("-all") || prior.contains("-history")) {
1517 return Collections.emptyList();
1518 }
1519 String argPrefix = prefix.substring(space + 1);
1520 return snippetsSupplier.get()
1521 .filter(k -> !prior.contains(String.valueOf(k.id()))
1522 && (!(k instanceof DeclarationSnippet)
1523 || !prior.contains(((DeclarationSnippet) k).name())))
1524 .flatMap(k -> (k instanceof DeclarationSnippet)
1525 ? Stream.of(String.valueOf(k.id()) + " ", ((DeclarationSnippet) k).name() + " ")
1526 : Stream.of(String.valueOf(k.id()) + " "))
1527 .filter(k -> k.startsWith(argPrefix))
1528 .map(ArgSuggestion::new)
1529 .collect(Collectors.toList());
1530 };
1531 }
1532
1533 // Completion based on snippet supplier with -all -start (and sometimes -history) options
1534 private CompletionProvider snippetWithOptionCompletion(CompletionProvider optionProvider,
1535 Supplier<Stream<? extends Snippet>> snippetsSupplier) {
1536 return (code, cursor, anchor) -> {
1537 List<Suggestion> result = new ArrayList<>();
1538 int pastSpace = code.lastIndexOf(' ') + 1; // zero if no space
1539 if (pastSpace == 0) {
1540 result.addAll(optionProvider.completionSuggestions(code, cursor, anchor));
1541 }
1542 result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor));
1543 anchor[0] += pastSpace;
1544 return result;
1545 };
1546 }
1547
1548 // Completion of help, commands and subjects
1549 private CompletionProvider helpCompletion() {
1550 return (code, cursor, anchor) -> {
1551 List<Suggestion> result;
1552 int pastSpace = code.indexOf(' ') + 1; // zero if no space
1553 if (pastSpace == 0) {
1554 // initially suggest commands (with slash) and subjects,
1555 // however, if their subject starts without slash, include
1556 // commands without slash
1557 boolean noslash = code.length() > 0 && !code.startsWith("/");
1558 result = new FixedCompletionProvider(commands.values().stream()
1559 .filter(cmd -> cmd.kind.showInHelp || cmd.kind == CommandKind.HELP_SUBJECT)
1560 .map(c -> ((noslash && c.command.startsWith("/"))
1561 ? c.command.substring(1)
1562 : c.command) + " ")
1563 .toArray(String[]::new))
1564 .completionSuggestions(code, cursor, anchor);
1565 } else if (code.startsWith("/se") || code.startsWith("se")) {
1566 result = new FixedCompletionProvider(SET_SUBCOMMANDS)
1567 .completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor);
1568 } else {
1569 result = Collections.emptyList();
1570 }
1571 anchor[0] += pastSpace;
1572 return result;
1573 };
1574 }
1575
1576 private static CompletionProvider saveCompletion() {
1577 return (code, cursor, anchor) -> {
1578 List<Suggestion> result = new ArrayList<>();
1579 int space = code.indexOf(' ');
1580 if (space == (-1)) {
1581 result.addAll(SAVE_OPTION_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
1582 }
1583 result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
1584 anchor[0] += space + 1;
1585 return result;
1586 };
1587 }
1588
1589 // command-line-like option completion -- options with values
1590 private static CompletionProvider optionCompletion(CompletionProvider provider) {
1591 return (code, cursor, anchor) -> {
1592 Matcher ovm = OPTION_VALUE_PATTERN.matcher(code);
1593 if (ovm.matches()) {
1594 String flag = ovm.group("flag");
1595 List<CompletionProvider> ps = ARG_OPTIONS.entrySet().stream()
1596 .filter(es -> es.getKey().startsWith(flag))
1597 .map(es -> es.getValue())
1598 .collect(toList());
1599 if (ps.size() == 1) {
1600 int pastSpace = ovm.start("val");
1601 List<Suggestion> result = ps.get(0).completionSuggestions(
1602 ovm.group("val"), cursor - pastSpace, anchor);
1603 anchor[0] += pastSpace;
1604 return result;
1605 }
1606 }
1607 Matcher om = OPTION_PATTERN.matcher(code);
1608 if (om.matches()) {
1609 int pastSpace = om.start("flag");
1610 List<Suggestion> result = provider.completionSuggestions(
1611 om.group("flag"), cursor - pastSpace, anchor);
1612 if (!om.group("dd").isEmpty()) {
1613 result = result.stream()
1614 .map(sug -> new Suggestion() {
1615 @Override
1616 public String continuation() {
1617 return "-" + sug.continuation();
1618 }
1619
1620 @Override
1621 public boolean matchesType() {
1622 return false;
1623 }
1624 })
1625 .collect(toList());
1626 --pastSpace;
1627 }
1628 anchor[0] += pastSpace;
1629 return result;
1630 }
1631 Matcher opp = OPTION_PRE_PATTERN.matcher(code);
1632 if (opp.matches()) {
1633 int pastSpace = opp.end();
1634 List<Suggestion> result = provider.completionSuggestions(
1635 "", cursor - pastSpace, anchor);
1636 anchor[0] += pastSpace;
1637 return result;
1638 }
1639 return Collections.emptyList();
1640 };
1641 }
1642
1643 // /reload command completion
1644 private static CompletionProvider reloadCompletion() {
1645 return optionCompletion(RELOAD_OPTIONS_COMPLETION_PROVIDER);
1646 }
1647
1648 // /env command completion
1649 private static CompletionProvider envCompletion() {
1650 return optionCompletion(COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER);
1651 }
1652
1653 private static CompletionProvider orMostSpecificCompletion(
1654 CompletionProvider left, CompletionProvider right) {
1655 return (code, cursor, anchor) -> {
1656 int[] leftAnchor = {-1};
1657 int[] rightAnchor = {-1};
1658
1659 List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor);
1660 List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor);
1661
1662 List<Suggestion> suggestions = new ArrayList<>();
1663
1664 if (leftAnchor[0] >= rightAnchor[0]) {
1665 anchor[0] = leftAnchor[0];
1666 suggestions.addAll(leftSuggestions);
1667 }
1668
1669 if (leftAnchor[0] <= rightAnchor[0]) {
1670 anchor[0] = rightAnchor[0];
1671 suggestions.addAll(rightSuggestions);
1672 }
1673
1674 return suggestions;
1675 };
1676 }
1677
1678 // Snippet lists
1679
1680 Stream<Snippet> allSnippets() {
1681 return state.snippets();
1682 }
1683
1684 Stream<Snippet> dropableSnippets() {
1685 return state.snippets()
1686 .filter(sn -> state.status(sn).isActive());
1687 }
1688
1689 Stream<VarSnippet> allVarSnippets() {
1690 return state.snippets()
1691 .filter(sn -> sn.kind() == Snippet.Kind.VAR)
1692 .map(sn -> (VarSnippet) sn);
1693 }
1694
1695 Stream<MethodSnippet> allMethodSnippets() {
1696 return state.snippets()
1697 .filter(sn -> sn.kind() == Snippet.Kind.METHOD)
1698 .map(sn -> (MethodSnippet) sn);
1699 }
1700
1701 Stream<TypeDeclSnippet> allTypeSnippets() {
1702 return state.snippets()
1703 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL)
1704 .map(sn -> (TypeDeclSnippet) sn);
1705 }
1706
1707 // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ...
1708
1709 {
1710 registerCommand(new Command("/list",
1711 this::cmdList,
1712 snippetWithOptionCompletion(SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER,
1713 this::allSnippets)));
1714 registerCommand(new Command("/edit",
1715 this::cmdEdit,
1716 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1717 this::allSnippets)));
1718 registerCommand(new Command("/drop",
1719 this::cmdDrop,
1720 snippetCompletion(this::dropableSnippets),
1721 CommandKind.REPLAY));
1722 registerCommand(new Command("/save",
1723 this::cmdSave,
1724 saveCompletion()));
1725 registerCommand(new Command("/open",
1726 this::cmdOpen,
1727 FILE_COMPLETION_PROVIDER));
1728 registerCommand(new Command("/vars",
1729 this::cmdVars,
1730 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1731 this::allVarSnippets)));
1732 registerCommand(new Command("/methods",
1733 this::cmdMethods,
1734 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1735 this::allMethodSnippets)));
1736 registerCommand(new Command("/types",
1737 this::cmdTypes,
1738 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1739 this::allTypeSnippets)));
1740 registerCommand(new Command("/imports",
1741 arg -> cmdImports(),
1742 EMPTY_COMPLETION_PROVIDER));
1743 registerCommand(new Command("/exit",
1744 arg -> cmdExit(arg),
1745 (sn, c, a) -> {
1746 if (analysis == null || sn.isEmpty()) {
1747 // No completions if uninitialized or snippet not started
1748 return Collections.emptyList();
1749 } else {
1750 // Give exit code an int context by prefixing the arg
1751 List<Suggestion> suggestions = analysis.completionSuggestions(INT_PREFIX + sn,
1752 INT_PREFIX.length() + c, a);
1753 a[0] -= INT_PREFIX.length();
1754 return suggestions;
1755 }
1756 }));
1757 registerCommand(new Command("/env",
1758 arg -> cmdEnv(arg),
1759 envCompletion()));
1760 registerCommand(new Command("/reset",
1761 arg -> cmdReset(arg),
1762 envCompletion()));
1763 registerCommand(new Command("/reload",
1764 this::cmdReload,
1765 reloadCompletion()));
1766 registerCommand(new Command("/history",
1767 arg -> cmdHistory(),
1768 EMPTY_COMPLETION_PROVIDER));
1769 registerCommand(new Command("/debug",
1770 this::cmdDebug,
1771 EMPTY_COMPLETION_PROVIDER,
1772 CommandKind.HIDDEN));
1773 registerCommand(new Command("/help",
1774 this::cmdHelp,
1775 helpCompletion()));
1776 registerCommand(new Command("/set",
1777 this::cmdSet,
1778 new ContinuousCompletionProvider(Map.of(
1779 // need more completion for format for usability
1780 "format", feedback.modeCompletions(),
1781 "truncation", feedback.modeCompletions(),
1782 "feedback", feedback.modeCompletions(),
1783 "mode", skipWordThenCompletion(orMostSpecificCompletion(
1784 feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER),
1785 SET_MODE_OPTIONS_COMPLETION_PROVIDER)),
1786 "prompt", feedback.modeCompletions(),
1787 "editor", fileCompletions(Files::isExecutable),
1788 "start", FILE_COMPLETION_PROVIDER),
1789 STARTSWITH_MATCHER)));
1790 registerCommand(new Command("/?",
1791 "help.quest",
1792 this::cmdHelp,
1793 helpCompletion(),
1794 CommandKind.NORMAL));
1795 registerCommand(new Command("/!",
1796 "help.bang",
1797 arg -> cmdUseHistoryEntry(-1),
1798 EMPTY_COMPLETION_PROVIDER,
1799 CommandKind.NORMAL));
1800
1801 // Documentation pseudo-commands
1802 registerCommand(new Command("/<id>",
1803 "help.id",
1804 arg -> cmdHelp("rerun"),
1805 EMPTY_COMPLETION_PROVIDER,
1806 CommandKind.HELP_ONLY));
1807 registerCommand(new Command("/-<n>",
1808 "help.previous",
1809 arg -> cmdHelp("rerun"),
1810 EMPTY_COMPLETION_PROVIDER,
1811 CommandKind.HELP_ONLY));
1812 registerCommand(new Command("intro",
1813 "help.intro",
1814 CommandKind.HELP_SUBJECT));
1815 registerCommand(new Command("shortcuts",
1816 "help.shortcuts",
1817 CommandKind.HELP_SUBJECT));
1818 registerCommand(new Command("context",
1819 "help.context",
1820 CommandKind.HELP_SUBJECT));
1821 registerCommand(new Command("rerun",
1822 "help.rerun",
1823 CommandKind.HELP_SUBJECT));
1824
1825 commandCompletions = new ContinuousCompletionProvider(
1826 commands.values().stream()
1827 .filter(c -> c.kind.shouldSuggestCompletions)
1828 .collect(toMap(c -> c.command, c -> c.completions)),
1829 STARTSWITH_MATCHER);
1830 }
1831
1832 private ContinuousCompletionProvider commandCompletions;
1833
1834 public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
1835 return commandCompletions.completionSuggestions(code, cursor, anchor);
1836 }
1837
1838 public List<String> commandDocumentation(String code, int cursor, boolean shortDescription) {
1839 code = code.substring(0, cursor).replaceAll("\\h+", " ");
1840 String stripped = code.replaceFirst("/help ", "");
1841 boolean inHelp = !code.equals(stripped);
1842 int space = stripped.indexOf(' ');
1843 String prefix = space != (-1) ? stripped.substring(0, space) : stripped;
1844 List<String> result = new ArrayList<>();
1845
1846 List<Entry<String, String>> toShow;
1847
1848 if (stripped.matches("/set .*") || stripped.matches("set .*")) {
1849 String setSubcommand = stripped.replaceFirst("/?set ([^ ]*)($| .*)", "$1");
1850 toShow =
1851 Arrays.stream(SET_SUBCOMMANDS)
1852 .filter(s -> s.startsWith(setSubcommand))
1853 .map(s -> new SimpleEntry<>("/set " + s, "help.set." + s))
1854 .collect(Collectors.toList());
1855 } else {
1856 toShow =
1857 commands.values()
1858 .stream()
1859 .filter(c -> c.command.startsWith(prefix)
1860 || c.command.substring(1).startsWith(prefix))
1861 .filter(c -> c.kind.showInHelp ||
1862 (inHelp && c.kind == CommandKind.HELP_SUBJECT))
1863 .sorted((c1, c2) -> c1.command.compareTo(c2.command))
1864 .map(c -> new SimpleEntry<>(c.command, c.helpKey))
1865 .collect(Collectors.toList());
1866 }
1867
1868 if (toShow.size() == 1 && !inHelp) {
1869 result.add(getResourceString(toShow.get(0).getValue() + (shortDescription ? ".summary" : "")));
1870 } else {
1871 for (Entry<String, String> e : toShow) {
1872 result.add(e.getKey() + "\n" + getResourceString(e.getValue() + (shortDescription ? ".summary" : "")));
1873 }
1874 }
1875
1876 return result;
1877 }
1878
1879 // Attempt to stop currently running evaluation
1880 void stop() {
1881 state.stop();
1882 }
1883
1884 // --- Command implementations ---
1885
1886 private static final String[] SET_SUBCOMMANDS = new String[]{
1887 "format", "truncation", "feedback", "mode", "prompt", "editor", "start"};
1888
1889 final boolean cmdSet(String arg) {
1890 String cmd = "/set";
1891 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim());
1892 String which = subCommand(cmd, at, SET_SUBCOMMANDS);
1893 if (which == null) {
1894 return false;
1895 }
1896 switch (which) {
1897 case "_retain": {
1898 errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole());
1899 return false;
1900 }
1901 case "_blank": {
1902 // show top-level settings
1903 new SetEditor().set();
1904 showSetStart();
1905 setFeedback(this, at); // no args so shows feedback setting
1906 hardmsg("jshell.msg.set.show.mode.settings");
1907 return true;
1908 }
1909 case "format":
1910 return feedback.setFormat(this, at);
1911 case "truncation":
1912 return feedback.setTruncation(this, at);
1913 case "feedback":
1914 return setFeedback(this, at);
1915 case "mode":
1916 return feedback.setMode(this, at,
1917 retained -> prefs.put(MODE_KEY, retained));
1918 case "prompt":
1919 return feedback.setPrompt(this, at);
1920 case "editor":
1921 return new SetEditor(at).set();
1922 case "start":
1923 return setStart(at);
1924 default:
1925 errormsg("jshell.err.arg", cmd, at.val());
1926 return false;
1927 }
1928 }
1929
1930 boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) {
1931 return feedback.setFeedback(messageHandler, at,
1932 fb -> prefs.put(FEEDBACK_KEY, fb));
1933 }
1934
1935 // Find which, if any, sub-command matches.
1936 // Return null on error
1937 String subCommand(String cmd, ArgTokenizer at, String[] subs) {
1938 at.allowedOptions("-retain");
1939 String sub = at.next();
1940 if (sub == null) {
1941 // No sub-command was given
1942 return at.hasOption("-retain")
1943 ? "_retain"
1944 : "_blank";
1945 }
1946 String[] matches = Arrays.stream(subs)
1947 .filter(s -> s.startsWith(sub))
1948 .toArray(String[]::new);
1949 if (matches.length == 0) {
1950 // There are no matching sub-commands
1951 errormsg("jshell.err.arg", cmd, sub);
1952 fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs)
1953 .collect(Collectors.joining(", "))
1954 );
1955 return null;
1956 }
1957 if (matches.length > 1) {
1958 // More than one sub-command matches the initial characters provided
1959 errormsg("jshell.err.sub.ambiguous", cmd, sub);
1960 fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches)
1961 .collect(Collectors.joining(", "))
1962 );
1963 return null;
1964 }
1965 return matches[0];
1966 }
1967
1968 static class EditorSetting {
1969
1970 static String BUILT_IN_REP = "-default";
1971 static char WAIT_PREFIX = '-';
1972 static char NORMAL_PREFIX = '*';
1973
1974 final String[] cmd;
1975 final boolean wait;
1976
1977 EditorSetting(String[] cmd, boolean wait) {
1978 this.wait = wait;
1979 this.cmd = cmd;
1980 }
1981
1982 // returns null if not stored in preferences
1983 static EditorSetting fromPrefs(PersistentStorage prefs) {
1984 // Read retained editor setting (if any)
1985 String editorString = prefs.get(EDITOR_KEY);
1986 if (editorString == null || editorString.isEmpty()) {
1987 return null;
1988 } else if (editorString.equals(BUILT_IN_REP)) {
1989 return BUILT_IN_EDITOR;
1990 } else {
1991 boolean wait = false;
1992 char waitMarker = editorString.charAt(0);
1993 if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) {
1994 wait = waitMarker == WAIT_PREFIX;
1995 editorString = editorString.substring(1);
1996 }
1997 String[] cmd = editorString.split(RECORD_SEPARATOR);
1998 return new EditorSetting(cmd, wait);
1999 }
2000 }
2001
2002 static void removePrefs(PersistentStorage prefs) {
2003 prefs.remove(EDITOR_KEY);
2004 }
2005
2006 void toPrefs(PersistentStorage prefs) {
2007 prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR)
2008 ? BUILT_IN_REP
2009 : (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd));
2010 }
2011
2012 @Override
2013 public boolean equals(Object o) {
2014 if (o instanceof EditorSetting) {
2015 EditorSetting ed = (EditorSetting) o;
2016 return Arrays.equals(cmd, ed.cmd) && wait == ed.wait;
2017 } else {
2018 return false;
2019 }
2020 }
2021
2022 @Override
2023 public int hashCode() {
2024 int hash = 7;
2025 hash = 71 * hash + Arrays.deepHashCode(this.cmd);
2026 hash = 71 * hash + (this.wait ? 1 : 0);
2027 return hash;
2028 }
2029 }
2030
2031 class SetEditor {
2032
2033 private final ArgTokenizer at;
2034 private final String[] command;
2035 private final boolean hasCommand;
2036 private final boolean defaultOption;
2037 private final boolean deleteOption;
2038 private final boolean waitOption;
2039 private final boolean retainOption;
2040 private final int primaryOptionCount;
2041
2042 SetEditor(ArgTokenizer at) {
2043 at.allowedOptions("-default", "-wait", "-retain", "-delete");
2044 String prog = at.next();
2045 List<String> ed = new ArrayList<>();
2046 while (at.val() != null) {
2047 ed.add(at.val());
2048 at.nextToken(); // so that options are not interpreted as jshell options
2049 }
2050 this.at = at;
2051 this.command = ed.toArray(new String[ed.size()]);
2052 this.hasCommand = command.length > 0;
2053 this.defaultOption = at.hasOption("-default");
2054 this.deleteOption = at.hasOption("-delete");
2055 this.waitOption = at.hasOption("-wait");
2056 this.retainOption = at.hasOption("-retain");
2057 this.primaryOptionCount = (hasCommand? 1 : 0) + (defaultOption? 1 : 0) + (deleteOption? 1 : 0);
2058 }
2059
2060 SetEditor() {
2061 this(new ArgTokenizer("", ""));
2062 }
2063
2064 boolean set() {
2065 if (!check()) {
2066 return false;
2067 }
2068 if (primaryOptionCount == 0 && !retainOption) {
2069 // No settings or -retain, so this is a query
2070 EditorSetting retained = EditorSetting.fromPrefs(prefs);
2071 if (retained != null) {
2072 // retained editor is set
2073 hard("/set editor -retain %s", format(retained));
2074 }
2075 if (retained == null || !retained.equals(editor)) {
2076 // editor is not retained or retained is different from set
2077 hard("/set editor %s", format(editor));
2078 }
2079 return true;
2080 }
2081 if (retainOption && deleteOption) {
2082 EditorSetting.removePrefs(prefs);
2083 }
2084 install();
2085 if (retainOption && !deleteOption) {
2086 editor.toPrefs(prefs);
2087 fluffmsg("jshell.msg.set.editor.retain", format(editor));
2088 }
2089 return true;
2090 }
2091
2092 private boolean check() {
2093 if (!checkOptionsAndRemainingInput(at)) {
2094 return false;
2095 }
2096 if (primaryOptionCount > 1) {
2097 errormsg("jshell.err.default.option.or.program", at.whole());
2098 return false;
2099 }
2100 if (waitOption && !hasCommand) {
2101 errormsg("jshell.err.wait.applies.to.external.editor", at.whole());
2102 return false;
2103 }
2104 return true;
2105 }
2106
2107 private void install() {
2108 if (hasCommand) {
2109 editor = new EditorSetting(command, waitOption);
2110 } else if (defaultOption) {
2111 editor = BUILT_IN_EDITOR;
2112 } else if (deleteOption) {
2113 configEditor();
2114 } else {
2115 return;
2116 }
2117 fluffmsg("jshell.msg.set.editor.set", format(editor));
2118 }
2119
2120 private String format(EditorSetting ed) {
2121 if (ed == BUILT_IN_EDITOR) {
2122 return "-default";
2123 } else {
2124 Stream<String> elems = Arrays.stream(ed.cmd);
2125 if (ed.wait) {
2126 elems = Stream.concat(Stream.of("-wait"), elems);
2127 }
2128 return elems.collect(joining(" "));
2129 }
2130 }
2131 }
2132
2133 // The sub-command: /set start <start-file>
2134 boolean setStart(ArgTokenizer at) {
2135 at.allowedOptions("-default", "-none", "-retain");
2136 List<String> fns = new ArrayList<>();
2137 while (at.next() != null) {
2138 fns.add(at.val());
2139 }
2140 if (!checkOptionsAndRemainingInput(at)) {
2141 return false;
2142 }
2143 boolean defaultOption = at.hasOption("-default");
2144 boolean noneOption = at.hasOption("-none");
2145 boolean retainOption = at.hasOption("-retain");
2146 boolean hasFile = !fns.isEmpty();
2147
2148 int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0);
2149 if (argCount > 1) {
2150 errormsg("jshell.err.option.or.filename", at.whole());
2151 return false;
2152 }
2153 if (argCount == 0 && !retainOption) {
2154 // no options or filename, show current setting
2155 showSetStart();
2156 return true;
2157 }
2158 if (hasFile) {
2159 startup = Startup.fromFileList(fns, "/set start", this);
2160 if (startup == null) {
2161 return false;
2162 }
2163 } else if (defaultOption) {
2164 startup = Startup.defaultStartup(this);
2165 } else if (noneOption) {
2166 startup = Startup.noStartup();
2167 }
2168 if (retainOption) {
2169 // retain startup setting
2170 prefs.put(STARTUP_KEY, startup.storedForm());
2171 }
2172 return true;
2173 }
2174
2175 // show the "/set start" settings (retained and, if different, current)
2176 // as commands (and file contents). All commands first, then contents.
2177 void showSetStart() {
2178 StringBuilder sb = new StringBuilder();
2179 String retained = prefs.get(STARTUP_KEY);
2180 if (retained != null) {
2181 Startup retainedStart = Startup.unpack(retained, this);
2182 boolean currentDifferent = !startup.equals(retainedStart);
2183 sb.append(retainedStart.show(true));
2184 if (currentDifferent) {
2185 sb.append(startup.show(false));
2186 }
2187 sb.append(retainedStart.showDetail());
2188 if (currentDifferent) {
2189 sb.append(startup.showDetail());
2190 }
2191 } else {
2192 sb.append(startup.show(false));
2193 sb.append(startup.showDetail());
2194 }
2195 hard(sb.toString());
2196 }
2197
2198 boolean cmdDebug(String arg) {
2199 if (arg.isEmpty()) {
2200 debug = !debug;
2201 InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0);
2202 fluff("Debugging %s", debug ? "on" : "off");
2203 } else {
2204 int flags = 0;
2205 for (char ch : arg.toCharArray()) {
2206 switch (ch) {
2207 case '0':
2208 flags = 0;
2209 debug = false;
2210 fluff("Debugging off");
2211 break;
2212 case 'r':
2213 debug = true;
2214 fluff("REPL tool debugging on");
2215 break;
2216 case 'g':
2217 flags |= DBG_GEN;
2218 fluff("General debugging on");
2219 break;
2220 case 'f':
2221 flags |= DBG_FMGR;
2222 fluff("File manager debugging on");
2223 break;
2224 case 'c':
2225 flags |= DBG_COMPA;
2226 fluff("Completion analysis debugging on");
2227 break;
2228 case 'd':
2229 flags |= DBG_DEP;
2230 fluff("Dependency debugging on");
2231 break;
2232 case 'e':
2233 flags |= DBG_EVNT;
2234 fluff("Event debugging on");
2235 break;
2236 case 'w':
2237 flags |= DBG_WRAP;
2238 fluff("Wrap debugging on");
2239 break;
2240 default:
2241 error("Unknown debugging option: %c", ch);
2242 fluff("Use: 0 r g f c d e w");
2243 return false;
2244 }
2245 }
2246 InternalDebugControl.setDebugFlags(state, flags);
2247 }
2248 return true;
2249 }
2250
2251 private boolean cmdExit(String arg) {
2252 if (!arg.trim().isEmpty()) {
2253 debug("Compiling exit: %s", arg);
2254 List<SnippetEvent> events = state.eval(arg);
2255 for (SnippetEvent e : events) {
2256 // Only care about main snippet
2257 if (e.causeSnippet() == null) {
2258 Snippet sn = e.snippet();
2259
2260 // Show any diagnostics
2261 List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
2262 String source = sn.source();
2263 displayDiagnostics(source, diagnostics);
2264
2265 // Show any exceptions
2266 if (e.exception() != null && e.status() != Status.REJECTED) {
2267 if (displayException(e.exception())) {
2268 // Abort: an exception occurred (reported)
2269 return false;
2270 }
2271 }
2272
2273 if (e.status() != Status.VALID) {
2274 // Abort: can only use valid snippets, diagnostics have been reported (above)
2275 return false;
2276 }
2277 String typeName;
2278 if (sn.kind() == Kind.EXPRESSION) {
2279 typeName = ((ExpressionSnippet) sn).typeName();
2280 } else if (sn.subKind() == TEMP_VAR_EXPRESSION_SUBKIND) {
2281 typeName = ((VarSnippet) sn).typeName();
2282 } else {
2283 // Abort: not an expression
2284 errormsg("jshell.err.exit.not.expression", arg);
2285 return false;
2286 }
2287 switch (typeName) {
2288 case "int":
2289 case "Integer":
2290 case "byte":
2291 case "Byte":
2292 case "short":
2293 case "Short":
2294 try {
2295 int i = Integer.parseInt(e.value());
2296 /**
2297 addToReplayHistory("/exit " + arg);
2298 replayableHistory.storeHistory(prefs);
2299 closeState();
2300 try {
2301 input.close();
2302 } catch (Exception exc) {
2303 // ignore
2304 }
2305 * **/
2306 exitCode = i;
2307 break;
2308 } catch (NumberFormatException exc) {
2309 // Abort: bad value
2310 errormsg("jshell.err.exit.bad.value", arg, e.value());
2311 return false;
2312 }
2313 default:
2314 // Abort: bad type
2315 errormsg("jshell.err.exit.bad.type", arg, typeName);
2316 return false;
2317 }
2318 }
2319 }
2320 }
2321 regenerateOnDeath = false;
2322 live = false;
2323 if (exitCode == 0) {
2324 fluffmsg("jshell.msg.goodbye");
2325 } else {
2326 fluffmsg("jshell.msg.goodbye.value", exitCode);
2327 }
2328 return true;
2329 }
2330
2331 boolean cmdHelp(String arg) {
2332 ArgTokenizer at = new ArgTokenizer("/help", arg);
2333 String subject = at.next();
2334 if (subject != null) {
2335 // check if the requested subject is a help subject or
2336 // a command, with or without slash
2337 Command[] matches = commands.values().stream()
2338 .filter(c -> c.command.startsWith(subject)
2339 || c.command.substring(1).startsWith(subject))
2340 .toArray(Command[]::new);
2341 if (matches.length == 1) {
2342 String cmd = matches[0].command;
2343 if (cmd.equals("/set")) {
2344 // Print the help doc for the specified sub-command
2345 String which = subCommand(cmd, at, SET_SUBCOMMANDS);
2346 if (which == null) {
2347 return false;
2348 }
2349 if (!which.equals("_blank")) {
2350 hardrb("help.set." + which);
2351 return true;
2352 }
2353 }
2354 }
2355 if (matches.length > 0) {
2356 for (Command c : matches) {
2357 hard("");
2358 hard("%s", c.command);
2359 hard("");
2360 hardrb(c.helpKey);
2361 }
2362 return true;
2363 } else {
2364 // failing everything else, check if this is the start of
2365 // a /set sub-command name
2366 String[] subs = Arrays.stream(SET_SUBCOMMANDS)
2367 .filter(s -> s.startsWith(subject))
2368 .toArray(String[]::new);
2369 if (subs.length > 0) {
2370 for (String sub : subs) {
2371 hardrb("help.set." + sub);
2372 hard("");
2373 }
2374 return true;
2375 }
2376 errormsg("jshell.err.help.arg", arg);
2377 }
2378 }
2379 hardmsg("jshell.msg.help.begin");
2380 hardPairs(commands.values().stream()
2381 .filter(cmd -> cmd.kind.showInHelp),
2382 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"),
2383 cmd -> getResourceString(cmd.helpKey + ".summary")
2384 );
2385 hardmsg("jshell.msg.help.subject");
2386 hardPairs(commands.values().stream()
2387 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT),
2388 cmd -> cmd.command,
2389 cmd -> getResourceString(cmd.helpKey + ".summary")
2390 );
2391 return true;
2392 }
2393
2394 private boolean cmdHistory() {
2395 cmdout.println();
2396 for (String s : input.currentSessionHistory()) {
2397 // No number prefix, confusing with snippet ids
2398 cmdout.printf("%s\n", s);
2399 }
2400 return true;
2401 }
2402
2403 /**
2404 * Avoid parameterized varargs possible heap pollution warning.
2405 */
2406 private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { }
2407
2408 /**
2409 * Apply filters to a stream until one that is non-empty is found.
2410 * Adapted from Stuart Marks
2411 *
2412 * @param supplier Supply the Snippet stream to filter
2413 * @param filters Filters to attempt
2414 * @return The non-empty filtered Stream, or null
2415 */
2416 @SafeVarargs
2417 private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier,
2418 SnippetPredicate<T>... filters) {
2419 for (SnippetPredicate<T> filt : filters) {
2420 Iterator<T> iterator = supplier.get().filter(filt).iterator();
2421 if (iterator.hasNext()) {
2422 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
2423 }
2424 }
2425 return null;
2426 }
2427
2428 private boolean inStartUp(Snippet sn) {
2429 return mapSnippet.get(sn).space == startNamespace;
2430 }
2431
2432 private boolean isActive(Snippet sn) {
2433 return state.status(sn).isActive();
2434 }
2435
2436 private boolean mainActive(Snippet sn) {
2437 return !inStartUp(sn) && isActive(sn);
2438 }
2439
2440 private boolean matchingDeclaration(Snippet sn, String name) {
2441 return sn instanceof DeclarationSnippet
2442 && ((DeclarationSnippet) sn).name().equals(name);
2443 }
2444
2445 /**
2446 * Convert user arguments to a Stream of snippets referenced by those
2447 * arguments (or lack of arguments).
2448 *
2449 * @param snippets the base list of possible snippets
2450 * @param defFilter the filter to apply to the arguments if no argument
2451 * @param rawargs the user's argument to the command, maybe be the empty
2452 * string
2453 * @return a Stream of referenced snippets or null if no matches are found
2454 */
2455 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
2456 Predicate<Snippet> defFilter, String rawargs, String cmd) {
2457 ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim());
2458 at.allowedOptions("-all", "-start");
2459 return argsOptionsToSnippets(snippetSupplier, defFilter, at);
2460 }
2461
2462 /**
2463 * Convert user arguments to a Stream of snippets referenced by those
2464 * arguments (or lack of arguments).
2465 *
2466 * @param snippets the base list of possible snippets
2467 * @param defFilter the filter to apply to the arguments if no argument
2468 * @param at the ArgTokenizer, with allowed options set
2469 * @return
2470 */
2471 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
2472 Predicate<Snippet> defFilter, ArgTokenizer at) {
2473 List<String> args = new ArrayList<>();
2474 String s;
2475 while ((s = at.next()) != null) {
2476 args.add(s);
2477 }
2478 if (!checkOptionsAndRemainingInput(at)) {
2479 return null;
2480 }
2481 if (at.optionCount() > 0 && args.size() > 0) {
2482 errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole());
2483 return null;
2484 }
2485 if (at.optionCount() > 1) {
2486 errormsg("jshell.err.conflicting.options", at.whole());
2487 return null;
2488 }
2489 if (at.isAllowedOption("-all") && at.hasOption("-all")) {
2490 // all snippets including start-up, failed, and overwritten
2491 return snippetSupplier.get();
2492 }
2493 if (at.isAllowedOption("-start") && at.hasOption("-start")) {
2494 // start-up snippets
2495 return snippetSupplier.get()
2496 .filter(this::inStartUp);
2497 }
2498 if (args.isEmpty()) {
2499 // Default is all active user snippets
2500 return snippetSupplier.get()
2501 .filter(defFilter);
2502 }
2503 return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args);
2504 }
2505
2506 /**
2507 * Support for converting arguments that are definition names, snippet ids,
2508 * or snippet id ranges into a stream of snippets,
2509 *
2510 * @param <T> the snipper subtype
2511 */
2512 private class ArgToSnippets<T extends Snippet> {
2513
2514 // the supplier of snippet streams
2515 final Supplier<Stream<T>> snippetSupplier;
2516 // these two are parallel, and lazily filled if a range is encountered
2517 List<T> allSnippets;
2518 String[] allIds = null;
2519
2520 /**
2521 *
2522 * @param snippetSupplier the base list of possible snippets
2523 */
2524 ArgToSnippets(Supplier<Stream<T>> snippetSupplier) {
2525 this.snippetSupplier = snippetSupplier;
2526 }
2527
2528 /**
2529 * Convert user arguments to a Stream of snippets referenced by those
2530 * arguments.
2531 *
2532 * @param args the user's argument to the command, maybe be the empty
2533 * list
2534 * @return a Stream of referenced snippets or null if no matches to
2535 * specific arg
2536 */
2537 Stream<T> argsToSnippets(List<String> args) {
2538 Stream<T> result = null;
2539 for (String arg : args) {
2540 // Find the best match
2541 Stream<T> st = argToSnippets(arg);
2542 if (st == null) {
2543 return null;
2544 } else {
2545 result = (result == null)
2546 ? st
2547 : Stream.concat(result, st);
2548 }
2549 }
2550 return result;
2551 }
2552
2553 /**
2554 * Convert a user argument to a Stream of snippets referenced by the
2555 * argument.
2556 *
2557 * @param snippetSupplier the base list of possible snippets
2558 * @param arg the user's argument to the command
2559 * @return a Stream of referenced snippets or null if no matches to
2560 * specific arg
2561 */
2562 Stream<T> argToSnippets(String arg) {
2563 if (arg.contains("-")) {
2564 return range(arg);
2565 }
2566 // Find the best match
2567 Stream<T> st = layeredSnippetSearch(snippetSupplier, arg);
2568 if (st == null) {
2569 badSnippetErrormsg(arg);
2570 return null;
2571 } else {
2572 return st;
2573 }
2574 }
2575
2576 /**
2577 * Look for inappropriate snippets to give best error message
2578 *
2579 * @param arg the bad snippet arg
2580 * @param errKey the not found error key
2581 */
2582 void badSnippetErrormsg(String arg) {
2583 Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
2584 if (est == null) {
2585 if (ID.matcher(arg).matches()) {
2586 errormsg("jshell.err.no.snippet.with.id", arg);
2587 } else {
2588 errormsg("jshell.err.no.such.snippets", arg);
2589 }
2590 } else {
2591 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command",
2592 arg, est.findFirst().get().source());
2593 }
2594 }
2595
2596 /**
2597 * Search through the snippets for the best match to the id/name.
2598 *
2599 * @param <R> the snippet type
2600 * @param aSnippetSupplier the supplier of snippet streams
2601 * @param arg the arg to match
2602 * @return a Stream of referenced snippets or null if no matches to
2603 * specific arg
2604 */
2605 <R extends Snippet> Stream<R> layeredSnippetSearch(Supplier<Stream<R>> aSnippetSupplier, String arg) {
2606 return nonEmptyStream(
2607 // the stream supplier
2608 aSnippetSupplier,
2609 // look for active user declarations matching the name
2610 sn -> isActive(sn) && matchingDeclaration(sn, arg),
2611 // else, look for any declarations matching the name
2612 sn -> matchingDeclaration(sn, arg),
2613 // else, look for an id of this name
2614 sn -> sn.id().equals(arg)
2615 );
2616 }
2617
2618 /**
2619 * Given an id1-id2 range specifier, return a stream of snippets within
2620 * our context
2621 *
2622 * @param arg the range arg
2623 * @return a Stream of referenced snippets or null if no matches to
2624 * specific arg
2625 */
2626 Stream<T> range(String arg) {
2627 int dash = arg.indexOf('-');
2628 String iid = arg.substring(0, dash);
2629 String tid = arg.substring(dash + 1);
2630 int iidx = snippetIndex(iid);
2631 if (iidx < 0) {
2632 return null;
2633 }
2634 int tidx = snippetIndex(tid);
2635 if (tidx < 0) {
2636 return null;
2637 }
2638 if (tidx < iidx) {
2639 errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid);
2640 return null;
2641 }
2642 return allSnippets.subList(iidx, tidx+1).stream();
2643 }
2644
2645 /**
2646 * Lazily initialize the id mapping -- needed only for id ranges.
2647 */
2648 void initIdMapping() {
2649 if (allIds == null) {
2650 allSnippets = snippetSupplier.get()
2651 .sorted((a, b) -> order(a) - order(b))
2652 .collect(toList());
2653 allIds = allSnippets.stream()
2654 .map(sn -> sn.id())
2655 .toArray(n -> new String[n]);
2656 }
2657 }
2658
2659 /**
2660 * Return all the snippet ids -- within the context, and in order.
2661 *
2662 * @return the snippet ids
2663 */
2664 String[] allIds() {
2665 initIdMapping();
2666 return allIds;
2667 }
2668
2669 /**
2670 * Establish an order on snippet ids. All startup snippets are first,
2671 * all error snippets are last -- within that is by snippet number.
2672 *
2673 * @param id the id string
2674 * @return an ordering int
2675 */
2676 int order(String id) {
2677 try {
2678 switch (id.charAt(0)) {
2679 case 's':
2680 return Integer.parseInt(id.substring(1));
2681 case 'e':
2682 return 0x40000000 + Integer.parseInt(id.substring(1));
2683 default:
2684 return 0x20000000 + Integer.parseInt(id);
2685 }
2686 } catch (Exception ex) {
2687 return 0x60000000;
2688 }
2689 }
2690
2691 /**
2692 * Establish an order on snippets, based on its snippet id. All startup
2693 * snippets are first, all error snippets are last -- within that is by
2694 * snippet number.
2695 *
2696 * @param sn the id string
2697 * @return an ordering int
2698 */
2699 int order(Snippet sn) {
2700 return order(sn.id());
2701 }
2702
2703 /**
2704 * Find the index into the parallel allSnippets and allIds structures.
2705 *
2706 * @param s the snippet id name
2707 * @return the index, or, if not found, report the error and return a
2708 * negative number
2709 */
2710 int snippetIndex(String s) {
2711 int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s,
2712 (a, b) -> order(a) - order(b));
2713 if (idx < 0) {
2714 // the id is not in the snippet domain, find the right error to report
2715 if (!ID.matcher(s).matches()) {
2716 errormsg("jshell.err.range.requires.id", s);
2717 } else {
2718 badSnippetErrormsg(s);
2719 }
2720 }
2721 return idx;
2722 }
2723
2724 }
2725
2726 private boolean cmdDrop(String rawargs) {
2727 ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim());
2728 at.allowedOptions();
2729 List<String> args = new ArrayList<>();
2730 String s;
2731 while ((s = at.next()) != null) {
2732 args.add(s);
2733 }
2734 if (!checkOptionsAndRemainingInput(at)) {
2735 return false;
2736 }
2737 if (args.isEmpty()) {
2738 errormsg("jshell.err.drop.arg");
2739 return false;
2740 }
2741 Stream<Snippet> stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args);
2742 if (stream == null) {
2743 // Snippet not found. Error already printed
2744 fluffmsg("jshell.msg.see.classes.etc");
2745 return false;
2746 }
2747 stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent));
2748 return true;
2749 }
2750
2751 private boolean cmdEdit(String arg) {
2752 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
2753 this::mainActive, arg, "/edit");
2754 if (stream == null) {
2755 return false;
2756 }
2757 Set<String> srcSet = new LinkedHashSet<>();
2758 stream.forEachOrdered(sn -> {
2759 String src = sn.source();
2760 switch (sn.subKind()) {
2761 case VAR_VALUE_SUBKIND:
2762 break;
2763 case ASSIGNMENT_SUBKIND:
2764 case OTHER_EXPRESSION_SUBKIND:
2765 case TEMP_VAR_EXPRESSION_SUBKIND:
2766 case UNKNOWN_SUBKIND:
2767 if (!src.endsWith(";")) {
2768 src = src + ";";
2769 }
2770 srcSet.add(src);
2771 break;
2772 case STATEMENT_SUBKIND:
2773 if (src.endsWith("}")) {
2774 // Could end with block or, for example, new Foo() {...}
2775 // so, we need deeper analysis to know if it needs a semicolon
2776 src = analysis.analyzeCompletion(src).source();
2777 } else if (!src.endsWith(";")) {
2778 src = src + ";";
2779 }
2780 srcSet.add(src);
2781 break;
2782 default:
2783 srcSet.add(src);
2784 break;
2785 }
2786 });
2787 StringBuilder sb = new StringBuilder();
2788 for (String s : srcSet) {
2789 sb.append(s);
2790 sb.append('\n');
2791 }
2792 String src = sb.toString();
2793 Consumer<String> saveHandler = new SaveHandler(src, srcSet);
2794 Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
2795 if (editor == BUILT_IN_EDITOR) {
2796 return builtInEdit(src, saveHandler, errorHandler);
2797 } else {
2798 // Changes have occurred in temp edit directory,
2799 // transfer the new sources to JShell (unless the editor is
2800 // running directly in JShell's window -- don't make a mess)
2801 String[] buffer = new String[1];
2802 Consumer<String> extSaveHandler = s -> {
2803 if (input.terminalEditorRunning()) {
2804 buffer[0] = s;
2805 } else {
2806 saveHandler.accept(s);
2807 }
2808 };
2809 ExternalEditor.edit(editor.cmd, src,
2810 errorHandler, extSaveHandler,
2811 () -> input.suspend(),
2812 () -> input.resume(),
2813 editor.wait,
2814 () -> hardrb("jshell.msg.press.return.to.leave.edit.mode"));
2815 if (buffer[0] != null) {
2816 saveHandler.accept(buffer[0]);
2817 }
2818 }
2819 return true;
2820 }
2821 //where
2822 // start the built-in editor
2823 private boolean builtInEdit(String initialText,
2824 Consumer<String> saveHandler, Consumer<String> errorHandler) {
2825 try {
2826 ServiceLoader<BuildInEditorProvider> sl
2827 = ServiceLoader.load(BuildInEditorProvider.class);
2828 // Find the highest ranking provider
2829 BuildInEditorProvider provider = null;
2830 for (BuildInEditorProvider p : sl) {
2831 if (provider == null || p.rank() > provider.rank()) {
2832 provider = p;
2833 }
2834 }
2835 if (provider != null) {
2836 provider.edit(getResourceString("jshell.label.editpad"),
2837 initialText, saveHandler, errorHandler);
2838 return true;
2839 } else {
2840 errormsg("jshell.err.no.builtin.editor");
2841 }
2842 } catch (RuntimeException ex) {
2843 errormsg("jshell.err.cant.launch.editor", ex);
2844 }
2845 fluffmsg("jshell.msg.try.set.editor");
2846 return false;
2847 }
2848 //where
2849 // receives editor requests to save
2850 private class SaveHandler implements Consumer<String> {
2851
2852 String src;
2853 Set<String> currSrcs;
2854
2855 SaveHandler(String src, Set<String> ss) {
2856 this.src = src;
2857 this.currSrcs = ss;
2858 }
2859
2860 @Override
2861 public void accept(String s) {
2862 if (!s.equals(src)) { // quick check first
2863 src = s;
2864 try {
2865 Set<String> nextSrcs = new LinkedHashSet<>();
2866 boolean failed = false;
2867 while (true) {
2868 CompletionInfo an = analysis.analyzeCompletion(s);
2869 if (!an.completeness().isComplete()) {
2870 break;
2871 }
2872 String tsrc = trimNewlines(an.source());
2873 if (!failed && !currSrcs.contains(tsrc)) {
2874 failed = processSource(tsrc);
2875 }
2876 nextSrcs.add(tsrc);
2877 if (an.remaining().isEmpty()) {
2878 break;
2879 }
2880 s = an.remaining();
2881 }
2882 currSrcs = nextSrcs;
2883 } catch (IllegalStateException ex) {
2884 errormsg("jshell.msg.resetting");
2885 resetState();
2886 currSrcs = new LinkedHashSet<>(); // re-process everything
2887 }
2888 }
2889 }
2890
2891 private String trimNewlines(String s) {
2892 int b = 0;
2893 while (b < s.length() && s.charAt(b) == '\n') {
2894 ++b;
2895 }
2896 int e = s.length() -1;
2897 while (e >= 0 && s.charAt(e) == '\n') {
2898 --e;
2899 }
2900 return s.substring(b, e + 1);
2901 }
2902 }
2903
2904 private boolean cmdList(String arg) {
2905 if (arg.length() >= 2 && "-history".startsWith(arg)) {
2906 return cmdHistory();
2907 }
2908 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
2909 this::mainActive, arg, "/list");
2910 if (stream == null) {
2911 return false;
2912 }
2913
2914 // prevent double newline on empty list
2915 boolean[] hasOutput = new boolean[1];
2916 stream.forEachOrdered(sn -> {
2917 if (!hasOutput[0]) {
2918 cmdout.println();
2919 hasOutput[0] = true;
2920 }
2921 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n "));
2922 });
2923 return true;
2924 }
2925
2926 private boolean cmdOpen(String filename) {
2927 return runFile(filename, "/open");
2928 }
2929
2930 private boolean runFile(String filename, String context) {
2931 if (!filename.isEmpty()) {
2932 try {
2933 Scanner scanner;
2934 if (!interactiveModeBegun && filename.equals("-")) {
2935 // - on command line: no interactive later, read from input
2936 regenerateOnDeath = false;
2937 scanner = new Scanner(cmdin);
2938 } else {
2939 Path path = toPathResolvingUserHome(filename);
2940 String resource;
2941 scanner = new Scanner(
2942 (!Files.exists(path) && (resource = getResource(filename)) != null)
2943 ? new StringReader(resource) // Not found as file, but found as resource
2944 : new FileReader(path.toString())
2945 );
2946 }
2947 run(new ScannerIOContext(scanner));
2948 return true;
2949 } catch (FileNotFoundException e) {
2950 errormsg("jshell.err.file.not.found", context, filename, e.getMessage());
2951 } catch (Exception e) {
2952 errormsg("jshell.err.file.exception", context, filename, e);
2953 }
2954 } else {
2955 errormsg("jshell.err.file.filename", context);
2956 }
2957 return false;
2958 }
2959
2960 static String getResource(String name) {
2961 if (BUILTIN_FILE_PATTERN.matcher(name).matches()) {
2962 try {
2963 return readResource(name);
2964 } catch (Throwable t) {
2965 // Fall-through to null
2966 }
2967 }
2968 return null;
2969 }
2970
2971 // Read a built-in file from resources or compute it
2972 static String readResource(String name) throws Exception {
2973 // Class to compute imports by following requires for a module
2974 class ComputeImports {
2975 final String base;
2976 ModuleFinder finder = ModuleFinder.ofSystem();
2977
2978 ComputeImports(String base) {
2979 this.base = base;
2980 }
2981
2982 Set<ModuleDescriptor> modules() {
2983 Set<ModuleDescriptor> closure = new HashSet<>();
2984 moduleClosure(finder.find(base), closure);
2985 return closure;
2986 }
2987
2988 void moduleClosure(Optional<ModuleReference> omr, Set<ModuleDescriptor> closure) {
2989 if (omr.isPresent()) {
2990 ModuleDescriptor mdesc = omr.get().descriptor();
2991 if (closure.add(mdesc)) {
2992 for (ModuleDescriptor.Requires req : mdesc.requires()) {
2993 if (!req.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) {
2994 moduleClosure(finder.find(req.name()), closure);
2995 }
2996 }
2997 }
2998 }
2999 }
3000
3001 Set<String> packages() {
3002 return modules().stream().flatMap(md -> md.exports().stream())
3003 .filter(e -> !e.isQualified()).map(Object::toString).collect(Collectors.toSet());
3004 }
3005
3006 String imports() {
3007 Set<String> si = packages();
3008 String[] ai = si.toArray(new String[si.size()]);
3009 Arrays.sort(ai);
3010 return Arrays.stream(ai)
3011 .map(p -> String.format("import %s.*;\n", p))
3012 .collect(Collectors.joining());
3013 }
3014 }
3015
3016 if (name.equals("JAVASE")) {
3017 // The built-in JAVASE is computed as the imports of all the packages in Java SE
3018 return new ComputeImports("java.se").imports();
3019 }
3020
3021 // Attempt to find the file as a resource
3022 String spec = String.format(BUILTIN_FILE_PATH_FORMAT, name);
3023
3024 try (InputStream in = JShellTool.class.getResourceAsStream(spec);
3025 BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
3026 return reader.lines().collect(Collectors.joining("\n", "", "\n"));
3027 }
3028 }
3029
3030 private boolean cmdReset(String rawargs) {
3031 Options oldOptions = rawargs.trim().isEmpty()? null : options;
3032 if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
3033 return false;
3034 }
3035 live = false;
3036 fluffmsg("jshell.msg.resetting.state");
3037 return doReload(null, false, oldOptions);
3038 }
3039
3040 private boolean cmdReload(String rawargs) {
3041 Options oldOptions = rawargs.trim().isEmpty()? null : options;
3042 OptionParserReload ap = new OptionParserReload();
3043 if (!parseCommandLineLikeFlags(rawargs, ap)) {
3044 return false;
3045 }
3046 ReplayableHistory history;
3047 if (ap.restore()) {
3048 if (replayableHistoryPrevious == null) {
3049 errormsg("jshell.err.reload.no.previous");
3050 return false;
3051 }
3052 history = replayableHistoryPrevious;
3053 fluffmsg("jshell.err.reload.restarting.previous.state");
3054 } else {
3055 history = replayableHistory;
3056 fluffmsg("jshell.err.reload.restarting.state");
3057 }
3058 boolean success = doReload(history, !ap.quiet(), oldOptions);
3059 if (success && ap.restore()) {
3060 // if we are restoring from previous, then if nothing was added
3061 // before time of exit, there is nothing to save
3062 replayableHistory.markSaved();
3063 }
3064 return success;
3065 }
3066
3067 private boolean cmdEnv(String rawargs) {
3068 if (rawargs.trim().isEmpty()) {
3069 // No arguments, display current settings (as option flags)
3070 StringBuilder sb = new StringBuilder();
3071 for (String a : options.commonOptions()) {
3072 sb.append(
3073 a.startsWith("-")
3074 ? sb.length() > 0
3075 ? "\n "
3076 : " "
3077 : " ");
3078 sb.append(a);
3079 }
3080 if (sb.length() > 0) {
3081 hard(sb.toString());
3082 }
3083 return false;
3084 }
3085 Options oldOptions = options;
3086 if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
3087 return false;
3088 }
3089 fluffmsg("jshell.msg.set.restore");
3090 return doReload(replayableHistory, false, oldOptions);
3091 }
3092
3093 private boolean doReload(ReplayableHistory history, boolean echo, Options oldOptions) {
3094 if (oldOptions != null) {
3095 try {
3096 resetState();
3097 } catch (IllegalStateException ex) {
3098 currentNameSpace = mainNamespace; // back out of start-up (messages)
3099 errormsg("jshell.err.restart.failed", ex.getMessage());
3100 // attempt recovery to previous option settings
3101 options = oldOptions;
3102 resetState();
3103 }
3104 } else {
3105 resetState();
3106 }
3107 if (history != null) {
3108 run(new ReloadIOContext(history.iterable(),
3109 echo ? cmdout : null));
3110 }
3111 return true;
3112 }
3113
3114 private boolean parseCommandLineLikeFlags(String rawargs, OptionParserBase ap) {
3115 String[] args = Arrays.stream(rawargs.split("\\s+"))
3116 .filter(s -> !s.isEmpty())
3117 .toArray(String[]::new);
3118 Options opts = ap.parse(args);
3119 if (opts == null) {
3120 return false;
3121 }
3122 if (!ap.nonOptions().isEmpty()) {
3123 errormsg("jshell.err.unexpected.at.end", ap.nonOptions(), rawargs);
3124 return false;
3125 }
3126 options = options.override(opts);
3127 return true;
3128 }
3129
3130 private boolean cmdSave(String rawargs) {
3131 // The filename to save to is the last argument, extract it
3132 String[] args = rawargs.split("\\s");
3133 String filename = args[args.length - 1];
3134 if (filename.isEmpty()) {
3135 errormsg("jshell.err.file.filename", "/save");
3136 return false;
3137 }
3138 // All the non-filename arguments are the specifier of what to save
3139 String srcSpec = Arrays.stream(args, 0, args.length - 1)
3140 .collect(Collectors.joining("\n"));
3141 // From the what to save specifier, compute the snippets (as a stream)
3142 ArgTokenizer at = new ArgTokenizer("/save", srcSpec);
3143 at.allowedOptions("-all", "-start", "-history");
3144 Stream<Snippet> snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at);
3145 if (snippetStream == null) {
3146 // error occurred, already reported
3147 return false;
3148 }
3149 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
3150 Charset.defaultCharset(),
3151 CREATE, TRUNCATE_EXISTING, WRITE)) {
3152 if (at.hasOption("-history")) {
3153 // they want history (commands and snippets), ignore the snippet stream
3154 for (String s : input.currentSessionHistory()) {
3155 writer.write(s);
3156 writer.write("\n");
3157 }
3158 } else {
3159 // write the snippet stream to the file
3160 writer.write(snippetStream
3161 .map(Snippet::source)
3162 .collect(Collectors.joining("\n")));
3163 }
3164 } catch (FileNotFoundException e) {
3165 errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
3166 return false;
3167 } catch (Exception e) {
3168 errormsg("jshell.err.file.exception", "/save", filename, e);
3169 return false;
3170 }
3171 return true;
3172 }
3173
3174 private boolean cmdVars(String arg) {
3175 Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets,
3176 this::isActive, arg, "/vars");
3177 if (stream == null) {
3178 return false;
3179 }
3180 stream.forEachOrdered(vk ->
3181 {
3182 String val = state.status(vk) == Status.VALID
3183 ? feedback.truncateVarValue(state.varValue(vk))
3184 : getResourceString("jshell.msg.vars.not.active");
3185 hard(" %s %s = %s", vk.typeName(), vk.name(), val);
3186 });
3187 return true;
3188 }
3189
3190 private boolean cmdMethods(String arg) {
3191 Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets,
3192 this::isActive, arg, "/methods");
3193 if (stream == null) {
3194 return false;
3195 }
3196 stream.forEachOrdered(meth -> {
3197 String sig = meth.signature();
3198 int i = sig.lastIndexOf(")") + 1;
3199 if (i <= 0) {
3200 hard(" %s", meth.name());
3201 } else {
3202 hard(" %s %s%s", sig.substring(i), meth.name(), sig.substring(0, i));
3203 }
3204 printSnippetStatus(meth, true);
3205 });
3206 return true;
3207 }
3208
3209 private boolean cmdTypes(String arg) {
3210 Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets,
3211 this::isActive, arg, "/types");
3212 if (stream == null) {
3213 return false;
3214 }
3215 stream.forEachOrdered(ck
3216 -> {
3217 String kind;
3218 switch (ck.subKind()) {
3219 case INTERFACE_SUBKIND:
3220 kind = "interface";
3221 break;
3222 case CLASS_SUBKIND:
3223 kind = "class";
3224 break;
3225 case ENUM_SUBKIND:
3226 kind = "enum";
3227 break;
3228 case ANNOTATION_TYPE_SUBKIND:
3229 kind = "@interface";
3230 break;
3231 default:
3232 assert false : "Wrong kind" + ck.subKind();
3233 kind = "class";
3234 break;
3235 }
3236 hard(" %s %s", kind, ck.name());
3237 printSnippetStatus(ck, true);
3238 });
3239 return true;
3240 }
3241
3242 private boolean cmdImports() {
3243 state.imports().forEach(ik -> {
3244 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname());
3245 });
3246 return true;
3247 }
3248
3249 private boolean cmdUseHistoryEntry(int index) {
3250 List<Snippet> keys = state.snippets().collect(toList());
3251 if (index < 0)
3252 index += keys.size();
3253 else
3254 index--;
3255 if (index >= 0 && index < keys.size()) {
3256 rerunSnippet(keys.get(index));
3257 } else {
3258 errormsg("jshell.err.out.of.range");
3259 return false;
3260 }
3261 return true;
3262 }
3263
3264 boolean checkOptionsAndRemainingInput(ArgTokenizer at) {
3265 String junk = at.remainder();
3266 if (!junk.isEmpty()) {
3267 errormsg("jshell.err.unexpected.at.end", junk, at.whole());
3268 return false;
3269 } else {
3270 String bad = at.badOptions();
3271 if (!bad.isEmpty()) {
3272 errormsg("jshell.err.unknown.option", bad, at.whole());
3273 return false;
3274 }
3275 }
3276 return true;
3277 }
3278
3279 /**
3280 * Handle snippet reevaluation commands: {@code /<id>}. These commands are a
3281 * sequence of ids and id ranges (names are permitted, though not in the
3282 * first position. Support for names is purposely not documented).
3283 *
3284 * @param rawargs the whole command including arguments
3285 */
3286 private void rerunHistoryEntriesById(String rawargs) {
3287 ArgTokenizer at = new ArgTokenizer("/<id>", rawargs.trim().substring(1));
3288 at.allowedOptions();
3289 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, sn -> true, at);
3290 if (stream != null) {
3291 // successfully parsed, rerun snippets
3292 stream.forEach(sn -> rerunSnippet(sn));
3293 }
3294 }
3295
3296 private void rerunSnippet(Snippet snippet) {
3297 String source = snippet.source();
3298 cmdout.printf("%s\n", source);
3299 input.replaceLastHistoryEntry(source);
3300 processSourceCatchingReset(source);
3301 }
3302
3303 /**
3304 * Filter diagnostics for only errors (no warnings, ...)
3305 * @param diagnostics input list
3306 * @return filtered list
3307 */
3308 List<Diag> errorsOnly(List<Diag> diagnostics) {
3309 return diagnostics.stream()
3310 .filter(Diag::isError)
3311 .collect(toList());
3312 }
3313
3314 /**
3315 * Print out a snippet exception.
3316 *
3317 * @param exception the exception to print
3318 * @return true on fatal exception
3319 */
3320 private boolean displayException(Exception exception) {
3321 if (exception instanceof EvalException) {
3322 printEvalException((EvalException) exception);
3323 return true;
3324 } else if (exception instanceof UnresolvedReferenceException) {
3325 printUnresolvedException((UnresolvedReferenceException) exception);
3326 return false;
3327 } else {
3328 error("Unexpected execution exception: %s", exception);
3329 return true;
3330 }
3331 }
3332
3333 /**
3334 * Display a list of diagnostics.
3335 *
3336 * @param source the source line with the error/warning
3337 * @param diagnostics the diagnostics to display
3338 */
3339 private void displayDiagnostics(String source, List<Diag> diagnostics) {
3340 for (Diag d : diagnostics) {
3341 errormsg(d.isError() ? "jshell.msg.error" : "jshell.msg.warning");
3342 List<String> disp = new ArrayList<>();
3343 displayableDiagnostic(source, d, disp);
3344 disp.stream()
3345 .forEach(l -> error("%s", l));
3346 }
3347 }
3348
3349 /**
3350 * Convert a diagnostic into a list of pretty displayable strings with
3351 * source context.
3352 *
3353 * @param source the source line for the error/warning
3354 * @param diag the diagnostic to convert
3355 * @param toDisplay a list that the displayable strings are added to
3356 */
3357 private void displayableDiagnostic(String source, Diag diag, List<String> toDisplay) {
3358 for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize
3359 if (!line.trim().startsWith("location:")) {
3360 toDisplay.add(line);
3361 }
3362 }
3363
3364 int pstart = (int) diag.getStartPosition();
3365 int pend = (int) diag.getEndPosition();
3366 Matcher m = LINEBREAK.matcher(source);
3367 int pstartl = 0;
3368 int pendl = -2;
3369 while (m.find(pstartl)) {
3370 pendl = m.start();
3371 if (pendl >= pstart) {
3372 break;
3373 } else {
3374 pstartl = m.end();
3375 }
3376 }
3377 if (pendl < pstart) {
3378 pendl = source.length();
3379 }
3380 toDisplay.add(source.substring(pstartl, pendl));
3381
3382 StringBuilder sb = new StringBuilder();
3383 int start = pstart - pstartl;
3384 for (int i = 0; i < start; ++i) {
3385 sb.append(' ');
3386 }
3387 sb.append('^');
3388 boolean multiline = pend > pendl;
3389 int end = (multiline ? pendl : pend) - pstartl - 1;
3390 if (end > start) {
3391 for (int i = start + 1; i < end; ++i) {
3392 sb.append('-');
3393 }
3394 if (multiline) {
3395 sb.append("-...");
3396 } else {
3397 sb.append('^');
3398 }
3399 }
3400 toDisplay.add(sb.toString());
3401
3402 debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
3403 debug("Code: %s", diag.getCode());
3404 debug("Pos: %d (%d - %d)", diag.getPosition(),
3405 diag.getStartPosition(), diag.getEndPosition());
3406 }
3407
3408 /**
3409 * Process a source snippet.
3410 *
3411 * @param source the input source
3412 * @return true if the snippet succeeded
3413 */
3414 boolean processSource(String source) {
3415 debug("Compiling: %s", source);
3416 boolean failed = false;
3417 boolean isActive = false;
3418 List<SnippetEvent> events = state.eval(source);
3419 for (SnippetEvent e : events) {
3420 // Report the event, recording failure
3421 failed |= handleEvent(e);
3422
3423 // If any main snippet is active, this should be replayable
3424 // also ignore var value queries
3425 isActive |= e.causeSnippet() == null &&
3426 e.status().isActive() &&
3427 e.snippet().subKind() != VAR_VALUE_SUBKIND;
3428 }
3429 // If this is an active snippet and it didn't cause the backend to die,
3430 // add it to the replayable history
3431 if (isActive && live) {
3432 addToReplayHistory(source);
3433 }
3434
3435 return !failed;
3436 }
3437
3438 // Handle incoming snippet events -- return true on failure
3439 private boolean handleEvent(SnippetEvent ste) {
3440 Snippet sn = ste.snippet();
3441 if (sn == null) {
3442 debug("Event with null key: %s", ste);
3443 return false;
3444 }
3445 List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
3446 String source = sn.source();
3447 if (ste.causeSnippet() == null) {
3448 // main event
3449 displayDiagnostics(source, diagnostics);
3450
3451 if (ste.status() != Status.REJECTED) {
3452 if (ste.exception() != null) {
3453 if (displayException(ste.exception())) {
3454 return true;
3455 }
3456 } else {
3457 new DisplayEvent(ste, FormatWhen.PRIMARY, ste.value(), diagnostics)
3458 .displayDeclarationAndValue();
3459 }
3460 } else {
3461 if (diagnostics.isEmpty()) {
3462 errormsg("jshell.err.failed");
3463 }
3464 return true;
3465 }
3466 } else {
3467 // Update
3468 if (sn instanceof DeclarationSnippet) {
3469 List<Diag> other = errorsOnly(diagnostics);
3470
3471 // display update information
3472 new DisplayEvent(ste, FormatWhen.UPDATE, ste.value(), other)
3473 .displayDeclarationAndValue();
3474 }
3475 }
3476 return false;
3477 }
3478 //where
3479 void printStackTrace(StackTraceElement[] stes) {
3480 for (StackTraceElement ste : stes) {
3481 StringBuilder sb = new StringBuilder();
3482 String cn = ste.getClassName();
3483 if (!cn.isEmpty()) {
3484 int dot = cn.lastIndexOf('.');
3485 if (dot > 0) {
3486 sb.append(cn.substring(dot + 1));
3487 } else {
3488 sb.append(cn);
3489 }
3490 sb.append(".");
3491 }
3492 if (!ste.getMethodName().isEmpty()) {
3493 sb.append(ste.getMethodName());
3494 sb.append(" ");
3495 }
3496 String fileName = ste.getFileName();
3497 int lineNumber = ste.getLineNumber();
3498 String loc = ste.isNativeMethod()
3499 ? getResourceString("jshell.msg.native.method")
3500 : fileName == null
3501 ? getResourceString("jshell.msg.unknown.source")
3502 : lineNumber >= 0
3503 ? fileName + ":" + lineNumber
3504 : fileName;
3505 error(" at %s(%s)", sb, loc);
3506
3507 }
3508 }
3509 //where
3510 void printUnresolvedException(UnresolvedReferenceException ex) {
3511 printSnippetStatus(ex.getSnippet(), false);
3512 }
3513 //where
3514 void printEvalException(EvalException ex) {
3515 if (ex.getMessage() == null) {
3516 error("%s thrown", ex.getExceptionClassName());
3517 } else {
3518 error("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
3519 }
3520 printStackTrace(ex.getStackTrace());
3521 }
3522
3523 private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) {
3524 FormatAction act;
3525 switch (status) {
3526 case VALID:
3527 case RECOVERABLE_DEFINED:
3528 case RECOVERABLE_NOT_DEFINED:
3529 if (previousStatus.isActive()) {
3530 act = isSignatureChange
3531 ? FormatAction.REPLACED
3532 : FormatAction.MODIFIED;
3533 } else {
3534 act = FormatAction.ADDED;
3535 }
3536 break;
3537 case OVERWRITTEN:
3538 act = FormatAction.OVERWROTE;
3539 break;
3540 case DROPPED:
3541 act = FormatAction.DROPPED;
3542 break;
3543 case REJECTED:
3544 case NONEXISTENT:
3545 default:
3546 // Should not occur
3547 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString());
3548 act = FormatAction.DROPPED;
3549 }
3550 return act;
3551 }
3552
3553 void printSnippetStatus(DeclarationSnippet sn, boolean resolve) {
3554 List<Diag> otherErrors = errorsOnly(state.diagnostics(sn).collect(toList()));
3555 new DisplayEvent(sn, state.status(sn), resolve, otherErrors)
3556 .displayDeclarationAndValue();
3557 }
3558
3559 class DisplayEvent {
3560 private final Snippet sn;
3561 private final FormatAction action;
3562 private final FormatWhen update;
3563 private final String value;
3564 private final List<String> errorLines;
3565 private final FormatResolve resolution;
3566 private final String unresolved;
3567 private final FormatUnresolved unrcnt;
3568 private final FormatErrors errcnt;
3569 private final boolean resolve;
3570
3571 DisplayEvent(SnippetEvent ste, FormatWhen update, String value, List<Diag> errors) {
3572 this(ste.snippet(), ste.status(), false,
3573 toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()),
3574 update, value, errors);
3575 }
3576
3577 DisplayEvent(Snippet sn, Status status, boolean resolve, List<Diag> errors) {
3578 this(sn, status, resolve, FormatAction.USED, FormatWhen.UPDATE, null, errors);
3579 }
3580
3581 private DisplayEvent(Snippet sn, Status status, boolean resolve,
3582 FormatAction action, FormatWhen update, String value, List<Diag> errors) {
3583 this.sn = sn;
3584 this.resolve =resolve;
3585 this.action = action;
3586 this.update = update;
3587 this.value = value;
3588 this.errorLines = new ArrayList<>();
3589 for (Diag d : errors) {
3590 displayableDiagnostic(sn.source(), d, errorLines);
3591 }
3592 if (resolve) {
3593 // resolve needs error lines indented
3594 for (int i = 0; i < errorLines.size(); ++i) {
3595 errorLines.set(i, " " + errorLines.get(i));
3596 }
3597 }
3598 long unresolvedCount;
3599 if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) {
3600 resolution = (status == Status.RECOVERABLE_NOT_DEFINED)
3601 ? FormatResolve.NOTDEFINED
3602 : FormatResolve.DEFINED;
3603 unresolved = unresolved((DeclarationSnippet) sn);
3604 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count();
3605 } else {
3606 resolution = FormatResolve.OK;
3607 unresolved = "";
3608 unresolvedCount = 0;
3609 }
3610 unrcnt = unresolvedCount == 0
3611 ? FormatUnresolved.UNRESOLVED0
3612 : unresolvedCount == 1
3613 ? FormatUnresolved.UNRESOLVED1
3614 : FormatUnresolved.UNRESOLVED2;
3615 errcnt = errors.isEmpty()
3616 ? FormatErrors.ERROR0
3617 : errors.size() == 1
3618 ? FormatErrors.ERROR1
3619 : FormatErrors.ERROR2;
3620 }
3621
3622 private String unresolved(DeclarationSnippet key) {
3623 List<String> unr = state.unresolvedDependencies(key).collect(toList());
3624 StringBuilder sb = new StringBuilder();
3625 int fromLast = unr.size();
3626 if (fromLast > 0) {
3627 sb.append(" ");
3628 }
3629 for (String u : unr) {
3630 --fromLast;
3631 sb.append(u);
3632 switch (fromLast) {
3633 // No suffix
3634 case 0:
3635 break;
3636 case 1:
3637 sb.append(", and ");
3638 break;
3639 default:
3640 sb.append(", ");
3641 break;
3642 }
3643 }
3644 return sb.toString();
3645 }
3646
3647 private void custom(FormatCase fcase, String name) {
3648 custom(fcase, name, null);
3649 }
3650
3651 private void custom(FormatCase fcase, String name, String type) {
3652 if (resolve) {
3653 String resolutionErrors = feedback.format("resolve", fcase, action, update,
3654 resolution, unrcnt, errcnt,
3655 name, type, value, unresolved, errorLines);
3656 if (!resolutionErrors.trim().isEmpty()) {
3657 error(" %s", resolutionErrors);
3658 }
3659 } else if (interactive()) {
3660 String display = feedback.format(fcase, action, update,
3661 resolution, unrcnt, errcnt,
3662 name, type, value, unresolved, errorLines);
3663 cmdout.print(display);
3664 }
3665 }
3666
3667 @SuppressWarnings("fallthrough")
3668 private void displayDeclarationAndValue() {
3669 switch (sn.subKind()) {
3670 case CLASS_SUBKIND:
3671 custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name());
3672 break;
3673 case INTERFACE_SUBKIND:
3674 custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name());
3675 break;
3676 case ENUM_SUBKIND:
3677 custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name());
3678 break;
3679 case ANNOTATION_TYPE_SUBKIND:
3680 custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name());
3681 break;
3682 case METHOD_SUBKIND:
3683 custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes());
3684 break;
3685 case VAR_DECLARATION_SUBKIND: {
3686 VarSnippet vk = (VarSnippet) sn;
3687 custom(FormatCase.VARDECL, vk.name(), vk.typeName());
3688 break;
3689 }
3690 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: {
3691 VarSnippet vk = (VarSnippet) sn;
3692 custom(FormatCase.VARINIT, vk.name(), vk.typeName());
3693 break;
3694 }
3695 case TEMP_VAR_EXPRESSION_SUBKIND: {
3696 VarSnippet vk = (VarSnippet) sn;
3697 custom(FormatCase.EXPRESSION, vk.name(), vk.typeName());
3698 break;
3699 }
3700 case OTHER_EXPRESSION_SUBKIND:
3701 error("Unexpected expression form -- value is: %s", (value));
3702 break;
3703 case VAR_VALUE_SUBKIND: {
3704 ExpressionSnippet ek = (ExpressionSnippet) sn;
3705 custom(FormatCase.VARVALUE, ek.name(), ek.typeName());
3706 break;
3707 }
3708 case ASSIGNMENT_SUBKIND: {
3709 ExpressionSnippet ek = (ExpressionSnippet) sn;
3710 custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName());
3711 break;
3712 }
3713 case SINGLE_TYPE_IMPORT_SUBKIND:
3714 case TYPE_IMPORT_ON_DEMAND_SUBKIND:
3715 case SINGLE_STATIC_IMPORT_SUBKIND:
3716 case STATIC_IMPORT_ON_DEMAND_SUBKIND:
3717 custom(FormatCase.IMPORT, ((ImportSnippet) sn).name());
3718 break;
3719 case STATEMENT_SUBKIND:
3720 custom(FormatCase.STATEMENT, null);
3721 break;
3722 }
3723 }
3724 }
3725
3726 /** The current version number as a string.
3727 */
3728 String version() {
3729 return version("release"); // mm.nn.oo[-milestone]
3730 }
3731
3732 /** The current full version number as a string.
3733 */
3734 String fullVersion() {
3735 return version("full"); // mm.mm.oo[-milestone]-build
3736 }
3737
3738 private String version(String key) {
3739 if (versionRB == null) {
3740 try {
3741 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale);
3742 } catch (MissingResourceException e) {
3743 return "(version info not available)";
3744 }
3745 }
3746 try {
3747 return versionRB.getString(key);
3748 }
3749 catch (MissingResourceException e) {
3750 return "(version info not available)";
3751 }
3752 }
3753
3754 class NameSpace {
3755 final String spaceName;
3756 final String prefix;
3757 private int nextNum;
3758
3759 NameSpace(String spaceName, String prefix) {
3760 this.spaceName = spaceName;
3761 this.prefix = prefix;
3762 this.nextNum = 1;
3763 }
3764
3765 String tid(Snippet sn) {
3766 String tid = prefix + nextNum++;
3767 mapSnippet.put(sn, new SnippetInfo(sn, this, tid));
3768 return tid;
3769 }
3770
3771 String tidNext() {
3772 return prefix + nextNum;
3773 }
3774 }
3775
3776 static class SnippetInfo {
3777 final Snippet snippet;
3778 final NameSpace space;
3779 final String tid;
3780
3781 SnippetInfo(Snippet snippet, NameSpace space, String tid) {
3782 this.snippet = snippet;
3783 this.space = space;
3784 this.tid = tid;
3785 }
3786 }
3787
3788 static class ArgSuggestion implements Suggestion {
3789
3790 private final String continuation;
3791
3792 /**
3793 * Create a {@code Suggestion} instance.
3794 *
3795 * @param continuation a candidate continuation of the user's input
3796 */
3797 public ArgSuggestion(String continuation) {
3798 this.continuation = continuation;
3799 }
3800
3801 /**
3802 * The candidate continuation of the given user's input.
3803 *
3804 * @return the continuation string
3805 */
3806 @Override
3807 public String continuation() {
3808 return continuation;
3809 }
3810
3811 /**
3812 * Indicates whether input continuation matches the target type and is thus
3813 * more likely to be the desired continuation. A matching continuation is
3814 * preferred.
3815 *
3816 * @return {@code false}, non-types analysis
3817 */
3818 @Override
3819 public boolean matchesType() {
3820 return false;
3821 }
3822 }
3823 }
3824
3825 abstract class NonInteractiveIOContext extends IOContext {
3826
3827 @Override
3828 public boolean interactiveOutput() {
3829 return false;
3830 }
3831
3832 @Override
3833 public Iterable<String> currentSessionHistory() {
3834 return Collections.emptyList();
3835 }
3836
3837 @Override
3838 public boolean terminalEditorRunning() {
3839 return false;
3840 }
3841
3842 @Override
3843 public void suspend() {
3844 }
3845
3846 @Override
3847 public void resume() {
3848 }
3849
3850 @Override
3851 public void beforeUserCode() {
3852 }
3853
3854 @Override
3855 public void afterUserCode() {
3856 }
3857
3858 @Override
3859 public void replaceLastHistoryEntry(String source) {
3860 }
3861 }
3862
3863 class ScannerIOContext extends NonInteractiveIOContext {
3864 private final Scanner scannerIn;
3865
3866 ScannerIOContext(Scanner scannerIn) {
3867 this.scannerIn = scannerIn;
3868 }
3869
3870 ScannerIOContext(Reader rdr) throws FileNotFoundException {
3871 this(new Scanner(rdr));
3872 }
3873
3874 @Override
3875 public String readLine(String prompt, String prefix) {
3876 if (scannerIn.hasNextLine()) {
3877 return scannerIn.nextLine();
3878 } else {
3879 return null;
3880 }
3881 }
3882
3883 @Override
3884 public void close() {
3885 scannerIn.close();
3886 }
3887
3888 @Override
3889 public int readUserInput() {
3890 return -1;
3891 }
3892 }
3893
3894 class ReloadIOContext extends NonInteractiveIOContext {
3895 private final Iterator<String> it;
3896 private final PrintStream echoStream;
3897
3898 ReloadIOContext(Iterable<String> history, PrintStream echoStream) {
3899 this.it = history.iterator();
3900 this.echoStream = echoStream;
3901 }
3902
3903 @Override
3904 public String readLine(String prompt, String prefix) {
3905 String s = it.hasNext()
3906 ? it.next()
3907 : null;
3908 if (echoStream != null && s != null) {
3909 String p = "-: ";
3910 String p2 = "\n ";
3911 echoStream.printf("%s%s\n", p, s.replace("\n", p2));
3912 }
3913 return s;
3914 }
3915
3916 @Override
3917 public void close() {
3918 }
3919
3920 @Override
3921 public int readUserInput() {
3922 return -1;
3923 }
3924 }
--- EOF ---