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.Suggestion; 30 31 import java.awt.event.ActionListener; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.PrintStream; 35 import java.io.UncheckedIOException; 36 import java.lang.reflect.Method; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Objects; 40 import java.util.Optional; 41 import java.util.function.Supplier; 42 43 import jdk.internal.jline.NoInterruptUnixTerminal; 44 import jdk.internal.jline.Terminal; 45 import jdk.internal.jline.TerminalFactory; 46 import jdk.internal.jline.WindowsTerminal; 47 import jdk.internal.jline.console.ConsoleReader; 48 import jdk.internal.jline.console.KeyMap; 49 import jdk.internal.jline.console.UserInterruptException; 50 import jdk.internal.jline.console.completer.Completer; 51 import jdk.internal.jshell.tool.StopDetectingInputStream.State; 52 53 class ConsoleIOContext extends IOContext { 54 55 final JShellTool repl; 56 final StopDetectingInputStream input; 57 final ConsoleReader in; 58 final EditingHistory history; 59 60 String prefix = ""; 61 62 ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception { 63 this.repl = repl; 64 this.input = new StopDetectingInputStream(() -> repl.state.stop(), ex -> repl.hard("Error on input: %s", ex)); 65 Terminal term; 66 if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) { 67 term = new JShellWindowsTerminal(input); 68 } else { 69 term = new JShellUnixTerminal(input); 70 } 71 term.init(); 72 in = new ConsoleReader(cmdin, cmdout, term); 73 in.setExpandEvents(false); 74 in.setHandleUserInterrupt(true); 75 in.setHistory(history = new EditingHistory(JShellTool.PREFS) { 76 @Override protected CompletionInfo analyzeCompletion(String input) { 77 return repl.analysis.analyzeCompletion(input); 78 } 79 }); 80 in.setBellEnabled(true); 81 in.addCompleter(new Completer() { 82 private String lastTest; 83 private int lastCursor; 84 private boolean allowSmart = false; 85 @Override public int complete(String test, int cursor, List<CharSequence> result) { 86 int[] anchor = new int[] {-1}; 87 List<Suggestion> suggestions; 88 if (prefix.isEmpty() && test.trim().startsWith("/")) { 89 suggestions = repl.commandCompletionSuggestions(test, cursor, anchor); 90 } else { 91 int prefixLength = prefix.length(); 92 suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor); 93 anchor[0] -= prefixLength; 94 } 95 if (!Objects.equals(lastTest, test) || lastCursor != cursor) 96 allowSmart = true; 97 98 boolean smart = allowSmart && 99 suggestions.stream() 100 .anyMatch(s -> s.isSmart); 101 102 lastTest = test; 103 lastCursor = cursor; 104 allowSmart = !allowSmart; 105 106 suggestions.stream() 107 .filter(s -> !smart || s.isSmart) 108 .map(s -> s.continuation) 109 .forEach(result::add); 110 111 boolean onlySmart = suggestions.stream() 112 .allMatch(s -> s.isSmart); 113 114 if (smart && !onlySmart) { 115 Optional<String> prefix = 116 suggestions.stream() 117 .map(s -> s.continuation) 118 .reduce(ConsoleIOContext::commonPrefix); 119 120 String prefixStr = prefix.orElse("").substring(cursor - anchor[0]); 121 try { 122 in.putString(prefixStr); 123 cursor += prefixStr.length(); 124 } catch (IOException ex) { 125 throw new IllegalStateException(ex); 126 } 127 result.add("<press tab to see more>"); 128 return cursor; //anchor should not be used. 129 } 130 131 if (result.isEmpty()) { 132 try { 133 //provide "empty completion" feedback 134 //XXX: this only works correctly when there is only one Completer: 135 in.beep(); 136 } catch (IOException ex) { 137 throw new UncheckedIOException(ex); 138 } 139 } 140 141 return anchor[0]; 142 } 143 }); 144 bind(DOCUMENTATION_SHORTCUT, (ActionListener) evt -> documentation(repl)); 145 bind(CTRL_UP, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::previousSnippet)); 146 bind(CTRL_DOWN, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::nextSnippet)); 147 } 148 149 @Override 150 public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException { 151 this.prefix = prefix; 152 try { 153 return in.readLine(prompt); 154 } catch (UserInterruptException ex) { 155 throw (InputInterruptedException) new InputInterruptedException().initCause(ex); 156 } 157 } 158 159 @Override 160 public boolean interactiveOutput() { 161 return true; 162 } 163 164 @Override 165 public Iterable<String> currentSessionHistory() { 166 return history.currentSessionEntries(); 167 } 168 169 @Override 170 public void close() throws IOException { 171 history.save(); 172 in.shutdown(); 173 try { 174 in.getTerminal().restore(); 175 } catch (Exception ex) { 176 throw new IOException(ex); 177 } 178 } 179 180 private void moveHistoryToSnippet(Supplier<Boolean> action) { 181 if (!action.get()) { 182 try { 183 in.beep(); 184 } catch (IOException ex) { 185 throw new IllegalStateException(ex); 186 } 187 } else { 188 try { 189 //could use: 190 //in.resetPromptLine(in.getPrompt(), in.getHistory().current().toString(), -1); 191 //but that would mean more re-writing on the screen, (and prints an additional 192 //empty line), so using setBuffer directly: 193 Method setBuffer = in.getClass().getDeclaredMethod("setBuffer", String.class); 194 195 setBuffer.setAccessible(true); 196 setBuffer.invoke(in, in.getHistory().current().toString()); 197 in.flush(); 198 } catch (ReflectiveOperationException | IOException ex) { 199 throw new IllegalStateException(ex); 200 } 201 } 202 } 203 204 private void bind(String shortcut, Object action) { 205 KeyMap km = in.getKeys(); 206 for (int i = 0; i < shortcut.length(); i++) { 207 Object value = km.getBound(Character.toString(shortcut.charAt(i))); 208 if (value instanceof KeyMap) { 209 km = (KeyMap) value; 210 } else { 211 km.bind(shortcut.substring(i), action); 212 } 213 } 214 } 215 216 private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB 217 private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP 218 private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN 219 220 private void documentation(JShellTool repl) { 221 String buffer = in.getCursorBuffer().buffer.toString(); 222 int cursor = in.getCursorBuffer().cursor; 223 String doc; 224 if (prefix.isEmpty() && buffer.trim().startsWith("/")) { 225 doc = repl.commandDocumentation(buffer, cursor); 226 } else { 227 doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length()); 228 } 229 230 try { 231 if (doc != null) { 232 in.println(); 233 in.println(doc); 234 in.redrawLine(); 235 in.flush(); 236 } else { 237 in.beep(); 238 } 239 } catch (IOException ex) { 240 throw new IllegalStateException(ex); 241 } 242 } 243 244 private static String commonPrefix(String str1, String str2) { 245 for (int i = 0; i < str2.length(); i++) { 246 if (!str1.startsWith(str2.substring(0, i + 1))) { 247 return str2.substring(0, i); 248 } 249 } 250 251 return str2; 252 } 253 254 @Override 255 public boolean terminalEditorRunning() { 256 Terminal terminal = in.getTerminal(); 257 if (terminal instanceof JShellUnixTerminal) 258 return ((JShellUnixTerminal) terminal).isRaw(); 259 return false; 260 } 261 262 @Override 263 public void suspend() { 264 try { 265 in.getTerminal().restore(); 266 } catch (Exception ex) { 267 throw new IllegalStateException(ex); 268 } 269 } 270 271 @Override 272 public void resume() { 273 try { 274 in.getTerminal().init(); 275 } catch (Exception ex) { 276 throw new IllegalStateException(ex); 277 } 278 } 279 280 public void beforeUserCode() { 281 input.setState(State.BUFFER); 282 } 283 284 public void afterUserCode() { 285 input.setState(State.WAIT); 286 } 287 288 @Override 289 public void replaceLastHistoryEntry(String source) { 290 history.fullHistoryReplace(source); 291 } 292 293 private static final class JShellUnixTerminal extends NoInterruptUnixTerminal { 294 295 private final StopDetectingInputStream input; 296 297 public JShellUnixTerminal(StopDetectingInputStream input) throws Exception { 298 this.input = input; 299 } 300 301 public boolean isRaw() { 302 try { 303 return getSettings().get("-a").contains("-icanon"); 304 } catch (IOException | InterruptedException ex) { 305 return false; 306 } 307 } 308 309 @Override 310 public InputStream wrapInIfNeeded(InputStream in) throws IOException { 311 return input.setInputStream(super.wrapInIfNeeded(in)); 312 } 313 314 @Override 315 public void disableInterruptCharacter() { 316 } 317 318 @Override 319 public void enableInterruptCharacter() { 320 } 321 322 } 323 324 private static final class JShellWindowsTerminal extends WindowsTerminal { 325 326 private final StopDetectingInputStream input; 327 328 public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception { 329 this.input = input; 330 } 331 332 @Override 333 public void init() throws Exception { 334 super.init(); 335 setAnsiSupported(false); 336 } 337 338 @Override 339 public InputStream wrapInIfNeeded(InputStream in) throws IOException { 340 return input.setInputStream(super.wrapInIfNeeded(in)); 341 } 342 343 } 344 }