Browse Source

initial commit

workspaces
Nicolas Petton 5 years ago
commit
d110bb71ea
6 changed files with 1262 additions and 0 deletions
  1. +243
    -0
      jade-debugger.el
  2. +68
    -0
      jade-faces.el
  3. +145
    -0
      jade-inspector.el
  4. +103
    -0
      jade-render.el
  5. +282
    -0
      jade-repl.el
  6. +421
    -0
      jade.el

+ 243
- 0
jade-debugger.el View File

@ -0,0 +1,243 @@
;;; jade-debugger.el --- Jade debugger -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: tools
;; 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:
;;
;;; Code:
(require 'seq)
(require 'map)
(require 'jade-inspector)
(require 'jade-render)
(defgroup jade-debugger nil
"JavaScript debugger"
:prefix "jade-debugger-"
:group 'jade)
(defcustom jade-debugger-major-mode
#'js-mode
"Major mode used in debugger buffers."
:group 'jade-debugger)
(defvar jade-debugger-buffer nil "Buffer used for debugging JavaScript sources.")
(defvar jade-debugger-frames nil "List of frames of the current debugger context.")
(make-variable-buffer-local 'jade-debugger-frames)
(defconst jade-debugger-fringe-arrow-string
#("." 0 1 (display (left-fringe right-triangle)))
"Used as an overlay's before-string prop to place a fringe arrow.")
(declare 'jade-backend-debugger-get-script-source)
(defun jade-debugger-paused (frames)
(setq jade-debugger-frames frames)
(jade-backend-get-script-source (jade-debugger-top-frame)
(lambda (source)
(jade-debugger-switch-to-frame
(jade-debugger-top-frame)
(map-nested-elt source '(result scriptSource))))))
(defun jade-debugger-resumed (&rest _args)
(let ((buf (jade-debugger-get-buffer)))
(when buf
(set-marker overlay-arrow-position nil (current-buffer))
(remove-overlays))))
(defun jade-debugger-switch-to-frame (frame source)
(jade-debugger-debug-frame frame source)
(switch-to-buffer (jade-debugger-get-buffer))
(jade-debugger-locals-maybe-refresh))
(defun jade-debugger-debug-frame (frame source)
(let* ((location (map-elt frame 'location))
(line (map-elt location 'lineNumber))
(column (map-elt location 'columnNumber))
(inhibit-read-only t))
(with-current-buffer (jade-debugger-get-buffer-create)
(unless (string= (buffer-substring-no-properties (point-min) (point-max))
source)
(erase-buffer)
(insert source))
(goto-line (1+ line))
(forward-char column)
(jade-debugger-setup-overlay-arrow)
(jade-debugger-highlight-node frame))))
(defun jade-debugger-setup-overlay-arrow ()
(let ((pos (line-beginning-position)))
(setq overlay-arrow-string "=>")
(setq overlay-arrow-position (make-marker))
(set-marker overlay-arrow-position pos (current-buffer))))
(defun jade-debugger-highlight-node (frame)
(let ((beg (point))
(end (line-end-position)))
(remove-overlays)
(overlay-put (make-overlay beg end)
'face 'jade-highlight-face)))
(defun jade-debugger-top-frame ()
"Return the top frame of the current debugging context."
(car jade-debugger-frames))
(defun jade-debugger-step-into ()
(interactive)
(jade-backend-step-into))
(defun jade-debugger-step-over ()
(interactive)
(jade-backend-step-over))
(defun jade-debugger-step-out ()
(interactive)
(jade-backend-step-out))
(defun jade-debugger-resume ()
(interactive)
(jade-backend-resume #'jade-debugger-resumed)
(kill-buffer (jade-debugger-locals-get-buffer))
(kill-buffer (jade-debugger-get-buffer)))
(defun jade-debugger-here ()
(interactive)
;; TODO
(jade-backend-continue-to-location '()))
(defun jade-debugger-get-buffer-create ()
"Create a debugger buffer unless one exists, and return it."
(let ((buf (jade-debugger-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-debugger-buffer-name)))
(jade-debugger-setup-buffer buf jade-connection))
buf))
(defun jade-debugger-buffer-name ()
(concat "*JS Debugger " (map-elt jade-connection 'url) "*"))
(defun jade-debugger-get-buffer ()
(get-buffer (jade-debugger-buffer-name)))
(defun jade-debugger-setup-buffer (buffer connection)
(with-current-buffer buffer
(funcall jade-debugger-major-mode)
(setq-local jade-connection connection)
(jade-debugger-mode)
(read-only-mode)))
(defvar jade-debugger-mode-map
(let ((map (make-sparse-keymap)))
(define-key map " " #'jade-debugger-step-over)
(define-key map (kbd "i") #'jade-debugger-step-into)
(define-key map (kbd "o") #'jade-debugger-step-out)
(define-key map (kbd "c") #'jade-debugger-resume)
(define-key map (kbd "l") #'jade-debugger-locals)
(define-key map (kbd "q") #'jade-debugger-resume)
(define-key map (kbd "h") #'jade-debugger-here)
map))
(define-minor-mode jade-debugger-mode "Minor mode for debugging JS scripts."
:group 'jade
:lighter " JS debug"
:keymap jade-debugger-mode-map)
;;; Locals
(declare 'jade-backend-get-properties)
(defun jade-debugger-locals (&optional no-pop)
"Inspect the local variables in the current stack frame's scope.
Unless NO-POP is non-nil, pop the locals buffer."
(interactive)
(let* ((buf (jade-debugger-locals-get-buffer-create))
(inhibit-read-only t))
(with-current-buffer buf
(erase-buffer)))
(seq-do (lambda (scope)
(jade-backend-get-properties
(map-nested-elt scope '(object objectid))
(lambda (properties)
(jade-debugger-locals-render-properties properties scope no-pop))))
;; do not inspect the window object
(seq-remove (lambda (scope)
(string= (map-elt scope 'type) "global"))
(map-elt (jade-debugger-top-frame) 'scope-chain))))
(defun jade-debugger-locals-maybe-refresh ()
"When a local inspector is open, refresh it."
(interactive)
(let ((buf (jade-debugger-locals-get-buffer)))
(when buf
(jade-debugger-locals t))))
(defun jade-debugger-locals-render-properties (properties scope &optional no-pop)
(let* ((buf (jade-debugger-locals-get-buffer-create))
(inhibit-read-only t)
(name (map-elt scope 'name))
(type (map-elt scope 'type))
(description (if (string= name "undefined")
type
name)))
(with-current-buffer buf
(save-excursion
(goto-char (point-max))
(jade-render-keyword description)
(insert "\n\n")
(jade-render-properties properties scope)
(insert "\n")))
(unless no-pop
(pop-to-buffer buf))))
(defun jade-debugger-locals-get-buffer ()
(get-buffer (jade-debugger-locals-buffer-name)))
(defun jade-debugger-locals-buffer-name ()
(concat "*JS Debugger Locals " (map-elt jade-connection 'url) "*"))
(defun jade-debugger-locals-get-buffer-create ()
"Create a locals buffer unless one exists, and return it."
(let ((buf (jade-debugger-locals-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-debugger-locals-buffer-name)))
(jade-debugger-locals-setup-buffer buf jade-connection))
buf))
(defun jade-debugger-locals-setup-buffer (buffer connection)
(with-current-buffer buffer
(jade-debugger-locals-mode)
(read-only-mode)
(setq-local jade-connection connection)))
(defvar jade-debugger-locals-mode-map
(let ((map (copy-keymap jade-inspector-mode-map)))
(define-key map "g" #'jade-debugger-locals-maybe-refresh)
(define-key map "l" nil)
map))
(define-derived-mode jade-debugger-locals-mode jade-inspector-mode "Locals"
"Major mode for inspecting local variables in a scope-chain.
\\{jade-debugger-locals-mode-map}")
(provide 'jade-debugger)
;;; jade-debugger.el ends here

+ 68
- 0
jade-faces.el View File

@ -0,0 +1,68 @@
;;; jade-faces.el --- Faces for jade -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: faces
;; 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:
;;
;;; Code:
(defgroup jade-faces nil
"Faces used in jade."
:prefix "jade-"
:group 'jade)
(defface jade-keyword-face
'((t (:inherit font-lock-keyword-face)))
"Face for the keywords."
:group 'jade-faces)
(defface jade-header-face
'((t (:inherit header-line)))
"Face use in headers."
:group 'jade-faces)
(defface jade-repl-prompt-face
'((t (:inherit jade-keyword-face)))
"Face for the prompt in the REPL buffer."
:group 'jade-faces)
(defface jade-repl-stdout-face
'((t (:inherit font-lock-string-face)))
"Face for the STDOUT output in the REPL buffer."
:group 'jade-faces)
(defface jade-repl-error-face
'((t (:inherit font-lock-warning-face)))
"Face for the error output in the REPL buffer."
:group 'jade-faces)
(defface jade-link-face
'((t (:inherit link)))
"Face used when outputting objects to which we can navigate to."
:group 'jade-faces)
(defface jade-highlight-face
'((t (:inherit highlight)))
"Face used when highlighting regions of a buffer."
:group 'jade-faces)
(provide 'jade-faces)
;;; jade-faces.el ends here

+ 145
- 0
jade-inspector.el View File

@ -0,0 +1,145 @@
;;; jade-inspector.el --- Inspector for JavaScript objects -*- lexical-binding: t; -*-
;; Copyright (C) 2016 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:
;;
;;; Code:
(require 'seq)
(require 'map)
(require 'jade-render)
(require 'jade-faces)
(defvar jade-inspector-history nil)
(declare jade-backend-get-properties)
(defun jade-inspector-inspect (reference)
(jade-backend-get-properties (map-elt reference 'objectid)
(lambda (properties)
(jade-inspector-push-to-history reference)
(jade-inspector-render-properties properties reference))))
(defun jade-inspector-render-properties (properties reference)
(let ((buf (jade-inspector-get-buffer-create))
(inhibit-read-only t))
(with-current-buffer buf
(save-excursion
(erase-buffer)
(jade-render-keyword (jade-description-string reference t))
(insert "\n\n")
(jade-render-properties properties reference)))
(pop-to-buffer buf)))
(defun jade-inspector-keybinding (command)
(key-description (car (where-is-internal command))))
(defun jade-inspector-pop ()
(interactive)
(if (cdr jade-inspector-history)
(progn
(pop jade-inspector-history)
(funcall #'jade-inspector-inspect (car jade-inspector-history)))
(message "No previous object to inspect")))
(defun jade-inspector-goto-reference (direction)
(let* ((delta (pcase direction
(`next 1)
(`previous -1)))
(reference (save-excursion
(forward-line delta)
(when (eq direction 'previous)
(end-of-line))
(while (and (not (eobp))
(not (get-text-property (point) 'jade-reference)))
(forward-char delta))
(when (get-text-property (point) 'jade-reference)
(point)))))
(when reference
(goto-char reference)
;; go to the first char of the reference
(while (get-text-property (point) 'jade-reference)
(backward-char 1))
(forward-char 1))))
(defun jade-inspector-next-reference ()
(interactive)
(jade-inspector-goto-reference 'next))
(defun jade-inspector-previous-reference ()
(interactive)
(jade-inspector-goto-reference 'previous))
(defun jade-inspector-refresh ()
(interactive)
(when jade-inspector-history
(funcall #'jade-inspector-inspect (car jade-inspector-history))))
(defun jade-inspector-push-to-history (reference)
(unless (string= (map-elt reference 'objectid)
(map-elt (car jade-inspector-history) 'objectid))
(push reference jade-inspector-history)))
(defun jade-inspector-get-buffer ()
(get-buffer (jade-inspector-buffer-name)))
(defun jade-inspector-get-buffer-create ()
"Create an inspector buffer for the current ws unless one
exists, and return it."
(let ((buf (jade-inspector-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-inspector-buffer-name)))
(jade-inspector-setup-buffer buf jade-connection))
buf))
(defun jade-inspector-setup-buffer (buffer connection)
(with-current-buffer buffer
(jade-inspector-mode)
(setq-local jade-connection connection)))
(defun jade-inspector-buffer-name ()
(concat "*JS Inspector " (map-elt jade-connection 'url) "*"))
(defvar jade-inspector-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [return] #'jade-follow-link)
(define-key map "\C-m" #'jade-follow-link)
(define-key map [mouse-1] #'jade-follow-link)
(define-key map "l" #'jade-inspector-pop)
(define-key map "g" #'jade-inspector-refresh)
(define-key map "n" #'jade-inspector-next-reference)
(define-key map "p" #'jade-inspector-previous-reference)
(define-key map [tab] #'jade-inspector-next-reference)
(define-key map [backtab] #'jade-inspector-previous-reference)
map))
(define-derived-mode jade-inspector-mode special-mode "Inspector"
"Major mode for inspecting JavaScript objects.
\\{jade-inspector-mode-map}"
(setq buffer-read-only t)
(font-lock-ensure)
(setq-local electric-indent-chars nil)
(setq-local truncate-lines t))
(provide 'jade-inspector)
;;; jade-inspector.el ends here

+ 103
- 0
jade-render.el View File

@ -0,0 +1,103 @@
;;; jade-render.el --- Helper functions to display JS objects in buffers -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords:
;; 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:
;;
;;; Code:
(require 'jade-faces)
(declare 'jade-backend-object-reference-p)
(defun jade-render-value (value &optional error)
(if (jade-backend-object-reference-p value)
(jade-render-object-link value error)
(jade-render-description value error)))
(defun jade-render-description (value &optional error)
(let ((description (jade-description-string value))
(face (when error 'jade-repl-error-face)))
(insert
(propertize description
'font-lock-face (or face 'jade-repl-stdout-face)
'rear-nonsticky '(font-lock-face)))))
(defun jade-render-keyword (string)
(insert
(propertize string
'font-lock-face 'jade-keyword-face
'rear-nonsticky '(font-lock-face))))
(defun jade-description-string (value &optional full)
"Return a short string describing VALUE.
When FULL is non-nil, do not strip long descriptions and function
definitions."
(let ((description (map-elt value 'description))
(type (map-elt value 'type)))
;; Showing the source code of the function is too verbose
(if (and (not full) (eq type 'function))
"function"
description)))
(defun jade-render-object-link (value error)
(let* ((description (jade-description-string value))
(preview (map-elt value 'preview))
(beg (point))
(end (progn
(insert description)
(point)))
(face (if error
'jade-repl-error-face
'jade-link-face)))
(set-text-properties beg end
`(font-lock-face ,face
mouse-face highlight
jade-reference ,value))
(when preview
(insert preview))))
(defun jade-render-properties (properties value)
(let ((beg (point)))
(seq-map #'jade-render-property properties)
;; (align-regexp beg (point) "\\(:\\s-*\\)" nil 2)
))
(defun jade-render-property (property)
(insert " " (map-elt property 'name) ": ")
(jade-render-value (map-elt property 'value))
(insert "\n"))
(declare #'jade-inspector-inspect)
(defun jade-follow-link ()
"Follow the link at point."
(interactive)
(let ((reference (get-text-property (point) 'jade-reference))
(action (get-text-property (point) 'jade-action)))
(cond
(reference (jade-inspector-inspect reference))
(action (funcall action)))))
(provide 'jade-render)
;;; jade-render.el ends here

+ 282
- 0
jade-repl.el View File

@ -0,0 +1,282 @@
;;; jade-repl.el --- JavaScript REPL connected to a browser tab -*- lexical-binding: t; -*-
;; Copyright (C) 2016 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:
;;
;;; Code:
(require 'company)
(require 'jade-render)
(require 'jade-faces)
(require 'map)
(require 'js)
(defgroup jade-repl nil
"Interaction with the REPL."
:prefix "jade-repl-"
:group 'jade)
(defvar jade-repl-evaluate-hook nil
"Hook run when input is evaluated in the repl.")
(defvar jade-repl-history nil "History of the REPL inputs.")
(defvar jade-repl-history-position -1 "Position in the REPL history.")
(defvar-local jade-repl-input-start-marker nil)
(defvar-local jade-repl-prompt-start-marker nil)
(defvar-local jade-repl-output-start-marker nil)
(defvar-local jade-repl-output-end-marker nil)
(defmacro jade-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 jade-repl-get-buffer-create (connection)
"Create a REPL buffer unless one exists, and return it."
(let* ((ws (map-elt connection 'ws))
(url (map-elt connection 'url))
(buf (get-buffer (jade-repl-buffer-name url))))
(unless buf
(setq buf (get-buffer-create (jade-repl-buffer-name url)))
(jade-repl-setup-buffer buf connection))
buf))
(defun jade-repl-get-buffer ()
"Return the REPL buffer, or nil."
(get-buffer (jade-repl-buffer-name)))
(defun jade-repl-buffer-name (&optional url)
(concat "*JS REPL " (or url (map-elt jade-connection 'url)) "*"))
(defun jade-repl-setup-buffer (buffer connection)
"Setup the REPL BUFFER."
(with-current-buffer buffer
(jade-repl-mode)
(setq-local jade-connection connection)
;; (cursor-intangible-mode)
(jade-repl-setup-markers)
(jade-repl-mark-output-start)
(jade-repl-mark-input-start)
(jade-repl-insert-prompt))
(jade-repl-emit-console-message
(format "Welcome to Jade!\nConnected to %s\n"
(map-elt jade-connection 'url))))
(defun jade-repl-setup-markers ()
(dolist (marker '(jade-repl-prompt-start-marker
jade-repl-output-start-marker
jade-repl-output-end-marker
jade-repl-input-start-marker))
(set marker (make-marker))
(set-marker (symbol-value marker) (point))))
(defun jade-repl-mark-output-start ()
"Mark the output start."
(set-marker jade-repl-output-start-marker (point))
(set-marker jade-repl-output-end-marker (point)))
(defun jade-repl-mark-input-start ()
"Mark the input start."
(set-marker jade-repl-input-start-marker (point)))
(defun jade-repl-insert-prompt ()
"Insert the prompt in the REPL buffer."
(goto-char jade-repl-input-start-marker)
(jade-save-marker jade-repl-output-start-marker
(jade-save-marker jade-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 jade-repl-prompt-face
read-only t
intangible t
field jade-repl-prompt
rear-nonsticky (read-only font-lock-face intangible field)))
(set-marker jade-repl-prompt-start-marker beg)))))
(defun jade-repl-return ()
(interactive)
(if (get-text-property (point) 'jade-reference)
(jade-follow-link)
(progn
(unless (jade-repl--in-input-area-p)
(error "No input at point"))
(jade-repl-evaluate (buffer-substring-no-properties jade-repl-input-start-marker (point-max))))))
(declare-function #'jade-backend-evaluate "jade")
(defun jade-repl-evaluate (string)
"Evaluate STRING in the browser tab and emit the output."
(push string jade-repl-history)
(jade-backend-evaluate string #'jade-repl-emit-value)
;; move the output markers so that output is put after the current prompt
(save-excursion
(goto-char (point-max))
(set-marker jade-repl-output-start-marker (point))
(set-marker jade-repl-output-end-marker (point))))
(defun jade-repl-emit-value (value error)
(with-current-buffer (jade-repl-get-buffer)
(save-excursion
(end-of-buffer)
(insert-before-markers "\n")
(set-marker jade-repl-output-start-marker (point))
(jade-render-value value error)
(insert "\n")
(set-marker jade-repl-input-start-marker (point))
(set-marker jade-repl-output-end-marker (point)))
(jade-repl-insert-prompt)
(run-hooks 'jade-repl-evaluate-hook)))
(defun jade-repl-emit-console-message (string &optional level)
(with-current-buffer (jade-repl-get-buffer)
(save-excursion
(let* ((error (string= level "error"))
(face (when error 'jade-repl-error-face))
(message (if level
(concat level ": " string)
string)))
(goto-char jade-repl-output-end-marker)
(insert "\n")
(set-marker jade-repl-output-start-marker (point))
(insert
(ansi-color-apply
(propertize message
'font-lock-face (or face 'jade-repl-stdout-face)
'rear-nonsticky '(font-lock-face))))
(set-marker jade-repl-output-end-marker (point))
(unless (eolp)
(insert "\n"))
;; when we get an error, also display it in the echo area for
;; convenience
(when error (message string))))))
(defun jade-repl-next-input ()
(interactive)
(jade-repl-history-replace 'forward))
(defun jade-repl-previous-input ()
(interactive)
(jade-repl-history-replace 'backward))
(defun jade-repl-history-replace (direction)
(let* ((history (seq-reverse jade-repl-history))
(search-in-progress (or (eq last-command 'jade-repl-previous-input)
(eq last-command 'jade-repl-next-input)))
(step (pcase direction
(`forward 1)
(`backward -1)))
(pos (or (and search-in-progress (+ jade-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 jade-repl-history-position pos)
(jade-repl-replace-input (seq-elt history pos))))
(defun jade-repl-replace-input (input)
(goto-char (point-max))
(delete-region jade-repl-input-start-marker (point))
(insert input))
(defun jade-repl-clear-output ()
(interactive)
(let ((inhibit-read-only t))
(save-excursion
(beginning-of-buffer)
(delete-region (point) jade-repl-prompt-start-marker))))
(defun jade-repl--in-input-area-p ()
"Return t if in input area."
(<= jade-repl-input-start-marker (point)))
(defun jade-repl-newline ()
(interactive)
(insert "\n"))
(defun company-jade-repl (command &optional arg &rest _args)
"Jade 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-jade-repl))
(prefix (jade-repl-company-prefix))
(ignore-case t)
(sorted t)
(candidates (cons :async
(lambda (callback)
(jade-repl-get-completions arg callback))))))
(defun jade-repl-get-completions (arg callback)
(let ((expression (buffer-substring-no-properties jade-repl-input-start-marker
(point-max-marker))))
(jade-backend-get-completions expression arg callback)))
(defun jade-repl-company-prefix ()
"Prefix for company."
(and (eq major-mode 'jade-repl-mode)
(or (company-grab-symbol-cons "\\." 1)
'stop)))
(defvar jade-repl-mode-hook nil
"Hook executed when entering `jade-repl-mode'.")
(declare 'jade-quit)
(defvar jade-repl-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [return] #'jade-repl-return)
(define-key map "\C-m"#'jade-repl-return)
(define-key map [mouse-1] #'jade-follow-link)
(define-key map (kbd "C-<return>") #'jade-repl-newline)
(define-key map (kbd "C-c C-o") #'jade-repl-clear-output)
(define-key map (kbd "C-c C-q") #'jade-quit)
(define-key map (kbd "M-p") #'jade-repl-previous-input)
(define-key map (kbd "M-n") #'jade-repl-next-input)
;; (define-key map (kbd "C-c M-i") #'jade-repl-inspect)
map))
(define-derived-mode jade-repl-mode fundamental-mode "JS REPL"
"Major mode for jade REPL interactions.
\\{jade-repl-mode-map}"
(setq-local font-lock-defaults (list js--font-lock-keywords))
(setq-local syntax-propertize-function #'js-syntax-propertize)
(font-lock-ensure)
(setq-local company-backends '(company-jade-repl))
(company-mode 1)
(setq-local comment-start "// ")
(setq-local comment-end ""))
(provide 'jade-repl)
;;; jade-repl.el ends here

+ 421
- 0
jade.el View File

@ -0,0 +1,421 @@
;;; jade.el --- JavaScript Awesome Development Environment -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: 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:
;;
;;; Code:
(require 'websocket)
(require 'url)
(require 'json)
(require 'map)
(require 'seq)
(require 'jade-repl)
(require 'jade-debugger)
(defvar jade-connections (list) "List of connections.")
(defvar jade-connection nil
"Current connection to the browser tab.
A connection should be an alist with the following required keys:
`backend' and `url'. Other backend-specific keys might be used
by backends.")
(make-variable-buffer-local 'jade-connection)
(defvar jade-backend-completion-function "function getCompletions(type)\n{var object;if(type===\"string\")\nobject=new String(\"\");else if(type===\"number\")\nobject=new Number(0);else if(type===\"boolean\")\nobject=new Boolean(false);else\nobject=this;var resultSet={};for(var o=object;o;o=o.__proto__){try{if(type===\"array\"&&o===object&&ArrayBuffer.isView(o)&&o.length>9999)\ncontinue;var names=Object.getOwnPropertyNames(o);for(var i=0;i<names.length;++i)\nresultSet[names[i]]=true;}catch(e){}}\nreturn resultSet;}")
(defun jade-connect (host port)
"Open a connection to a browser tab on HOST:PORT."
(interactive (list (read-from-minibuffer "Host: " "127.0.0.1")
(read-from-minibuffer "Port: " "9222")))
(jade-backend-get-tabs-data host port #'jade-backend-connect-to-tab))
(defun jade-quit ()
"Close the current connection and kill its REPL buffer if any."
(interactive)
(unless jade-connection
(user-error "No active connection to close"))
(when (y-or-n-p (format "Do you really want to close the connection to %s ?"
(map-elt jade-connection 'url)))
(when (jade-connected-p)
(websocket-close (map-elt jade-connection 'ws)))
(map-put jade-connection 'callbacks nil)
(setq jade-connections (remq jade-connection jade-connections))
(kill-buffer (jade-repl-get-buffer))))
(defun jade-backend-get-tabs-data (host port callback)
"Evaluate CALLBACK with the list of open tabs on HOST:PORT."
(url-retrieve (format "http://%s:%s/json" host port)
(lambda (status)
;; TODO: handle errors
(funcall callback (jade-backend--read-tab-data)))))
(defun jade-backend-connect-to-tab (tabs)
"Ask the user for a tab in the list TABS and connects to it."
(let* ((titles (seq-map (lambda (tab)
(map-elt tab 'title))
tabs))
(title (completing-read "Tab: " titles nil t)))
(jade-backend--open-ws-connection (seq-find (lambda (tab)
(string= (map-elt tab 'title) title))
tabs))))
(defun jade-backend--read-tab-data ()
"Return the JSON tabs data."
(when (save-match-data
(looking-at "^HTTP/1\\.1 200 OK$"))
(goto-char (point-min))
(search-forward "\n\n")
(delete-region (point-min) (point))
(json-read)))
(defun jade-backend--open-ws-connection (tab)
"Open a websocket connection to TAB."
(let ((url (map-elt tab 'url))
(debugger-url (map-elt tab 'webSocketDebuggerUrl)))
(unless debugger-url
(user-error "Cannot open connection, another devtools instance might be open"))
(websocket-open debugger-url
:on-open (lambda (ws) (jade-backend-handle-ws-open ws url))
:on-message #'jade-backend-handle-ws-message
:on-close #'jade-backend-handle-ws-closed
:on-error #'jade-backend-handle-ws-error)))
(defun jade-backend--make-connection (ws url)
"Return a new connection for WS and URL."
(let ((connection `((ws . ,ws)
(url . ,url)
(callbacks . ,(make-hash-table)))))
(add-to-list 'jade-connections connection)
connection))
(defun jade-backend--callbacks ()
"Return the callbacks for the current connection."
(map-elt jade-connection 'callbacks))
(defun jade-backend--connection-for-ws (ws)
"Return the connection associated with WS."
(seq-find (lambda (connection)
(eq (map-elt connection 'ws) ws))
jade-connections))
(defun jade-backend-handle-ws-open (ws url)
(let* ((connection (jade-backend--make-connection ws url))
(jade-connection connection))
(jade-backend-enable-tools)
(switch-to-buffer (jade-repl-get-buffer-create connection))))
(defun jade-backend-handle-ws-message (ws frame)
(let* ((jade-connection (jade-backend--connection-for-ws ws))
(message (jade--backend-read-ws-message frame))
(error (map-elt message 'error))
(method (map-elt message 'method))
(request-id (map-elt message 'id))
(callback (map-elt (jade-backend--callbacks) request-id)))
(cond
(error (message (map-elt error 'message)))
(request-id (when callback
(funcall callback message)))
(t (pcase method
("Console.messageAdded" (jade-backend-handle-console-message message))
("Debugger.paused" (jade-backend-handle-debugger-paused message))
("Debugger.resumed" (jade-backend-handle-debugger-resumed message)))))))
(defun jade-backend-handle-console-message (message)
(let* ((level (map-nested-elt message '(params message level)))
(text (map-nested-elt message '(params message text))))
(jade-repl-emit-console-message text level)))
(defun jade-backend-handle-debugger-paused (message)
(let ((frames (map-nested-elt message '(params callFrames))))
(jade-debugger-paused (jade-backend-frames frames))))
(defun jade-backend-handle-debugger-resumed (_message)
(jade-debugger-resumed))
(defun jade-backend-handle-ws-closed (_ws)
;; TODO
)
(defun jade-backend-handle-ws-error (ws action error)
(message "WS Error! %s %s" action error))
(defun jade-backend-send-request (request &optional callback)
(when (not (jade-connected-p))
(jade-repl-emit-console-message "Socket connection closed" "error"))
(let ((id (jade-next-request-id))
(callbacks (jade-backend--callbacks)))
(when callback
(map-put callbacks id callback))
(websocket-send-text (map-elt jade-connection 'ws)
(json-encode (cons `(id . ,id) request)))))
(defun jade--backend-read-ws-message (frame)
(with-temp-buffer
(insert (websocket-frame-payload frame))
(goto-char (point-min))
(json-read)))
(defun jade-backend-enable-tools ()
(jade-backend-enable-console)
(jade-backend-enable-runtime)
(jade-backend-enable-debugger))
(defun jade-backend-enable-console ()
(jade-backend-send-request '((method . "Console.enable"))))
(defun jade-backend-enable-runtime ()
(jade-backend-send-request '((method . "Runtime.enable"))))
(defun jade-backend-enable-debugger ()
(jade-backend-send-request '((method . "Debugger.enable"))))
(defun jade-backend-evaluate (string &optional callback context)
"Evaluate STRING then call CALLBACK.
CALLBACK is called with two arguments, the value returned by the
evaluation and non-nil if the evaluation threw an error."
(jade-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,string)
(generatePreview . t))))
(lambda (response)
(jade-backend-handle-evaluation-response response callback))))
(defun jade-backend-handle-evaluation-response (response callback)
"Evaluate CALLBACK with the evaluation result in RESPONSE."
(let* ((result (map-nested-elt response '(result result)))
(error (eq (map-nested-elt response '(result wasThrown)) t)))
(funcall callback (jade-backend-value result) error)))
(defun jade-backend-get-completions (expression prefix callback)
"Get the completion candidates for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates"
(let ((expression (jade-backend-completion-expression expression)))
(jade-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(objectGroup . "completion"))))
(lambda (response)
(jade-backend-handle-completions-response response prefix callback)))))
(defun jade-backend-handle-completions-response (response prefix callback)
"Request a completion list for the object in RESPONSE.
Evaluate CALLBACK with the completion list, filtered using PREFIX."
(let ((objectid (map-nested-elt response '(result result objectId)))
(type (map-nested-elt response '(result result type))))
(if objectid
(jade-backend-get-completion-list-by-reference objectid prefix callback)
(jade-backend-get-completion-list-by-type type prefix callback))))
(defun jade-backend-get-completion-list-by-reference (objectid prefix callback)
"Request the completion list for a remote object referenced by OBJECTID.
Evaluate CALLBACK with the completion list, filtered using PREFIX."
(jade-backend-send-request
`((method . "Runtime.callFunctionOn")
(params . ((objectId . ,objectid)
(functionDeclaration . ,jade-backend-completion-function)
(returnByValue . t))))
(lambda (response)
(jade-backend-handle-completion-list-response response prefix callback))))
(defun jade-backend-get-completion-list-by-type (type prefix callback)
"Request the completion list for an object of type TYPE.
Evaluate CALLBACK with the completion list, filtered using PREFIX.
This method is used for strings, numbers and booleans. See
`jade-backend-get-completion-list-by-reference' for getting
completions using references to remote objects (including
arrays)."
(let ((expression (format "(%s)(\"%s\")" jade-backend-completion-function type)))
(jade-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(returnByValue . t))))
(lambda (response)
(jade-backend-handle-completion-list-response response prefix callback)))))
(defun jade-backend-completion-expression (string)
"Return the expression from STRING for which the completion
should be requested."
(if (string-match-p "\\." string)
(replace-regexp-in-string "\\.[^\\.]*$" "" string)
"this"))
(defun jade-backend-handle-completion-list-response (response prefix callback)
"Evauate CALLBACK on the completion candidates from RESPONSE.
Candidates are filtered using the PREFIX string."
(let ((candidates (map-nested-elt response '(result result value))))
(funcall callback (seq-filter (lambda (candidate)
(string-prefix-p prefix candidate))
(seq-map (lambda (candidate)
(symbol-name (car candidate)))
candidates)))))
(defun jade-backend-get-properties (reference &optional callback all-properties)
"Get the properties of the remote object represented by
REFERENCE, then evaluate CALLBACK with the list of properties.
If ALL-PROPERTIES is non-nil, get all the properties from the
prototype chain of the remote object."
(jade-backend-send-request
`((method . "Runtime.getProperties")
(params . ((objectId . ,reference)
(ownProperties . ,(not all-properties)))))
(lambda (response)
(jade-backend-handle-get-properties-response response callback))))
(defun jade-backend-handle-get-properties-response (response callback)
(let ((properties (jade-backend-properties (map-nested-elt response '(result result)))))
(funcall callback properties)))
(defun jade-backend-get-script-source (frame callback)
(let ((script-id (map-nested-elt frame '(location scriptId))))
(jade-backend-send-request
`((method . "Debugger.getScriptSource")
(params . ((scriptId . ,script-id))))
callback)))
(defun jade-backend-resume (&optional callback)
(jade-backend-send-request
`((method . "Debugger.resume"))
callback))
(defun jade-backend-step-into (&optional callback)
(jade-backend-send-request
`((method . "Debugger.stepInto"))
callback))
(defun jade-backend-step-out (&optional callback)
(jade-backend-send-request
`((method . "Debugger.stepOut"))
callback))
(defun jade-backend-step-over (&optional callback)
(jade-backend-send-request
`((method . "Debugger.stepOver"))
callback))
(defun jade-backend-continue-to-location (location &optional callback)
(jade-backend-send-request
`((method . "Debugger.continueToLocation")
(params . ((location . ,location))))
callback))
(defun jade-backend-value (result)
"Return an alist representing the value of RESULT.
The returned value can be a reference to a remote object, in
which case the value associated to the `objectid' key is
non-nil."
(let* ((value (map-elt result 'value))
(type (intern (map-elt result 'type)))
(objectid (map-elt result 'objectId))
(preview (jade-backend-preview result))
(description (jade-backend-description result)))
`((objectid . ,objectid)
(description . ,description)
(type . ,type)
(preview . ,preview)
(value . ,value))))
(defun jade-backend-description (result)
(let ((value (map-elt result 'value))
(type (intern (map-elt result 'type))))
(or (map-elt result 'description)
(pcase type
(`undefined "undefined")
(`function "function")
(`number (if (numberp value)
(number-to-string value)
value))
(`string (format "\"%s\"" value))
(`boolean (pcase value
(`t "true")
(_ "false")))
(_ (or value "null"))))))
(defun jade-backend-preview (result)
(let* ((preview (map-elt result 'preview))
(subtype (map-elt preview 'subtype)))
(if (string= subtype "array")
(jade-backend-preview-array preview)
(jade-backend-preview-object preview))))
(defun jade-backend-preview-object (preview)
(concat " { "
(mapconcat (lambda (prop)
(format "%s: %s"
(map-elt prop 'name)
(jade-backend-description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
", … }"
" }")))
(defun jade-backend-preview-array (preview)
(concat " [ "
(mapconcat (lambda (prop)
(format "%s" (jade-backend-description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
"… ]"
" ]")))
(defun jade-backend-properties (result)
"Return a list of object properties built from RESULT."
(seq-map (lambda (prop)
`((name . ,(map-elt prop 'name))
(value . ,(jade-backend-value (or (map-elt prop 'value)
(map-elt prop 'get))))))
result))
(defun jade-backend-frames (list)
"Return a list of frames built from LIST."
(seq-map (lambda (frame)
`((scope-chain . ,(seq-map (lambda (scope)
`((object . ,(jade-backend-value (map-elt scope 'object)))
(name . ,(map-elt scope 'name))
(type . ,(map-elt scope 'type))))
(map-elt frame 'scopeChain)))
(location . ,(map-elt frame 'location))))
list))
(defun jade-backend-object-reference-p (value)
"Return non-nil if VALUE is a reference to a remote object."
(map-elt value 'objectid))
(defun jade-connected-p ()
"Return non-nil if the current connction is open."
(websocket-openp (map-elt jade-connection 'ws)))
(let ((id 0))
(defun jade-next-request-id ()
(setq id (1+ id))
id))
(provide 'jade)
;;; jade.el ends here

Loading…
Cancel
Save