eb1fab705461981e889648cbb6ba453829af2dd4
[riece] / lisp / riece-ruby.el
1 ;;; riece-ruby.el --- interact with Ruby interpreter
2 ;; Copyright (C) 1998-2005 Daiki Ueno
3
4 ;; Author: Daiki Ueno <ueno@unixuser.org>
5 ;; Created: 1998-09-28
6 ;; Keywords: IRC, riece
7
8 ;; This file is part of Riece.
9
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;; riece-ruby.el is a library to interact with the Ruby interpreter.
28 ;; It supports concurrent execution of Ruby programs in a single
29 ;; session.  For example:
30 ;; 
31 ;; (riece-ruby-execute "sleep 30"); returns immediately
32 ;; => "rubyserv0"
33 ;;
34 ;; (riece-ruby-execute "1 + 1")
35 ;; => "rubyserv1"
36 ;;
37 ;; (riece-ruby-execute "\"")
38 ;; => "rubyserv2"
39 ;;
40 ;; (riece-ruby-inspect "rubyserv0")
41 ;; => ((OK nil) nil "running")
42 ;;
43 ;; (riece-ruby-inspect "rubyserv1")
44 ;; => ((OK nil) "2" "finished")
45 ;;
46 ;; (riece-ruby-inspect "rubyserv2")
47 ;; => ((OK nil) "(eval):1: unterminated string meets end of file" "exited")
48
49 ;;; Code:
50
51 (defgroup riece-ruby nil
52   "Interact with the Ruby interpreter."
53   :group 'riece)
54
55 (defcustom riece-ruby-command "ruby"
56   "Command name for Ruby interpreter."
57   :type 'string
58   :group 'riece-ruby)
59
60 (defvar riece-ruby-server-program "server.rb"
61   "The server program file.  If the filename is not absolute, it is
62 assumed that the file is in the same directory of this file.")
63
64 (defvar riece-ruby-process nil
65   "Process object of the Ruby interpreter.")
66
67 (defvar riece-ruby-lock nil
68   "Lock for waiting server response.
69 Local to the process buffer.")
70 (defvar riece-ruby-response nil
71   "The server response.
72 Local to the process buffer.")
73 (defvar riece-ruby-data nil
74   "Data from server.
75 Local to the process buffer.")
76 (defvar riece-ruby-escaped-data nil
77   "Escaped data from server.  This variable is cleared every time
78 server response arrives.
79 Local to the process buffer.")
80 (defvar riece-ruby-status-alist nil
81   "Status from server.
82 Local to the process buffer.")
83
84 (defvar riece-ruby-output-handler-alist nil
85   "An alist mapping from program name to output handler.
86 Output handlers are called every time \"# output\" line arrives.
87 Use `riece-ruby-set-output-handler' to set this variable.")
88 (defvar riece-ruby-exit-handler-alist nil
89   "An alist mapping from program name to exit handler.
90 Exit handlers are called once when \"# exit\" line arrives.
91 Use `riece-ruby-set-exit-handler' to set this variable.")
92
93 (defun riece-ruby-substitute-variables (program alist)
94   (setq program (copy-sequence program))
95   (while alist
96     (let ((pointer program))
97       (while pointer
98         (setq pointer (memq (car (car alist)) program))
99         (if pointer
100             (setcar pointer (cdr (car alist))))))
101     (setq alist (cdr alist)))
102   (apply #'concat program))
103
104 (defun riece-ruby-escape-data (data)
105   (let ((index 0))
106     (while (string-match "[%\r\n]+" data index)
107       (setq data (replace-match
108                   (mapconcat (lambda (c) (format "%%%02X" c))
109                              (match-string 0 data) "")
110                   nil nil data)
111             index (+ (match-end 0)
112                      (* (- (match-end 0) (match-beginning 0)) 2))))
113     data))
114
115 (defun riece-ruby-unescape-data (data)
116   (let ((index 0))
117     (while (string-match "%\\([0-9A-F][0-9A-F]\\)" data index)
118       (setq data (replace-match
119                   (read (concat "\"\\x" (match-string 1 data) "\""))
120                   nil nil data)
121             index (- (match-end 0) 2)))
122     data))
123
124 (defun riece-ruby-reset-process-buffer ()
125   (save-excursion
126     (set-buffer (process-buffer riece-ruby-process))
127     (buffer-disable-undo)
128     (make-local-variable 'riece-ruby-response)
129     (setq riece-ruby-response nil)
130     (make-local-variable 'riece-ruby-data)
131     (setq riece-ruby-data nil)
132     (make-local-variable 'riece-ruby-escaped-data)
133     (setq riece-ruby-escaped-data nil)
134     (make-local-variable 'riece-ruby-status-alist)
135     (setq riece-ruby-status-alist nil)))
136
137 (defun riece-ruby-send-eval (program)
138   (let* ((string (riece-ruby-escape-data program))
139          (length (- (length string) 998))
140          (index 0)
141          data)
142     (while (< index length)
143       (setq data (cons (substring string index (setq index (+ index 998)))
144                        data)))
145     (setq data (cons (substring string index) data)
146           data (nreverse data))
147     (process-send-string riece-ruby-process "EVAL\r\n")
148     (while data
149       (process-send-string riece-ruby-process
150                            (concat "D " (car data) "\r\n"))
151       (setq data (cdr data)))
152     (process-send-string riece-ruby-process "END\r\n")))
153
154 (defun riece-ruby-send-poll (name)
155   (process-send-string riece-ruby-process
156                        (concat "POLL " name "\r\n")))
157
158 (defun riece-ruby-send-exit (name)
159   (process-send-string riece-ruby-process
160                        (concat "EXIT " name "\r\n")))
161
162 (defun riece-ruby-filter (process input)
163   (save-excursion
164     (set-buffer (process-buffer process))
165     (goto-char (point-max))
166     (insert input)
167     (goto-char (process-mark process))
168     (beginning-of-line)
169     (while (looking-at ".*\r\n")
170       (if (looking-at "OK\\( \\(.*\\)\\)?\r")
171           (progn
172             (if riece-ruby-escaped-data
173                 (setq riece-ruby-data (mapconcat #'riece-ruby-unescape-data
174                                                  riece-ruby-escaped-data "")))
175             (setq riece-ruby-escaped-data nil
176                   riece-ruby-response (list 'OK (match-string 2))
177                   riece-ruby-lock nil))
178         (if (looking-at "ERR \\([0-9]+\\)\\( \\(.*\\)\\)?\r")
179             (progn
180               (setq riece-ruby-escaped-data nil
181                     riece-ruby-response
182                     (list 'ERR (string-to-number (match-string 2))
183                           (match-string 3))
184                     riece-ruby-lock nil))
185           (if (looking-at "D \\(.*\\)\r")
186               (setq riece-ruby-escaped-data (cons (match-string 1)
187                                                   riece-ruby-escaped-data))
188             (if (looking-at "S \\(.*\\) \\(.*\\)\r")
189                 (progn
190                   (setq riece-ruby-status-alist (cons (cons (match-string 1)
191                                                             (match-string 2))
192                                                       riece-ruby-status-alist))
193                   (if (member (car (car riece-ruby-status-alist))
194                               '("finished" "exited"))
195                       (riece-ruby-run-exit-handler
196                        (cdr (car riece-ruby-status-alist)))))
197               (if (looking-at "# output \\(.*\\) \\(.*\\)\r")
198                   (let ((entry (assoc (match-string 1)
199                                       riece-ruby-output-handler-alist)))
200                     (if entry
201                         (funcall (car entry) (cdr entry) (match-string 2))))
202                 (if (looking-at "# exit \\(.*\\)\r")
203                     (riece-ruby-run-exit-handler (match-string 1))))))))
204       (forward-line))
205     (set-marker (process-mark process) (point-marker))))
206
207 (defun riece-ruby-run-exit-handler (name)
208   (let ((entry (assoc name riece-ruby-exit-handler-alist)))
209     (if entry
210         (progn
211           (funcall (cdr entry) (car entry))
212           (setq riece-ruby-exit-handler-alist (delq entry))))))
213
214 (defun riece-ruby-sentinel (process status)
215   (kill-buffer (process-buffer process)))
216
217 (defun riece-ruby-execute (program)
218   (unless (and riece-ruby-process
219                (eq (process-status riece-ruby-process) 'run))
220     (let (selective-display
221           (coding-system-for-write 'binary)
222           (coding-system-for-read 'binary))
223       (setq riece-ruby-process
224             (start-process "riece-ruby" (generate-new-buffer " *Ruby*")
225                            riece-ruby-command
226                            (if (file-name-absolute-p riece-ruby-server-program)
227                                riece-ruby-server-program
228                              (expand-file-name
229                               riece-ruby-server-program
230                               (file-name-directory
231                                (symbol-file 'riece-ruby-execute))))))
232       (set-process-filter riece-ruby-process #'riece-ruby-filter)
233       (set-process-sentinel riece-ruby-process #'riece-ruby-sentinel)))
234   (save-excursion
235     (set-buffer (process-buffer riece-ruby-process))
236     (riece-ruby-reset-process-buffer)
237     (make-local-variable 'riece-ruby-lock)
238     (setq riece-ruby-lock t)
239     (riece-ruby-send-eval program)
240     (while riece-ruby-lock
241       (accept-process-output riece-ruby-process))
242     (if (eq (car riece-ruby-response) 'ERR)
243         (error "Couldn't execute: %S" (cdr riece-ruby-response)))
244     (cdr (assoc "name" riece-ruby-status-alist))))
245
246 (defun riece-ruby-inspect (name)
247   (save-excursion
248     (set-buffer (process-buffer riece-ruby-process))
249     (riece-ruby-reset-process-buffer)
250     (make-local-variable 'riece-ruby-lock)
251     (setq riece-ruby-lock t)
252     (riece-ruby-send-poll name)
253     (while (null riece-ruby-response)
254       (accept-process-output riece-ruby-process))
255     (list riece-ruby-response
256           riece-ruby-data
257           riece-ruby-status-alist)))
258
259 (defun riece-ruby-clear (name)
260   (save-excursion
261     (set-buffer (process-buffer riece-ruby-process))
262     (riece-ruby-reset-process-buffer)
263     (make-local-variable 'riece-ruby-lock)
264     (setq riece-ruby-lock t)
265     (riece-ruby-send-exit name)
266     (while (null riece-ruby-response)
267       (accept-process-output riece-ruby-process))))
268
269 (defun riece-ruby-set-exit-handler (name handler)
270   (let ((entry (assoc name riece-ruby-exit-handler-alist)))
271     (if entry
272         (setcdr entry handler)
273       (setq riece-ruby-exit-handler-alist
274             (cons (cons name handler)
275                   riece-ruby-exit-handler-alist)))
276     ;;check if the program already exited
277     (riece-ruby-inspect)))
278
279 (defun riece-ruby-set-output-handler (name handler)
280   (let ((entry (assoc name riece-ruby-output-handler-alist)))
281     (if entry
282         (setcdr entry handler)
283       (setq riece-ruby-output-handler-alist
284             (cons (cons name handler)
285                   riece-ruby-output-handler-alist)))))
286
287 (provide 'riece-ruby)
288
289 ;;; riece-ruby.el ends here