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