1 /* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.internal.jshell.tool; 27 28 import jdk.jshell.SourceCodeAnalysis.CompletionInfo; 29 import jdk.jshell.SourceCodeAnalysis.IndexResult; 30 import jdk.jshell.SourceCodeAnalysis.Suggestion; 31 32 import java.awt.event.ActionListener; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.PrintStream; 36 import java.io.UncheckedIOException; 37 import java.lang.reflect.Method; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Locale; 43 import java.util.Map; 44 import java.util.Objects; 45 import java.util.Optional; 46 import java.util.function.Supplier; 47 48 import jdk.internal.jline.NoInterruptUnixTerminal; 49 import jdk.internal.jline.Terminal; 50 import jdk.internal.jline.TerminalFactory; 51 import jdk.internal.jline.WindowsTerminal; 52 import jdk.internal.jline.console.ConsoleReader; 53 import jdk.internal.jline.console.KeyMap; 54 import jdk.internal.jline.console.UserInterruptException; 55 import jdk.internal.jline.console.completer.Completer; 56 import jdk.internal.jshell.tool.StopDetectingInputStream.State; 57 58 class ConsoleIOContext extends IOContext { 59 60 final JShellTool repl; 61 final StopDetectingInputStream input; 62 final ConsoleReader in; 63 final EditingHistory history; 64 65 String prefix = ""; 66 67 ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception { 68 this.repl = repl; 69 this.input = new StopDetectingInputStream(() -> repl.state.stop(), ex -> repl.hard("Error on input: %s", ex)); 70 Terminal term; 71 if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) { 72 term = new JShellWindowsTerminal(input); 73 } else { 74 term = new JShellUnixTerminal(input); 75 } 76 term.init(); 77 in = new ConsoleReader(cmdin, cmdout, term); 78 in.setExpandEvents(false); 79 in.setHandleUserInterrupt(true); 80 in.setHistory(history = new EditingHistory(JShellTool.PREFS) { 81 @Override protected CompletionInfo analyzeCompletion(String input) { 82 return repl.analysis.analyzeCompletion(input); 83 } 84 }); 85 in.setBellEnabled(true); 86 in.addCompleter(new Completer() { 87 private String lastTest; 88 private int lastCursor; 89 private boolean allowSmart = false; 90 @Override public int complete(String test, int cursor, List<CharSequence> result) { 91 int[] anchor = new int[] {-1}; 92 List<Suggestion> suggestions; 93 if (prefix.isEmpty() && test.trim().startsWith("/")) { 94 suggestions = repl.commandCompletionSuggestions(test, cursor, anchor); 95 } else { 96 int prefixLength = prefix.length(); 97 suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor); 98 anchor[0] -= prefixLength; 99 } 100 if (!Objects.equals(lastTest, test) || lastCursor != cursor) 101 allowSmart = true; 102 103 boolean smart = allowSmart && 104 suggestions.stream() 105 .anyMatch(s -> s.isSmart); 106 107 lastTest = test; 108 lastCursor = cursor; 109 allowSmart = !allowSmart; 110 111 suggestions.stream() 112 .filter(s -> !smart || s.isSmart) 113 .map(s -> s.continuation) 114 .forEach(result::add); 115 116 boolean onlySmart = suggestions.stream() 117 .allMatch(s -> s.isSmart); 118 119 if (smart && !onlySmart) { 120 Optional<String> prefix = 121 suggestions.stream() 122 .map(s -> s.continuation) 123 .reduce(ConsoleIOContext::commonPrefix); 124 125 String prefixStr = prefix.orElse("").substring(cursor - anchor[0]); 126 try { 127 in.putString(prefixStr); 128 cursor += prefixStr.length(); 129 } catch (IOException ex) { 130 throw new IllegalStateException(ex); 131 } 132 result.add("<press tab to see more>"); 133 return cursor; //anchor should not be used. 134 } 135 136 if (result.isEmpty()) { 137 try { 138 //provide "empty completion" feedback 139 //XXX: this only works correctly when there is only one Completer: 140 in.beep(); 141 } catch (IOException ex) { 142 throw new UncheckedIOException(ex); 143 } 144 } 145 146 return anchor[0]; 147 } 148 }); 149 bind(DOCUMENTATION_SHORTCUT, (ActionListener) evt -> documentation(repl)); 150 bind(CTRL_UP, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::previousSnippet)); 151 bind(CTRL_DOWN, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::nextSnippet)); 152 for (FixComputer computer : fixComputers) { 153 for (String shortcuts : SHORTCUT_FIXES) { 154 bind(shortcuts + computer.shortcut, (ActionListener) evt -> fixes(computer)); 155 } 156 } 157 } 158 159 @Override 160 public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException { 161 this.prefix = prefix; 162 try { 163 return in.readLine(prompt); 164 } catch (UserInterruptException ex) { 165 throw (InputInterruptedException) new InputInterruptedException().initCause(ex); 166 } 167 } 168 169 @Override 170 public boolean interactiveOutput() { 171 return true; 172 } 173 174 @Override 175 public Iterable<String> currentSessionHistory() { 176 return history.currentSessionEntries(); 177 } 178 179 @Override 180 public void close() throws IOException { 181 history.save(); 182 in.shutdown(); 183 try { 184 in.getTerminal().restore(); 185 } catch (Exception ex) { 186 throw new IOException(ex); 187 } 188 } 189 190 private void moveHistoryToSnippet(Supplier<Boolean> action) { 191 if (!action.get()) { 192 try { 193 in.beep(); 194 } catch (IOException ex) { 195 throw new IllegalStateException(ex); 196 } 197 } else { 198 try { 199 //could use: 200 //in.resetPromptLine(in.getPrompt(), in.getHistory().current().toString(), -1); 201 //but that would mean more re-writing on the screen, (and prints an additional 202 //empty line), so using setBuffer directly: 203 Method setBuffer = in.getClass().getDeclaredMethod("setBuffer", String.class); 204 205 setBuffer.setAccessible(true); 206 setBuffer.invoke(in, in.getHistory().current().toString()); 207 in.flush(); 208 } catch (ReflectiveOperationException | IOException ex) { 209 throw new IllegalStateException(ex); 210 } 211 } 212 } 213 214 private void bind(String shortcut, Object action) { 215 KeyMap km = in.getKeys(); 216 for (int i = 0; i < shortcut.length(); i++) { 217 Object value = km.getBound(Character.toString(shortcut.charAt(i))); 218 if (value instanceof KeyMap) { 219 km = (KeyMap) value; 220 } else { 221 km.bind(shortcut.substring(i), action); 222 } 223 } 224 } 225 226 private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB 227 private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP 228 private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN 229 private static final String[] SHORTCUT_FIXES = { 230 "\033\015", //Alt-Enter (Linux) 231 "\033\133\061\067\176", //F6/Alt-F1 (Mac) 232 "\u001BO3P" //Alt-F1 (Linux) 233 }; 234 235 private void documentation(JShellTool repl) { 236 String buffer = in.getCursorBuffer().buffer.toString(); 237 int cursor = in.getCursorBuffer().cursor; 238 String doc; 239 if (prefix.isEmpty() && buffer.trim().startsWith("/")) { 240 doc = repl.commandDocumentation(buffer, cursor); 241 } else { 242 doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length()); 243 } 244 245 try { 246 if (doc != null) { 247 in.println(); 248 in.println(doc); 249 in.redrawLine(); 250 in.flush(); 251 } else { 252 in.beep(); 253 } 254 } catch (IOException ex) { 255 throw new IllegalStateException(ex); 256 } 257 } 258 259 private static String commonPrefix(String str1, String str2) { 260 for (int i = 0; i < str2.length(); i++) { 261 if (!str1.startsWith(str2.substring(0, i + 1))) { 262 return str2.substring(0, i); 263 } 264 } 265 266 return str2; 267 } 268 269 @Override 270 public boolean terminalEditorRunning() { 271 Terminal terminal = in.getTerminal(); 272 if (terminal instanceof JShellUnixTerminal) 273 return ((JShellUnixTerminal) terminal).isRaw(); 274 return false; 275 } 276 277 @Override 278 public void suspend() { 279 try { 280 in.getTerminal().restore(); 281 } catch (Exception ex) { 282 throw new IllegalStateException(ex); 283 } 284 } 285 286 @Override 287 public void resume() { 288 try { 289 in.getTerminal().init(); 290 } catch (Exception ex) { 291 throw new IllegalStateException(ex); 292 } 293 } 294 295 public void beforeUserCode() { 296 input.setState(State.BUFFER); 297 } 298 299 public void afterUserCode() { 300 input.setState(State.WAIT); 301 } 302 303 @Override 304 public void replaceLastHistoryEntry(String source) { 305 history.fullHistoryReplace(source); 306 } 307 308 private void fixes(FixComputer computer) { 309 String input = prefix + in.getCursorBuffer().toString(); 310 int cursor = prefix.length() + in.getCursorBuffer().cursor; 311 FixResult candidates = computer.compute(repl, input, cursor); 312 313 try { 314 final boolean printError = candidates.error != null && !candidates.error.isEmpty(); 315 if (printError) { 316 in.println(candidates.error); 317 } 318 if (candidates.fixes.isEmpty()) { 319 in.beep(); 320 if (printError) { 321 in.redrawLine(); 322 in.flush(); 323 } 324 } else if (candidates.fixes.size() == 1 && !computer.showMenu) { 325 if (printError) { 326 in.redrawLine(); 327 in.flush(); 328 } 329 candidates.fixes.get(0).perform(in); 330 } else { 331 List<Fix> fixes = new ArrayList<>(candidates.fixes); 332 fixes.add(0, new Fix() { 333 @Override 334 public String displayName() { 335 return "Do nothing"; 336 } 337 338 @Override 339 public void perform(ConsoleReader in) throws IOException { 340 in.redrawLine(); 341 } 342 }); 343 344 Map<Character, Fix> char2Fix = new HashMap<>(); 345 in.println(); 346 for (int i = 0; i < fixes.size(); i++) { 347 Fix fix = fixes.get(i); 348 char2Fix.put((char) ('0' + i), fix); 349 in.println("" + i + ": " + fixes.get(i).displayName()); 350 } 351 char2Fix.put((char) 3, fixes.get(0)); //Ctrl-C 352 in.print("Choice: "); 353 in.flush(); 354 int read; 355 356 while (true) { 357 read = in.readCharacter(); 358 359 Fix fix = char2Fix.get((char) read); 360 361 if (fix != null) { 362 in.println(); 363 364 fix.perform(in); 365 366 in.flush(); 367 break; 368 } 369 } 370 } 371 } catch (IOException ex) { 372 ex.printStackTrace(); 373 } 374 } 375 376 public interface Fix { 377 public String displayName(); 378 public void perform(ConsoleReader in) throws IOException; 379 } 380 381 public abstract static class FixComputer { 382 private final char shortcut; 383 private final boolean showMenu; 384 385 public FixComputer(char shortcut, boolean showMenu) { 386 this.shortcut = shortcut; 387 this.showMenu = showMenu; 388 } 389 390 public abstract FixResult compute(JShellTool repl, String code, int cursor); 391 } 392 393 public static class FixResult { 394 public final List<Fix> fixes; 395 public final String error; 396 397 public FixResult(List<Fix> fixes, String error) { 398 this.fixes = fixes; 399 this.error = error; 400 } 401 } 402 403 private static final FixComputer[] fixComputers = new FixComputer[] { 404 new FixComputer('v', false) { 405 @Override 406 public FixResult compute(JShellTool repl, String code, int cursor) { 407 String type = repl.analysis.analyzeType(code, cursor); 408 if (type == null) { 409 return new FixResult(Collections.emptyList(), null); 410 } 411 return new FixResult(Collections.singletonList(new Fix() { 412 @Override 413 public String displayName() { 414 return "Create variable"; 415 } 416 @Override 417 public void perform(ConsoleReader in) throws IOException { 418 in.redrawLine(); 419 in.setCursorPosition(0); 420 in.putString(type + " = "); 421 in.setCursorPosition(in.getCursorBuffer().cursor - 3); 422 in.flush(); 423 } 424 }), null); 425 } 426 }, 427 new FixComputer('i', true) { 428 @Override 429 public FixResult compute(JShellTool repl, String code, int cursor) { 430 IndexResult res = repl.analysis.getDeclaredSymbols(code, cursor); 431 List<Fix> fixes = new ArrayList<>(); 432 for (String fqn : res.getFqns()) { 433 fixes.add(new Fix() { 434 @Override 435 public String displayName() { 436 return "import: " + fqn; 437 } 438 @Override 439 public void perform(ConsoleReader in) throws IOException { 440 repl.state.eval("import " + fqn + ";"); 441 in.println("Imported: " + fqn); 442 in.redrawLine(); 443 } 444 }); 445 } 446 if (res.isResolvable()) { 447 return new FixResult(Collections.emptyList(), 448 "\nThe identifier is resolvable in this context."); 449 } else { 450 String error = ""; 451 if (fixes.isEmpty()) { 452 error = "\nNo candidate FQNs found to import."; 453 } 454 if (!res.isUpToDate()) { 455 error += "\nResults may be incomplete; try again later for complete results."; 456 } 457 return new FixResult(fixes, error); 458 } 459 } 460 } 461 }; 462 463 private static final class JShellUnixTerminal extends NoInterruptUnixTerminal { 464 465 private final StopDetectingInputStream input; 466 467 public JShellUnixTerminal(StopDetectingInputStream input) throws Exception { 468 this.input = input; 469 } 470 471 public boolean isRaw() { 472 try { 473 return getSettings().get("-a").contains("-icanon"); 474 } catch (IOException | InterruptedException ex) { 475 return false; 476 } 477 } 478 479 @Override 480 public InputStream wrapInIfNeeded(InputStream in) throws IOException { 481 return input.setInputStream(super.wrapInIfNeeded(in)); 482 } 483 484 @Override 485 public void disableInterruptCharacter() { 486 } 487 488 @Override 489 public void enableInterruptCharacter() { 490 } 491 492 } 493 494 private static final class JShellWindowsTerminal extends WindowsTerminal { 495 496 private final StopDetectingInputStream input; 497 498 public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception { 499 this.input = input; 500 } 501 502 @Override 503 public void init() throws Exception { 504 super.init(); 505 setAnsiSupported(false); 506 } 507 508 @Override 509 public InputStream wrapInIfNeeded(InputStream in) throws IOException { 510 return input.setInputStream(super.wrapInIfNeeded(in)); 511 } 512 513 } 514 }