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