/
ob-julia-vterm.el
325 lines (294 loc) · 12.5 KB
/
ob-julia-vterm.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
;;; ob-julia-vterm.el --- Babel functions for Julia that work with julia-vterm -*- lexical-binding: t -*-
;; Copyright (C) 2020-2024 Shigeaki Nishina
;; Author: Shigeaki Nishina
;; Maintainer: Shigeaki Nishina
;; Created: October 31, 2020
;; URL: https://github.com/shg/ob-julia-vterm.el
;; Package-Requires: ((emacs "26.1") (julia-vterm "0.25") (queue "0.2"))
;; Version: 0.5
;; Keywords: julia, org, outlines, literate programming, reproducible research
;; This file is not part of GNU Emacs.
;;; License:
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see https://www.gnu.org/licenses/.
;;; Commentary:
;; Org-Babel support for Julia source code block using julia-vterm.
;;; Requirements:
;; This package uses julia-vterm to run Julia code.
;;
;; - https://github.com/shg/julia-vterm.el
;;
;; See https://github.com/shg/ob-julia-vterm.el for installation
;; instructions.
;;; Code:
(require 'ob)
(require 'org-id)
(require 'queue)
(require 'filenotify)
(require 'julia-vterm)
(defun ob-julia-vterm-wrap-body (session body)
"Make Julia code that execute-s BODY and obtains the results, depending on SESSION."
(concat
(if session "" "let\n")
body
(if session "" "\nend\n")))
(defun ob-julia-vterm-make-str-to-run (uuid params src-file out-file)
"Make Julia code that execute-s the code in SRC-FILE depending on PARAMS.
The results are saved in OUT-FILE. UUID is a unique id assigned
to the evaluation."
(format
(pcase (cdr (assq :result-type params))
('output "\
#OB-JULIA-VTERM_BEGIN %s
import Logging; let
out_file = \"%s\"
open(out_file, \"w\") do io
logger = Logging.ConsoleLogger(io)
redirect_stdout(io) do
try
include(\"%s\")
# %s %s
catch e
showerror(logger.stream, e, %s)
end
end
end
result = open(io -> println(read(io, String)), out_file)
if result == nothing
open(out_file, \"a\") do io
print(io, \"\n\")
end
else
result
end
end #OB-JULIA-VTERM_END\n")
('value "\
#OB-JULIA-VTERM_BEGIN %s
import Logging; open(\"%s\", \"w\") do io
logger = Logging.ConsoleLogger(io)
try
result = include(\"%s\")
if %s
if isdefined(Main, :PrettyPrinting) && isdefined(PrettyPrinting, :pprint) ||
\"PrettyPrinting\" in [p.name for p in values(Pkg.dependencies())]
@eval import PrettyPrinting
Base.invokelatest(PrettyPrinting.pprint, io, result)
else
Base.invokelatest(print, io, result)
end
else
if %s
Base.invokelatest(show, io, \"text/plain\", result)
else
Base.invokelatest(show, IOContext(io, :limit => true), \"text/plain\", result)
end
end
result
catch e
msg = sprint(showerror, e, %s)
println(logger.stream, msg)
println(msg)
end
end #OB-JULIA-VTERM_END\n"))
(substring uuid 0 8) out-file src-file
(if (member "pp" (cdr (assq :result-params params))) "true" "false")
(if (member "nolimit" (cdr (assq :result-params params))) "true" "false")
(if (not (member (cdr (assq :debug params)) '(nil "no"))) "catch_backtrace()" "")))
(defun org-babel-execute:julia-vterm (body params)
"Execute a block of Julia code with Babel.
This function is called by `org-babel-execute-src-block'.
BODY is the contents and PARAMS are header arguments of the code block."
(let* ((session-name (cdr (assq :session params)))
(session (pcase session-name ('nil "main") ("none" nil) (_ session-name)))
(var-lines (org-babel-variable-assignments:julia-vterm params))
(result-params (cdr (assq :result-params params))))
(with-current-buffer (julia-vterm-repl-buffer session)
(add-hook 'julia-vterm-repl-filter-functions #'ob-julia-vterm-output-filter))
(ob-julia-vterm-evaluate (current-buffer)
session
(org-babel-expand-body:generic body params var-lines)
params)))
(defun org-babel-variable-assignments:julia-vterm (params)
"Return list of Julia statements assigning variables based on variable-value pairs in PARAMS."
(mapcar
(lambda (pair)
(format "%s = %s" (car pair) (ob-julia-vterm-value-to-julia (cdr pair))))
(org-babel--get-vars params)))
(defun ob-julia-vterm-escape-string (str)
"Escape special characters in STR for Julia variable assignments."
(replace-regexp-in-string "\"" "\\\\\"" str))
(defun ob-julia-vterm-value-to-julia (value)
"Convert an emacs-lisp VALUE to a string of julia code for the value."
(cond
((listp value) (format "\"%s\"" value))
((numberp value) value)
((stringp value) (or (org-babel--string-to-number value)
(concat "\"" (ob-julia-vterm-escape-string value) "\"")))
((symbolp value) (ob-julia-vterm-escape-string (symbol-name value)))
(t value)))
(defun ob-julia-vterm-check-long-line (str)
"Return t if STR is too long for org-babel result."
(catch 'loop
(dolist (line (split-string str "\n"))
(if (> (length line) 12000)
(throw 'loop t)))))
(defvar-local ob-julia-vterm-evaluation-queue nil)
(defvar-local ob-julia-vterm-evaluation-watches nil)
(defun ob-julia-vterm-add-evaluation-to-evaluation-queue (session evaluation)
"Add an EVALUATION of a source block to SESSION's evaluation queue."
(with-current-buffer (julia-vterm-repl-buffer session)
(if (not (queue-p ob-julia-vterm-evaluation-queue))
(setq ob-julia-vterm-evaluation-queue (queue-create)))
(queue-append ob-julia-vterm-evaluation-queue evaluation)))
(defun ob-julia-vterm-evaluation-completed-callback-func (session)
"Return a callback function to be called when an evaluation in SESSION is completed."
(lambda (event)
(if (eq 'changed (cadr event))
(with-current-buffer (julia-vterm-repl-buffer session)
(if (and (queue-p ob-julia-vterm-evaluation-queue)
(> (queue-length ob-julia-vterm-evaluation-queue) 0))
(let-alist (queue-first ob-julia-vterm-evaluation-queue)
(with-current-buffer .buf
(save-excursion
(goto-char .src-block-begin)
(when (and (not (equal .src-block-begin .src-block-end))
(or (eq (org-element-type (org-element-context)) 'src-block)
(eq (org-element-type (org-element-context)) 'inline-src-block)))
(ob-julia-vterm-wait-for-file-change .out-file 10 0.1)
(let ((result (with-temp-buffer
(insert-file-contents .out-file)
(replace-regexp-in-string "\n\\'" "" (buffer-string))))
(result-params (cdr (assq :result-params .params))))
(cond ((member "file" result-params)
(org-redisplay-inline-images))
((not (member "none" result-params))
(org-babel-insert-result
(if (ob-julia-vterm-check-long-line result)
"Output suppressed (line too long)"
(org-babel-result-cond result-params
result
(org-babel-reassemble-table
result
(org-babel-pick-name (cdr (assq :colname-names .params))
(cdr (assq :colnames .params)))
(org-babel-pick-name (cdr (assq :rowname-names .params))
(cdr (assq :rownames .params))))))
result-params
(org-babel-get-src-block-info 'light))))))))
(queue-dequeue ob-julia-vterm-evaluation-queue)
(file-notify-rm-watch (cdr (assoc .uuid ob-julia-vterm-evaluation-watches)))
(setq ob-julia-vterm-evaluation-watches
(delete (assoc .uuid ob-julia-vterm-evaluation-watches)
ob-julia-vterm-evaluation-watches))
(ob-julia-vterm-process-evaluation-queue .session)))))))
(defvar-local ob-julia-vterm-output-suppress-state nil)
(defun ob-julia-vterm-output-filter (str)
"Remove the pasted julia code from STR."
(let ((begin (string-match "#OB-JULIA-VTERM_BEGIN" str))
(end (string-match "#OB-JULIA-VTERM_END" str))
(state ob-julia-vterm-output-suppress-state))
(if begin (setq ob-julia-vterm-output-suppress-state 'suppress))
(if end (setq ob-julia-vterm-output-suppress-state nil))
(let* ((str (replace-regexp-in-string
"#OB-JULIA-VTERM_BEGIN \\([0-9a-z]*\\)\\(.*?\n\\)*.*" "Executing... \\1\r\n" str))
(str (replace-regexp-in-string
"\\(.*?\n\\)*.*#OB-JULIA-VTERM_END" "" str)))
(if (or begin end)
str
(if state "" str)))))
(defun ob-julia-vterm-wait-for-file-change (file sec interval)
"Wait up to SEC seconds synchronously until FILE becomes non-empty.
The file is checked at INTERVAL second intervals while waiting."
(let ((c 0))
(while (and (< c (/ sec interval))
(= 0 (file-attribute-size (file-attributes file))))
(sleep-for interval)
(setq c (1+ c))))
(sleep-for 0.1))
(defun ob-julia-vterm-process-one-evaluation-sync (session evaluation)
"Execute the first EVALUATION in SESSION's queue synchronously.
Return the result."
(with-current-buffer (julia-vterm-repl-buffer session)
(while (not (eq (julia-vterm-repl-prompt-status) :julia))
(message "Waiting REPL becomes ready")
(sleep-for 0.1))
(let-alist evaluation
(julia-vterm-paste-string
(ob-julia-vterm-make-str-to-run .uuid
.params
.src-file
.out-file)
.session)
(ob-julia-vterm-wait-for-file-change .out-file 10 0.1)
(with-temp-buffer
(insert-file-contents .out-file)
(buffer-string)))))
(defun ob-julia-vterm-process-one-evaluation-async (session)
"Execute the first evaluation in SESSION's queue asynchronously.
Always return nil."
(with-current-buffer (julia-vterm-repl-buffer session)
(if (eq (julia-vterm-repl-prompt-status) :julia)
(let-alist (queue-first ob-julia-vterm-evaluation-queue)
(unless (assoc .uuid ob-julia-vterm-evaluation-watches)
(let ((desc (file-notify-add-watch .out-file
'(change)
(ob-julia-vterm-evaluation-completed-callback-func session))))
(push (cons .uuid desc) ob-julia-vterm-evaluation-watches))
(julia-vterm-paste-string
(ob-julia-vterm-make-str-to-run .uuid
.params
.src-file
.out-file)
.session)))
(if (null ob-julia-vterm-evaluation-watches)
(run-at-time 0.1 nil #'ob-julia-vterm-process-evaluation-queue session))))
nil)
(defun ob-julia-vterm-process-evaluation-queue (session)
"Process the evaluation queue for SESSION.
If ASYNC is non-nil, the next evaluation will be executed asynchronously."
(with-current-buffer (julia-vterm-repl-buffer session)
(if (and (queue-p ob-julia-vterm-evaluation-queue)
(not (queue-empty ob-julia-vterm-evaluation-queue)))
(ob-julia-vterm-process-one-evaluation-async session)
(message "Queue empty"))))
(defun ob-julia-vterm-evaluate (buf session body params)
"Evaluate a Julia code block in BUF in a julia-vterm REPL specified with SESSION.
BODY contains the source code to be evaluated, and PARAMS contains header arguments."
(let* ((uuid (org-id-uuid))
(src-file (org-babel-temp-file "julia-vterm-src-"))
(out-file (org-babel-temp-file "julia-vterm-out-"))
(result-params (cdr (assq :result-params params)))
(async (not (member 'org-babel-ref-resolve (mapcar #'cadr (backtrace-frames))))))
(with-temp-file src-file (insert (ob-julia-vterm-wrap-body session body)))
(let ((elm (org-element-context))
(src-block-begin (make-marker))
(src-block-end (make-marker)))
(set-marker src-block-begin (org-element-property :begin elm))
(set-marker src-block-end (org-element-property :end elm))
(let ((evaluation (list (cons 'uuid uuid)
(cons 'async async)
(cons 'buf buf)
(cons 'session session)
(cons 'params params)
(cons 'src-file src-file)
(cons 'out-file out-file)
(cons 'src-block-begin src-block-begin)
(cons 'src-block-end src-block-end))))
(if (not async)
(ob-julia-vterm-process-one-evaluation-sync session evaluation)
(ob-julia-vterm-add-evaluation-to-evaluation-queue session evaluation)
(ob-julia-vterm-process-evaluation-queue session)
(concat "Executing... " (substring uuid 0 8)))))))
(add-to-list 'org-src-lang-modes '("julia-vterm" . julia))
(provide 'ob-julia-vterm)
;;; ob-julia-vterm.el ends here