Skip to content

Commit

Permalink
add a comint-based backend for the Julia REPL
Browse files Browse the repository at this point in the history
  • Loading branch information
dellison committed Jan 19, 2019
1 parent d8b94c6 commit 8c94e2a
Show file tree
Hide file tree
Showing 3 changed files with 404 additions and 58 deletions.
229 changes: 229 additions & 0 deletions julia-repl-comint.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
;;; julia-repl.el --- A minor mode for a Julia REPL -*- lexical-binding:t -*-

;; Copyright (C) 2016 Tamas K. Papp
;; Author: Tamas Papp <[email protected]>
;; Keywords: languages
;; Version: 0.0.1
;; Package-Requires: ((emacs "25"))
;; URL: https://github.com/tpapp/julia-repl

;;; Usage:
;; Put the following code in your .emacs, site-load.el, or other relevant file
;; (add-to-list 'load-path "path-to-julia-repl")
;; (require 'julia-repl)

;;; License:
;; Permission is hereby granted, free of charge, to any person obtaining
;; a copy of this software and associated documentation files (the
;; "Software"), to deal in the Software without restriction, including
;; without limitation the rights to use, copy, modify, merge, publish,
;; distribute, sublicense, and/or sell copies of the Software, and to
;; permit persons to whom the Software is furnished to do so, subject to
;; the following conditions:
;;
;; The above copyright notice and this permission notice shall be
;; included in all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

;;; Commentary:
;; Run a julia REPL inside a comint buffer in Emacs.

;;; Code:

(require 'subr-x)
(require 'cl-lib)

(defgroup julia-repl-comint nil
"A Julia REPL through comint."
:group 'julia-repl)

(defcustom julia-repl-comint-hook nil
"Hook to run after starting a Julia REPL comint buffer."
:type 'hook
:group 'julia-repl-comint)

(defcustom julia-repl-comint-use-prompt-faces nil
"When non-nil, color the julia prompts according to their respective faces."
:group 'julia-repl-comint)

(defgroup julia-repl-prompt-faces nil
"Faces used for Julia prompts in julia-repl."
:group 'julia-repl-comint
:group 'faces)

(defface julia-repl-julia-prompt-face
'((t (:foreground "green")))
"Face for the julia> prompt in the REPL buffer."
:group 'julia-repl-prompt-faces)

(defface julia-repl-help-prompt-face
'((t (:foreground "yellow")))
"Face for the help?> prompt in the REPL buffer."
:group 'julia-repl-prompt-faces)

(defface julia-repl-pkg-prompt-face
'((t (:foreground "blue")))
"Face for the (env)pkg> prompt in the REPL buffer."
:group 'julia-repl-prompt-faces)

(defface julia-repl-shell-prompt-face
'((t (:foreground "red")))
"Face for the shell> prompt in the REPL buffer."
:group 'julia-repl-prompt-faces)



;; sending to the REPL

(defun julia-repl-comint--send-string (string &optional no-newline no-bracketed-paste)
"Send STRING to the Julia REPL comint buffer.
A closing newline is sent according to NO-NEWLINE:
1. NIL sends the newline,
2. 'PREFIX sends it according to ‘current-prefix-arg’,
3. otherwise no newline.
NO-BRACKETED-PASTE is ignored for sending to a comint process."
(let ((inferior-buffer (julia-repl-inferior-buffer)))
(display-buffer inferior-buffer)
(when (eq no-newline 'prefix)
(setq no-newline current-prefix-arg))
(with-current-buffer inferior-buffer
(comint-send-string inferior-buffer
(concat string (unless no-newline "\n"))))))

(defun julia-repl-comint--result-as-string (command)
(save-window-excursion
(let ((inf-buf (julia-repl-inferior-buffer))
(buf (get-buffer-create "*julia-result*"))
(cmd (concat command))
result)
(with-current-buffer buf (erase-buffer))
(with-current-buffer inf-buf
(comint-redirect-send-command-to-process cmd buf inf-buf nil)
(while (not comint-redirect-completed)
(sleep-for 0 1)))
(with-current-buffer buf
(replace-regexp-in-string "\\(^\"\\|\"$\\)" "" (string-trim (buffer-string)))))))

(defun julia-repl-comint-send-line ()
"Send the current line to the Julia REPL comint buffer.
Closed with a newline, unless used with a prefix argument.
This is the only REPL interaction function that does not use
bracketed paste. Unless you want this specifically, you should
probably be using `julia-repl-send-region-or-line'."
(interactive)
(julia-repl-comint--send-string (thing-at-point 'line t) 'prefix t)
(forward-line))

(defun julia-repl--comint-input-sender (proc string)
(save-current-buffer
(let* ((help-?-regexp "^ *\\(?:\\(?1: *?\\? *\\)\\(?2:.+\\)\\)")
(help-?-match (string-match help-?-regexp string))
(pkg-bracket-regexp "^ *\\(?:\\(?1: *?\\] *\\)\\(?2:.+\\)\\)")
(pkg-bracket-match (string-match pkg-bracket-regexp string))
(shell-sc-regexp "^ *\\(?:\\(?1: *?; *\\)\\(?2:.+\\)\\)")
(shell-sc-match (string-match shell-sc-regexp string)))
(cond (help-?-match
(comint-simple-send proc (format "@doc %s" (match-string 2 string))))
(pkg-bracket-match
(comint-simple-send proc (format "using Pkg; pkg\"%s\"" (match-string 2 string))))
(shell-sc-match
(comint-simple-send proc (format "run(`%s`)" (match-string 2 string))))
(t
(comint-simple-send proc string))))))

(defun julia-repl-comint--start-inferior (inferior-buffer-name executable-path)
(let ((buf (get-buffer-create (julia-repl--add-earmuffs inferior-buffer-name))))
(with-current-buffer buf
(apply #'make-comint-in-buffer inferior-buffer-name nil
(executable-find executable-path) nil (julia-repl--split-switches))
(julia-repl-inferior-julia-mode)
(setq comint-input-sender 'julia-repl--comint-input-sender)
buf)))


(defvar julia-repl-julia-prompt-regexp "^julia> "
"Regexp for matching the julia prompt.")

(defun julia-repl-comint-r-square-bracket ()
"Switch to the Pkg3 REPL interface at the Julia prompt or insert ']'."
(interactive)
(insert "]")
(when (looking-back (concat julia-repl-julia-prompt-regexp "\\]"))
(let ((prompt (julia-repl-comint--result-as-string "using Pkg; Pkg.REPLMode.promptf()"))
(bol (save-excursion (search-backward-regexp "^") (point)))
(pt (point)))
(when julia-repl-comint-use-prompt-faces
(put-text-property 0 (1- (length prompt)) 'face 'julia-repl-pkg-prompt-face prompt))
(put-text-property bol pt 'display prompt))))

(defun julia-repl-comint-question-mark ()
"Switch to the help interface at the Julia prompt or insert '?'."
(interactive)
(insert "?")
(when (looking-back (concat julia-repl-julia-prompt-regexp "\\?"))
(let ((prompt "help?> ")
(bol (save-excursion (search-backward-regexp "^") (point))))
(when julia-repl-comint-use-prompt-faces
(put-text-property 0 (1- (length prompt)) 'face 'julia-repl-help-prompt-face prompt))
(put-text-property bol (point) 'display prompt))))

(defun julia-repl-comint-semicolon ()
"Switch to the shell interface at the Julia prompt or insert ';'."
(interactive)
(insert ";")
(when (looking-back (concat julia-repl-julia-prompt-regexp ";"))
(let ((prompt "shell> ")
(bol (save-excursion (search-backward-regexp "^") (point))))
(when julia-repl-comint-use-prompt-faces
(put-text-property 0 (1- (length prompt)) 'face 'julia-repl-shell-prompt-face prompt))
(put-text-property bol (point) 'display prompt))))

(defun julia-repl-comint-backspace ()
(interactive)
(let ((bol (save-excursion (search-backward-regexp "^") (point))))
(cond
((looking-back julia-repl-julia-prompt-regexp)
nil)
((looking-back (concat julia-repl-julia-prompt-regexp "."))
(put-text-property bol (point) 'display nil)
(delete-backward-char 1))
(t
(delete-backward-char 1)))))


(defvar julia-repl-inferior-julia-mode-map
(let ((keymap (copy-keymap comint-mode-map)))
(define-key keymap (kbd "]") #'julia-repl-comint-r-square-bracket)
(define-key keymap (kbd "?") #'julia-repl-comint-question-mark)
(define-key keymap (kbd ";") #'julia-repl-comint-semicolon)
(define-key keymap (kbd "DEL") #'julia-repl-comint-backspace)
keymap)
"Key bindings for the Julia REPL running in the comint buffer.")


(define-derived-mode julia-repl-inferior-julia-mode comint-mode "julia-repl"
"Major mode for interacting with the Julia REPL.
\\{julia-repl-inferior-julia-mode-map}"
:group 'julia-repl-comint

(when julia-repl-comint-use-prompt-faces
(set-face-attribute 'comint-highlight-prompt nil :inherit 'julia-repl-julia-prompt-face))

(setq comint-prompt-regexp julia-repl-julia-prompt-regexp
comint-prompt-read-only nil
comint-use-prompt-regexp nil))

(provide 'julia-repl-comint)
141 changes: 141 additions & 0 deletions julia-repl-term.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
;;; julia-repl.el --- A minor mode for a Julia REPL -*- lexical-binding:t -*-

;; Copyright (C) 2016 Tamas K. Papp
;; Author: Tamas Papp <[email protected]>
;; Keywords: languages
;; Version: 0.0.1
;; Package-Requires: ((emacs "25"))
;; URL: https://github.com/tpapp/julia-repl

;;; Usage:
;; Put the following code in your .emacs, site-load.el, or other relevant file
;; (add-to-list 'load-path "path-to-julia-repl")
;; (require 'julia-repl)

;;; License:
;; Permission is hereby granted, free of charge, to any person obtaining
;; a copy of this software and associated documentation files (the
;; "Software"), to deal in the Software without restriction, including
;; without limitation the rights to use, copy, modify, merge, publish,
;; distribute, sublicense, and/or sell copies of the Software, and to
;; permit persons to whom the Software is furnished to do so, subject to
;; the following conditions:
;;
;; The above copyright notice and this permission notice shall be
;; included in all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

;;; Commentary:
;; Run a julia REPL inside a terminal in Emacs. In contrast to ESS, use
;; the Julia REPL facilities for interactive features, such readline,
;; help, debugging.

;;; Code:

(require 'term)
(require 'subr-x)
(require 'cl-lib)

(defgroup julia-repl-term nil
"A Julia REPL."
:group 'julia-repl)

(defcustom julia-repl-term-hook nil
"Hook to run after starting a Julia REPL term buffer."
:type 'hook
:group 'julia-repl-term)

(defcustom julia-repl-term-captures (list (kbd "M-x"))
"List of key sequences that are passed through (the global binding is used).
Note that this affects all buffers using the ‘ansi-term’ map."
:type '(repeat key-sequence)
:group 'julia-repl-term)


;; sending to the REPL

(defun julia-repl-term--send-string (string &optional no-newline no-bracketed-paste)
"Send STRING to the Julia REPL term buffer.
A closing newline is sent according to NO-NEWLINE:
1. NIL sends the newline,
2. 'PREFIX sends it according to ‘current-prefix-arg’,
3. otherwise no newline.
Unless NO-BRACKETED-PASTE, bracketed paste control sequences are used."
(let ((inferior-buffer (julia-repl-inferior-buffer)))
(display-buffer inferior-buffer)
(with-current-buffer inferior-buffer
(unless no-bracketed-paste ; bracketed paste start
(term-send-raw-string "\e[200~"))
(term-send-raw-string (string-trim string))
(when (eq no-newline 'prefix)
(setq no-newline current-prefix-arg))
(unless no-newline
(term-send-raw-string "\^M"))
(unless no-bracketed-paste ; bracketed paste stop
(term-send-raw-string "\e[201~")))))

(defun julia-repl-term-send-line ()
"Send the current line to the Julia REPL term buffer.
Closed with a newline, unless used with a prefix argument.
This is the only REPL interaction function that does not use
bracketed paste. Unless you want this specifically, you should
probably be using `julia-repl-send-region-or-line'."
(interactive)
(julia-repl--send-string (thing-at-point 'line t) 'prefix t)
(forward-line))


(defun julia-repl-term--start-inferior (inferior-buffer-name executable-path)
"Start a Julia REPL inferior process.
Creates INFERIOR-BUFFER-NAME (‘make-term’ surrounds it with *s),
running EXECUTABLE-PATH.
Return the inferior buffer. No setup is performed."
(message "name %s path %s" inferior-buffer-name executable-path)
(apply #'make-term inferior-buffer-name executable-path nil
(julia-repl--split-switches)))

(defun julia-repl-term--setup-captures ()
"Set up captured keys which are captured from ‘term’.
Note that this affects ‘term’ globally."
(mapc (lambda (k)
(define-key term-raw-map k (global-key-binding k)))
julia-repl-term-captures))

(defun julia-repl-term--setup-term (inferior-buffer)
"Set up customizations for term mode in INFERIOR-BUFFER.
Note that not all effects are buffer local."
(with-current-buffer inferior-buffer
(term-char-mode)
(term-set-escape-char ?\C-x) ; useful for switching windows
(setq-local term-prompt-regexp "^(julia|shell|help\\?|(\\d+\\|debug ))>")
(setq-local term-suppress-hard-newline t) ; reflow text
(setq-local term-scroll-show-maximum-output t)
;; do I need this?
(setq-local term-scroll-to-bottom-on-output t)
))

(defun julia-repl-comint--live-buffer ()
""
(if-let ((inferior-buffer
(get-buffer (julia-repl--add-earmuffs
(julia-repl--inferior-buffer-name))))))
(or (comint-check-proc )))

(provide 'julia-repl-term)
Loading

0 comments on commit 8c94e2a

Please sign in to comment.