1 package gnu.kawa.io; 2 3 import java.io.*; 4 import java.util.List; 5 import gnu.expr.CommandCompleter; 6 import gnu.expr.Compilation; 7 import gnu.expr.Language; 8 import gnu.text.Lexer; 9 import gnu.text.SourceMessages; 10 import gnu.text.SyntaxException; 11 import org.jline.reader.Candidate; 12 import org.jline.reader.Completer; 13 import org.jline.reader.EndOfFileException; 14 import org.jline.reader.EOFError; 15 import org.jline.reader.LineReader; 16 import org.jline.reader.LineReaderBuilder; 17 import org.jline.reader.CompletingParsedLine; 18 import org.jline.reader.ParsedLine; 19 import org.jline.reader.Parser; 20 import org.jline.reader.Parser.ParseContext; 21 import org.jline.reader.UserInterruptException; 22 import org.jline.reader.SyntaxError; 23 import org.jline.reader.impl.DefaultParser; 24 import org.jline.terminal.Size; 25 import org.jline.terminal.Terminal; 26 import org.jline.terminal.TerminalBuilder; 27 import org.jline.terminal.impl.ExternalTerminal; 28 import java.nio.charset.Charset; 29 import java.nio.charset.StandardCharsets; 30 import java.util.ArrayList; 31 import java.util.List; 32 33 /** A variation of TtyInPort that uses the JLine library for input editing. */ 34 35 public class JLineInPort extends TtyInPort 36 implements Completer, Parser 37 { 38 LineReader jlreader; 39 org.jline.terminal.Terminal terminal; 40 String prompt; 41 SourceMessages messages; 42 String stringRest; 43 /** Remaining available characters in stringRest. */ 44 private int charsRest; 45 Language language; 46 JLineInPort(InputStream in, Path name, OutPort tie)47 public JLineInPort(InputStream in, Path name, OutPort tie) 48 throws java.io.IOException { 49 this(in, name, tie, TerminalBuilder.terminal()); 50 } makeTerminal(InputStream in, OutputStream out)51 private static Terminal makeTerminal(InputStream in, OutputStream out) throws IOException { 52 Terminal terminal = new ExternalTerminal("Kawa", "xterm-256color", 53 in, out, 54 StandardCharsets.UTF_8); 55 terminal.getAttributes().setOutputFlag(org.jline.terminal.Attributes.OutputFlag.ONLCR, true); 56 terminal.getAttributes().setOutputFlag(org.jline.terminal.Attributes.OutputFlag.OPOST, true); 57 return terminal; 58 } 59 JLineInPort(InputStream in, Path name, OutputStream out, OutPort tie)60 public JLineInPort(InputStream in, Path name, OutputStream out, OutPort tie) 61 throws java.io.IOException { 62 this(in, name, tie, makeTerminal(in, out)); 63 } JLineInPort(InputStream in, Path name, OutPort tie, Terminal terminal)64 public JLineInPort(InputStream in, Path name, OutPort tie, Terminal terminal) 65 throws java.io.IOException { 66 super(in, name, tie); 67 jlreader = LineReaderBuilder.builder() 68 .terminal(terminal) 69 .completer(this) 70 .parser(this) 71 .build(); 72 if (CheckConsole.useJLineMouse() > 0) 73 jlreader.setOpt(LineReader.Option.MOUSE); 74 this.terminal = terminal; 75 } 76 77 @Override setInDomTerm(boolean v)78 public void setInDomTerm(boolean v) { 79 super.setInDomTerm(v); 80 if (v) 81 jlreader.setOpt(LineReader.Option.DELAY_LINE_WRAP); 82 } 83 parse(String line, int cursor, ParseContext context)84 public ParsedLine parse(String line, int cursor, 85 ParseContext context) throws SyntaxError { 86 if (context == ParseContext.COMPLETE) 87 return parseForComplete(line, cursor); 88 CharArrayInPort cin = CharArrayInPort.make(line, "\n"); 89 cin.setLineNumber(this.getLineNumber()); 90 cin.setPath(this.getPath()); 91 if (language == null) 92 return new KawaParsedLine(this, line, cursor); 93 try { 94 Lexer lexer = language.getLexer(cin, this.messages); 95 lexer.setInteractive(true); 96 Compilation comp = 97 language.parse(lexer, 98 Language.PARSE_FOR_EVAL|Language.PARSE_INTERACTIVE_MODULE, 99 null); 100 if (comp == null) 101 throw new EndOfFileException(); 102 if (comp.getState() == Compilation.ERROR_SEEN && cin.eofSeen()) { 103 messages.clear(); 104 throw new EOFError(-1, -1, "unexpected end-of-file", ""); 105 } 106 return new KawaParsedLine(this, line, cursor, comp); 107 } catch (IOException ex) { 108 throw new RuntimeException(ex); 109 } 110 } 111 parseForComplete(String line, int cursor)112 ParsedLine parseForComplete(String line, int cursor) 113 throws SyntaxError { 114 int buflen = line.length(); 115 char[] tbuf = new char[buflen + 1]; 116 line.getChars(0, cursor, tbuf, 0); 117 tbuf[cursor] = CommandCompleter.COMPLETE_REQUEST; 118 line.getChars(cursor, buflen, tbuf, cursor+1); 119 CharArrayInPort cin = new CharArrayInPort(tbuf); 120 try { 121 SourceMessages messages = new SourceMessages(); 122 Lexer lexer = language.getLexer(cin, messages); 123 lexer.setInteractive(true); 124 lexer.setTentative(true); 125 Compilation comp = 126 language.parse(lexer, 127 Language.PARSE_FOR_EVAL|Language.PARSE_INTERACTIVE_MODULE, 128 null); 129 language.resolve(comp); 130 return new KawaParsedLine(this, line, cursor, comp); 131 } catch (SyntaxException ex) { 132 if (cin.eofSeen()) 133 throw new EOFError(-1, -1, "unexpected end-of-file", ""); 134 throw ex; 135 } catch (CommandCompleter ex) { 136 return new KawaParsedLine(this, line, cursor, ex); 137 } catch (IOException ex) { 138 throw new RuntimeException(ex); 139 } 140 } 141 142 @Override complete(LineReader reader, final ParsedLine commandLine, List<Candidate> candidates)143 public void complete(LineReader reader, final ParsedLine commandLine, 144 List<Candidate> candidates) { 145 KawaParsedLine kline = (KawaParsedLine) commandLine; 146 if (kline.ex != null) { 147 CommandCompleter ex = kline.ex; 148 java.util.Collections.sort(ex.candidates); 149 kline.word = ex.word; 150 kline.wordCursor = ex.wordCursor; 151 for (CharSequence cstr : ex.candidates) { 152 String str = cstr.toString(); 153 candidates.add(new Candidate(str, str, 154 null, null, null, null, true)); 155 } 156 } 157 } 158 159 @Override fill(int len)160 protected int fill(int len) throws java.io.IOException { 161 String line; 162 int count; 163 if (charsRest > 0) 164 line = stringRest; 165 else { 166 try { 167 jlreader.setVariable(LineReader.LINE_OFFSET, 168 getLineNumber()+1); 169 line = jlreader.readLine(prompt); 170 } catch (UserInterruptException ex) { 171 return -1; 172 } catch (EndOfFileException ex) { 173 promptEmitted = false; // Disable redundant newline. 174 return -1; 175 } 176 if (line == null) 177 return -1; 178 charsRest = line.length(); 179 } 180 int start = line.length()-charsRest; 181 if (charsRest < len) { 182 line.getChars(start, line.length(), buffer, pos); 183 buffer[pos+charsRest] = '\n'; 184 count = charsRest + 1; 185 charsRest = 0; 186 stringRest = null; 187 } else { 188 line.getChars(start, start+len, buffer, pos); 189 stringRest = line; 190 charsRest -= len; 191 count = len; 192 } 193 afterFill(count); 194 return count; 195 } 196 197 @Override promptTemplate1()198 public String promptTemplate1() { 199 return maybeColorizePrompt(super.promptTemplate1()); 200 } 201 202 @Override promptTemplate2()203 public String promptTemplate2() { 204 return maybeColorizePrompt(super.promptTemplate2()); 205 } 206 maybeColorizePrompt(String prompt)207 public String maybeColorizePrompt(String prompt) { 208 if (prompt.indexOf('\033') < 0 && prompt.indexOf("%{") < 0) 209 prompt = "\033[48;5;194m" + prompt + "\033[0m"; 210 return prompt; 211 } 212 213 @Override emitPrompt(String prompt)214 public void emitPrompt(String prompt) throws java.io.IOException { 215 this.prompt = prompt; 216 } 217 218 @Override expandPrompt(String pattern, int padToWidth, int line, String message, int[] width)219 public String expandPrompt(String pattern, int padToWidth, int line, 220 String message, int[] width) { 221 return pattern; 222 } setSize(int ncols, int nrows)223 public void setSize(int ncols, int nrows) { 224 Terminal term = terminal; 225 if (term != null) 226 term.setSize(new Size(ncols, nrows)); 227 } 228 229 @Override isJLine()230 public boolean isJLine() { return true; } 231 232 public static class KawaParsedLine implements CompletingParsedLine { 233 JLineInPort inp; 234 Compilation comp; 235 String source; 236 int cursor; 237 String word; 238 int wordCursor; 239 CommandCompleter ex; 240 KawaParsedLine(JLineInPort inp, String source, int cursor)241 public KawaParsedLine(JLineInPort inp, String source, int cursor) { 242 this.inp = inp; 243 this.source = source; 244 this.cursor = cursor; 245 this.word = ""; 246 } 247 KawaParsedLine(JLineInPort inp, String source, int cursor, Compilation comp)248 public KawaParsedLine(JLineInPort inp, String source, int cursor, Compilation comp) { 249 this.inp = inp; 250 this.comp = comp; 251 this.source = source; 252 this.cursor = cursor; 253 this.word = ""; 254 } 255 KawaParsedLine(JLineInPort inp, String source, int cursor, CommandCompleter ex)256 public KawaParsedLine(JLineInPort inp, String source, int cursor, CommandCompleter ex) { 257 this.inp = inp; 258 this.comp = ex.getCompilation(); 259 this.source = source; 260 this.cursor = cursor; 261 this.ex = ex; 262 } 263 264 // This method is called using reflection parse(Language language, Lexer lexer)265 public static Compilation parse(Language language, Lexer lexer) 266 throws java.io.IOException { 267 int opts = Language.PARSE_FOR_EVAL|Language.PARSE_ONE_LINE|Language.PARSE_INTERACTIVE_MODULE; 268 JLineInPort inp = (JLineInPort) lexer.getPort(); 269 if (inp.tie != null) 270 inp.tie.freshLine(); 271 int line = inp.getLineNumber() + 1; 272 Object p = null; 273 char saveState = inp.getReadState(); 274 inp.readState = ' '; 275 try { 276 if (inp.prompter != null) 277 p = inp.prompter.apply1(inp); 278 } catch (Throwable ex) { 279 } 280 String prompt = p == null ? "["+line+"] " : p.toString(); 281 inp.prompt = prompt; 282 LineReader jlreader = inp.jlreader; 283 jlreader.setVariable(LineReader.LINE_OFFSET, line); 284 String pattern2 = inp.promptTemplate2(); 285 jlreader.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, 286 pattern2); 287 inp.readState = saveState; 288 inp.messages = lexer.getMessages(); 289 Language saveLanguage = inp.language; // Normally null 290 inp.language = language; 291 try { 292 jlreader.readLine(inp.prompt); 293 if (inp.tie != null) 294 inp.tie.setColumnNumber(0); 295 KawaParsedLine parsedLine = (KawaParsedLine) jlreader.getParsedLine(); 296 inp.setLineNumber(line - 1 + parsedLine.lineCount()); 297 return parsedLine.comp; 298 } catch (org.jline.reader.EndOfFileException ex) { 299 return null; 300 } 301 finally { 302 inp.language = saveLanguage; 303 } 304 305 } word()306 public String word() { 307 return word; 308 } 309 wordCursor()310 public int wordCursor() { 311 return wordCursor; 312 } 313 wordIndex()314 public int wordIndex() { 315 return 0; 316 } 317 words()318 public List<String> words() { 319 return null; 320 } 321 line()322 public String line() { 323 return source; 324 } 325 lineCount()326 public int lineCount() { 327 int n = 1; 328 for (int i = 0; (i = source.indexOf('\n', i) + 1) > 0; ) 329 n++; 330 return n; 331 } 332 cursor()333 public int cursor() { 334 return cursor; 335 } escape(CharSequence candidate, boolean complete)336 public CharSequence escape(CharSequence candidate, boolean complete) { 337 return candidate; // FIXME 338 } rawWordCursor()339 public int rawWordCursor() { 340 return wordCursor(); 341 } rawWordLength()342 public int rawWordLength() { 343 return word().length(); 344 } 345 } 346 } 347