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; 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(); 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 } 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 } | 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; 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(); 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 } 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 } |