1 /*
2  * ConsoleDocument.java
3  *
4  * Copyright (C) 2008-2009 Alessio Stalla
5  *
6  * $Id$
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
21  *
22  * As a special exception, the copyright holders of this library give you
23  * permission to link this library with independent modules to produce an
24  * executable, regardless of the license terms of these independent
25  * modules, and to copy and distribute the resulting executable under
26  * terms of your choice, provided that you also meet, for each linked
27  * independent module, the terms and conditions of the license of that
28  * module.  An independent module is a module which is not derived from
29  * or based on this library.  If you modify this library, you may extend
30  * this exception to your version of the library, but you are not
31  * obligated to do so.  If you do not wish to do so, delete this
32  * exception statement from your version.
33  */
34 
35 package org.armedbear.lisp.java.swing;
36 
37 import java.awt.Window;
38 import java.awt.event.WindowAdapter;
39 import java.awt.event.WindowEvent;
40 import java.io.BufferedReader;
41 import java.io.BufferedWriter;
42 import java.lang.RuntimeException;
43 import java.io.Reader;
44 import java.io.Writer;
45 
46 import javax.swing.JFrame;
47 import javax.swing.JScrollPane;
48 import javax.swing.JTextArea;
49 import javax.swing.SwingUtilities;
50 import javax.swing.event.DocumentEvent;
51 import javax.swing.event.DocumentListener;
52 import javax.swing.text.AttributeSet;
53 import javax.swing.text.BadLocationException;
54 import javax.swing.text.DefaultStyledDocument;
55 import javax.swing.text.JTextComponent;
56 import org.armedbear.lisp.Function;
57 import org.armedbear.lisp.Interpreter;
58 import org.armedbear.lisp.LispObject;
59 import org.armedbear.lisp.LispThread;
60 import org.armedbear.lisp.SpecialBindingsMark;
61 import org.armedbear.lisp.Stream;
62 import org.armedbear.lisp.Symbol;
63 import org.armedbear.lisp.TwoWayStream;
64 
65 import static org.armedbear.lisp.Lisp.*;
66 
67 public class REPLConsole extends DefaultStyledDocument {
68 
69   private StringBuffer inputBuffer = new StringBuffer();
70 
71   private Reader reader = new Reader() {
72 
73       @Override
74       public void close() throws RuntimeException {}
75 
76       @Override
77       public synchronized int read(char[] cbuf, int off, int len) throws RuntimeException {
78         try {
79           int length = Math.min(inputBuffer.length(), len);
80           while(length <= 0) {
81             wait();
82             length = Math.min(inputBuffer.length(), len);
83           }
84           inputBuffer.getChars(0, length, cbuf, off);
85           inputBuffer.delete(0, length);
86           return length;
87         } catch (InterruptedException e) {
88           throw new RuntimeException(e);
89         }
90       }
91     };
92 
93   private Writer writer = new Writer() {
94 
95       @Override
96       public void close() throws RuntimeException {}
97 
98       @Override
99       public void flush() throws RuntimeException {}
100 
101       @Override
102       public void write(final char[] cbuf, final int off, final int len) throws RuntimeException {
103         try {
104           final int insertOffs;
105           synchronized(reader) {
106             if(inputBuffer.toString().matches("^\\s*$")) {
107               int length = inputBuffer.length();
108               inputBuffer.delete(0, length);
109             }
110             insertOffs = getLength() - inputBuffer.length();
111             reader.notifyAll();
112           }
113           Runnable r = new Runnable() {
114               public void run() {
115                 synchronized(reader) {
116                   try {
117                     superInsertString(insertOffs,
118                                       new String(cbuf, off, len),
119                                       null);
120                   } catch(Exception e) {
121                     assert(false); //BadLocationException should not happen here
122                   }
123                 }
124               }
125             };
126           SwingUtilities.invokeAndWait(r);
127         }  catch (Exception e) {
128           throw new RuntimeException(e);
129         }
130       }
131     };
132 
133   private boolean disposed = false;
134   private final Thread replThread;
135 
REPLConsole(LispObject replFunction)136   public REPLConsole(LispObject replFunction) {
137     final LispObject replWrapper = makeReplWrapper(new Stream(Symbol.SYSTEM_STREAM, new BufferedReader(reader)),
138                                                    new Stream(Symbol.SYSTEM_STREAM, new BufferedWriter(writer)),
139                                                    replFunction);
140     replThread = new Thread("REPL-thread-" + System.identityHashCode(this)) {
141         public void run() {
142           while(true) {
143             replWrapper.execute();
144             java.lang.Thread.yield();
145           }
146         }
147       };
148     replThread.start();
149   }
150 
151   @Override
insertString(int offs, String str, AttributeSet a)152   public void insertString(int offs, String str, AttributeSet a)
153     throws BadLocationException {
154     synchronized(reader) {
155       int bufferStart = getLength() - inputBuffer.length();
156       if(offs < bufferStart) {
157         throw new BadLocationException("Can only insert after " + bufferStart, offs);
158       }
159       superInsertString(offs, str, a);
160       inputBuffer.insert(offs - bufferStart, str);
161       if(processInputP(inputBuffer, str)) {
162         reader.notifyAll();
163       }
164     }
165   }
166 
superInsertString(int offs, String str, AttributeSet a)167   protected void superInsertString(int offs, String str, AttributeSet a)
168     throws BadLocationException {
169     super.insertString(offs, str, a);
170   }
171 
172   /**
173    * Guaranteed to run with exclusive access to the buffer.
174    * @param sb NB sb MUST NOT be destructively modified!!
175    * @return
176    */
processInputP(StringBuffer sb, String str)177   protected boolean processInputP(StringBuffer sb, String str) {
178     if(str.indexOf("\n") == -1) {
179       return false;
180     }
181     int parenCount = 0;
182     int len = sb.length();
183     for(int i = 0; i < len; i++) {
184       char c = sb.charAt(i);
185       if(c == '(') {
186         parenCount++;
187       } else if(c == ')') {
188         parenCount--;
189         if(parenCount == 0) {
190           return true;
191         }
192       }
193     }
194     return parenCount <= 0;
195   }
196 
197   @Override
remove(int offs, int len)198   public void remove(int offs, int len) throws BadLocationException {
199     synchronized(reader) {
200       int bufferStart = getLength() - inputBuffer.length();
201       if(offs < bufferStart) {
202         throw new BadLocationException("Can only remove after " + bufferStart, offs);
203       }
204       super.remove(offs, len);
205       inputBuffer.delete(offs - bufferStart, offs - bufferStart + len);
206     }
207   }
208 
getReader()209   public Reader getReader() {
210     return reader;
211   }
212 
getWriter()213   public Writer getWriter() {
214     return writer;
215   }
216 
setupTextComponent(final JTextComponent txt)217   public void setupTextComponent(final JTextComponent txt) {
218     addDocumentListener(new DocumentListener() {
219 
220         //			@Override
221         public void changedUpdate(DocumentEvent e) {
222         }
223 
224         // @Override
225         public void insertUpdate(DocumentEvent e) {
226           int len = getLength();
227           if(len - e.getLength() == e.getOffset()) { //The insert was at the end of the document
228             txt.setCaretPosition(getLength());
229           }
230         }
231 
232         // @Override
233         public void removeUpdate(DocumentEvent e) {
234         }
235       });
236     txt.setCaretPosition(getLength());
237   }
238 
dispose()239   public void dispose() {
240     disposed = true;
241     for(DocumentListener listener : getDocumentListeners()) {
242       removeDocumentListener(listener);
243     }
244     try {
245       reader.close();
246       writer.close();
247     } catch (Exception e) {
248       throw new RuntimeException(e);
249     }
250     replThread.interrupt(); //really?
251   }
252 
253   private final LispObject debuggerHook = new Function() {
254 
255       @Override
256       public LispObject execute(LispObject condition, LispObject debuggerHook) {
257         if(disposed) {
258           return PACKAGE_SYS.findSymbol("%DEBUGGER-HOOK-FUNCTION").execute(condition, debuggerHook);
259         } else {
260           return NIL;
261         }
262       }
263 
264     };
265 
makeReplWrapper(final Stream in, final Stream out, final LispObject fn)266   public LispObject makeReplWrapper(final Stream in, final Stream out, final LispObject fn) {
267     return new Function() {
268       @Override
269       public LispObject execute() {
270         SpecialBindingsMark lastSpecialBinding = LispThread.currentThread().markSpecialBindings();
271         try {
272           TwoWayStream ioStream = new TwoWayStream(in, out);
273           LispThread.currentThread().bindSpecial(Symbol.DEBUGGER_HOOK, debuggerHook);
274           LispThread.currentThread().bindSpecial(Symbol.STANDARD_INPUT, in);
275           LispThread.currentThread().bindSpecial(Symbol.STANDARD_OUTPUT, out);
276           LispThread.currentThread().bindSpecial(Symbol.ERROR_OUTPUT, out);
277           LispThread.currentThread().bindSpecial(Symbol.TERMINAL_IO, ioStream);
278           LispThread.currentThread().bindSpecial(Symbol.DEBUG_IO, ioStream);
279           LispThread.currentThread().bindSpecial(Symbol.QUERY_IO, ioStream);
280           return fn.execute();
281         } finally {
282           LispThread.currentThread().resetSpecialBindings(lastSpecialBinding);
283         }
284       }
285 
286     };
287   }
288 
289   public void disposeOnClose(final Window parent) {
290     parent.addWindowListener(new WindowAdapter() {
291         @Override
292         public void windowClosing(WindowEvent e) {
293           dispose();
294           parent.removeWindowListener(this);
295         }
296       });
297   }
298 
299   public static void main(String[] args) {
300     LispObject repl = null;
301     try {
302       repl = Interpreter.createInstance().eval("#'top-level::top-level-loop");
303     } catch (Throwable e) {
304       e.printStackTrace();
305       System.exit(1);  // Ok. We haven't done anything useful yet.
306     }
307     final REPLConsole d = new REPLConsole(repl);
308     final JTextComponent txt = new JTextArea(d);
309     d.setupTextComponent(txt);
310     JFrame f = new JFrame();
311     f.add(new JScrollPane(txt));
312     d.disposeOnClose(f);
313     f.setDefaultCloseOperation(f.EXIT_ON_CLOSE);
314     f.pack();
315     f.setVisible(true);
316   }
317 
318 }
319