|
|
- ;;; indium-repl.el --- JavaScript REPL connected to a browser tab -*- lexical-binding: t; -*-
-
- ;; Copyright (C) 2016-2018 Nicolas Petton
-
- ;; Author: Nicolas Petton <nicolas@petton.fr>
- ;; Keywords: convenience, tools, javascript
-
- ;; 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 <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
-
- ;; REPL interactions with a browser connection.
-
- ;;; Code:
-
- (require 'indium-render)
- (require 'indium-faces)
- (require 'indium-client)
- (require 'indium-debugger)
-
- (require 'company)
- (require 'easymenu)
- (require 'map)
- (require 'js)
-
- (require 'subr-x)
- (require 'ansi-color)
-
- (declare-function indium-inspector-inspect "indium-inspector.el")
- (declare-function indium-maybe-quit "indium-interaction.el")
-
- (defgroup indium-repl nil
- "Interaction with the REPL."
- :prefix "indium-repl-"
- :group 'indium)
-
- (defvar indium-repl-evaluate-hook nil
- "Hook run when input is evaluated in the repl.")
-
- (defvar indium-repl-switch-from-buffer nil
- "The buffer from which repl was activated last time.")
-
- (defvar indium-repl-history nil "History of the REPL inputs.")
- (make-variable-buffer-local 'indium-repl-history)
- (defvar indium-repl-history-position -1 "Position in the REPL history.")
- (make-variable-buffer-local 'indium-repl-history-position)
-
- (defvar-local indium-repl-input-start-marker nil)
- (defvar-local indium-repl-output-start-marker nil)
- (defvar-local indium-repl-output-end-marker nil)
-
- (defmacro indium-save-marker (marker &rest body)
- "Save MARKER and execute BODY."
- (declare (indent 1) (debug t))
- (let ((pos (make-symbol "pos")))
- `(let ((,pos (marker-position ,marker)))
- (prog1 (progn . ,body)
- (set-marker ,marker ,pos)))))
-
- (defun indium-repl-setup ()
- "Create and switch to the REPL buffer."
- (switch-to-buffer (indium-repl-get-buffer-create)))
-
- (defun indium-repl-get-buffer-create ()
- "Return a new REPL buffer."
- (let* ((buf (get-buffer-create (indium-repl-buffer-name))))
- (indium-repl-setup-buffer buf)
- buf))
-
- (defun indium-repl-get-buffer ()
- "Return the REPL buffer, or nil."
- (get-buffer (indium-repl-buffer-name)))
-
- (defun indium-repl-buffer-name ()
- "Return the name of the REPL buffer."
- "*JS REPL*")
-
- (defun indium-repl-setup-buffer (buffer)
- "Setup the REPL BUFFER."
- (with-current-buffer buffer
- (unless (eq major-mode 'indium-repl-mode)
- (indium-repl-mode)
- (indium-repl-setup-markers)
- (indium-repl-mark-output-start)
- (indium-repl-insert-prompt)
- (indium-repl-mark-input-start)
- (indium-repl-emit-console-message
- `((result . ,(indium-remote-object-create
- :description (indium-repl--welcome-message))))))))
-
- (defun indium-repl--welcome-message ()
- "Return the welcome message displayed in new REPL buffers."
- (substitute-command-keys
- "/* Welcome to Indium!
-
- Getting started:
-
- - Press <\\[indium-repl-return]> on links to open an inspector
- - Press <\\[indium-repl-previous-input]> and <\\[indium-repl-next-input]> to navigate in the history
- - Use <\\[indium-scratch]> to open a scratch buffer for JS evaluation
- - Press <\\[describe-mode]> to see a list of available keybindings
- - Press <\\[indium-repl-clear-output]> to clear the output
-
- To disconnect from the JavaScript process, press <\\[indium-quit]>.
- Doing this will also close all inspectors and debugger buffers
- connected to the process.
-
- */"))
-
- (defun indium-repl-setup-markers ()
- "Setup the initial markers for the current REPL buffer."
- (dolist (marker '(indium-repl-output-start-marker
- indium-repl-output-end-marker
- indium-repl-input-start-marker))
- (set marker (make-marker))
- (set-marker (symbol-value marker) (point))))
-
- (defun indium-repl-mark-output-start ()
- "Mark the output start."
- (set-marker indium-repl-output-start-marker (point))
- (set-marker indium-repl-output-end-marker (point)))
-
- (defun indium-repl-mark-input-start ()
- "Mark the input start."
- (set-marker indium-repl-input-start-marker (point)))
-
- (defun indium-repl-insert-prompt ()
- "Insert the prompt in the REPL buffer."
- (goto-char indium-repl-input-start-marker)
- (indium-save-marker indium-repl-output-start-marker
- (indium-save-marker indium-repl-output-end-marker
- (unless (bolp)
- (insert-before-markers "\n"))
- (insert-before-markers "js> ")
- (let ((beg (save-excursion
- (beginning-of-line)
- (point)))
- (end (point)))
- (set-text-properties beg end
- '(font-lock-face indium-repl-prompt-face
- read-only t
- intangible t
- field indium-repl-prompt
- rear-nonsticky (read-only font-lock-face intangible field)))))))
-
- (defun indium-repl-return ()
- "Depending on the position of point, jump to a reference of evaluate the input."
- (interactive)
- (cond
- ((get-text-property (point) 'indium-reference) (indium-follow-link))
- ((get-text-property (point) 'indium-action) (indium-perform-action))
- ((indium-repl--in-input-area-p) (indium-repl-evaluate (indium-repl--input-content)))
- (t (error "No input or action at point"))))
-
- (defun indium-repl-inspect ()
- "Inspect the result of the evaluation of the input at point."
- (interactive)
- (indium-client-evaluate (indium-repl--input-content)
- indium-debugger-current-frame
- (lambda (result)
- (indium-inspector-inspect result))))
-
- (defun indium-repl--input-content ()
- "Return the content of the current input."
- (buffer-substring-no-properties indium-repl-input-start-marker (point-max)))
-
- (defun indium-repl--in-input-area-p ()
- "Return t if in input area."
- (<= indium-repl-input-start-marker (point)))
-
- (defun indium-repl-evaluate (string)
- "Evaluate STRING in the browser tab and emit the output."
- (push string indium-repl-history)
- (indium-client-evaluate string indium-debugger-current-frame #'indium-repl-emit-value)
- ;; move the output markers so that output is put after the current prompt
- (save-excursion
- (goto-char (point-max))
- (set-marker indium-repl-output-start-marker (point))
- (set-marker indium-repl-output-end-marker (point))))
-
- (defun indium-repl-emit-value (value)
- "Emit a string representation of the remote object VALUE."
- (with-current-buffer (indium-repl-get-buffer)
- (save-excursion
- (goto-char (point-max))
- (insert-before-markers "\n")
- (set-marker indium-repl-output-start-marker (point))
- (indium-render-remote-object value)
- (insert "\n")
- (indium-repl-mark-input-start)
- (set-marker indium-repl-output-end-marker (point)))
- (indium-repl-insert-prompt)
- (run-hooks 'indium-repl-evaluate-hook)))
-
- (defun indium-repl-emit-console-message (message &optional error)
- "Emit a console MESSAGE.
- When ERROR is non-nil, display MESSAGE as an error.
-
- MESSAGE is a map (alist/hash-table) with the following keys:
- type type of message
- url url of the message origin
- line line number in the resource that generated this message
- result object to be logged
-
- MESSAGE must contain `result'. Other fields are
- optional."
- (with-current-buffer (indium-repl-get-buffer)
- (let-alist message
- (when (string= .type 'error)
- (setq error t)))
- (save-excursion
- (goto-char indium-repl-output-end-marker)
- (set-marker indium-repl-output-start-marker (point))
- (insert "\n")
- (when error
- (indium-repl--emit-logging-error))
- (indium-repl--emit-message message)
- (set-marker indium-repl-output-end-marker (point))
- (unless (eolp)
- (insert "\n")))))
-
- (defun indium-repl--emit-message (message)
- "Emit the value of console MESSAGE."
- (let-alist message
- (indium-render-remote-object .result)
- (indium-repl--emit-message-url-line .url .line)))
-
- (defun indium-repl--emit-logging-error ()
- "Emit a red \"Error\" label."
- (insert
- (ansi-color-apply
- (propertize "Error:"
- 'font-lock-face 'indium-repl-error-face
- 'rear-nonsticky '(font-lock-face)))
- " "))
-
- (defun indium-repl--emit-message-url-line (url line)
- "Emit the URL and LINE for a message."
- (unless (seq-empty-p url)
- (insert "\nFrom "
- (propertize (if line
- (format "%s:%s" url line)
- url)
- 'font-lock-face 'indium-link-face
- 'indium-action (lambda ()
- (if (file-regular-p url)
- (find-file url)
- (browse-url url)))
- 'rear-nonsticky '(font-lock-face indium-action)))))
-
- (defun indium-repl-next-input ()
- "Insert the content of the next input in the history."
- (interactive)
- (indium-repl--history-replace 'forward))
-
- (defun indium-repl-previous-input ()
- "Insert the content of the previous input in the history."
- (interactive)
- (indium-repl--history-replace 'backward))
-
- (defun indium-repl--history-replace (direction)
- "Replace the current input with one the next one in DIRECTION.
- DIRECTION is `forward' or `backard' (in the history list)."
- (let* ((history (seq-reverse indium-repl-history))
- (search-in-progress (or (eq last-command 'indium-repl-previous-input)
- (eq last-command 'indium-repl-next-input)))
- (step (pcase direction
- (`forward 1)
- (`backward -1)))
- (pos (or (and search-in-progress (+ indium-repl-history-position step))
- (1- (seq-length history)))))
- (unless (>= pos 0)
- (user-error "Beginning of history"))
- (unless (< pos (seq-length history))
- (user-error "End of history"))
- (setq indium-repl-history-position pos)
- (indium-repl--replace-input (seq-elt history pos))))
-
- (defun indium-repl--replace-input (input)
- "Replace the current input with INPUT."
- (goto-char (point-max))
- (delete-region indium-repl-input-start-marker (point))
- (insert input))
-
- (defun indium-repl-clear-output ()
- "Clear all output contents of the current buffer."
- (interactive)
- (let ((inhibit-read-only t))
- (save-excursion
- (goto-char (point-min))
- (delete-region (point) indium-repl-output-end-marker))))
-
- (defun indium-repl-pop-buffer ()
- "Switch to the buffer from which repl was opened buffer if any."
- (interactive)
- (when indium-repl-switch-from-buffer
- (pop-to-buffer indium-repl-switch-from-buffer t)))
-
- (defun company-indium-repl (command &optional arg &rest _args)
- "Indium REPL backend for company-mode.
- See `company-backends' for more info about COMMAND and ARG."
- (interactive (list 'interactive))
- (cl-case command
- (interactive (company-begin-backend 'company-indium-repl))
- (prefix (indium-repl-company-prefix))
- (ignore-case t)
- (sorted t)
- (candidates (cons :async
- (lambda (callback)
- (indium-repl-get-completions arg callback))))))
-
- (defun indium-repl-get-completions (prefix callback)
- "Get the completion list matching PREFIX.
- Evaluate CALLBACK with the completion candidates."
- (let* ((input (buffer-substring-no-properties
- (let ((bol (point-at-bol))
- (prev-delimiter (1+ (save-excursion
- (re-search-backward "[([:space:]]" nil t)))))
- (if prev-delimiter
- (max bol prev-delimiter)
- bol))
- (point)))
- (expression (if (string-match-p "\\." input)
- (replace-regexp-in-string "\\.[^\\.]*$" "" input)
- "this")))
- (indium-client-get-completion
- expression
- indium-debugger-current-frame
- (lambda (candidates)
- (funcall callback
- (seq-filter (lambda (candidate)
- (string-prefix-p prefix candidate))
- candidates))))))
-
- (defun indium-repl--complete-or-indent ()
- "Complete or indent at point."
- (interactive)
- (if (company-manual-begin)
- (company-complete-common)
- (indent-according-to-mode)))
-
- (defun indium-repl-company-prefix ()
- "Prefix for company."
- (and (or (eq major-mode 'indium-repl-mode)
- (bound-and-true-p indium-interaction-mode))
- (or (company-grab-symbol-cons "\\." 1)
- 'stop)))
-
- (defvar indium-repl-mode-hook nil
- "Hook executed when entering `indium-repl-mode'.")
-
- (defvar indium-repl-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map [return] #'indium-repl-return)
- (define-key map "\C-m" #'indium-repl-return)
- (define-key map (kbd "TAB") #'indium-repl--complete-or-indent)
- (define-key map [mouse-1] #'indium-follow-link)
- (define-key map (kbd "C-<return>") #'newline)
- (define-key map (kbd "C-c M-i") #'indium-repl-inspect)
- (define-key map (kbd "C-c C-o") #'indium-repl-clear-output)
- (define-key map (kbd "C-c C-z") #'indium-repl-pop-buffer)
- (define-key map (kbd "C-c C-q") #'indium-maybe-quit)
- (define-key map (kbd "M-p") #'indium-repl-previous-input)
- (define-key map (kbd "M-n") #'indium-repl-next-input)
- (define-key map (kbd "C-<up>") #'indium-repl-previous-input)
- (define-key map (kbd "C-<down>") #'indium-repl-next-input)
- (easy-menu-define indium-repl-mode-menu map
- "Menu for Indium REPL"
- '("Indium REPL"
- ["Clear output" indium-repl-clear-output]
- ["Inspect" indium-repl-inspect]
- "--"
- ["Switch to source buffer" indium-repl-pop-buffer]
- "--"
- ["Quit" indium-maybe-quit]))
- map))
-
- (define-derived-mode indium-repl-mode fundamental-mode "JS-REPL"
- "Major mode for indium REPL interactions.
-
- \\{indium-repl-mode-map}"
- (font-lock-add-keywords nil '(indium-repl--fontify-output))
- (setq-local company-backends '(company-indium-repl))
- (company-mode 1))
-
- (defun indium-repl--fontify-output (&rest _)
- "Fontify JS code output."
- (let* ((start indium-repl-input-start-marker)
- (end (point-max))
- (repl-buffer (current-buffer))
- (string (buffer-substring-no-properties start end)))
- (with-current-buffer
- (get-buffer-create " indium-fontification ")
- (let ((inhibit-modification-hooks nil))
- (js-mode)
- (erase-buffer)
- (insert string " ")
- (font-lock-ensure)
- (let ((pos (point-min)) next)
- (while (setq next (next-property-change pos))
- ;; Handle additional properties from font-lock, so as to
- ;; preserve, e.g., composition.
- (dolist (prop (cons 'face font-lock-extra-managed-props))
- (let ((new-prop (get-text-property pos prop)))
- (put-text-property
- (+ start (1- pos)) (1- (+ start next)) prop new-prop
- repl-buffer)))
- (setq pos next)))))))
-
- (add-hook 'indium-client-connected-hook #'indium-repl-setup)
- (add-hook 'indium-client-log-hook #'indium-repl-emit-console-message)
-
- (provide 'indium-repl)
- ;;; indium-repl.el ends here
|