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