diff --git a/lib/fg42/dashboard/core.el b/lib/fg42/dashboard/core.el new file mode 100644 index 0000000..e69de29 diff --git a/lib/fg42/devtools.el b/lib/fg42/devtools.el index 9084684..82cf34a 100644 --- a/lib/fg42/devtools.el +++ b/lib/fg42/devtools.el @@ -133,6 +133,7 @@ (let* ((data (fg42/devtools-decode (websocket-frame-payload data))) (method (plist-get data :method)) (params (plist-get data :params))) + (inspect-data-append data) (pcase method ("Debugger.scriptParsed" (fg42/devtools-on-script-parsed params)) ;; we are getting an error in Console.messageAdded @@ -143,10 +144,8 @@ ;; nil -> These are return messages from RPC calls, not notification (_ (if method (inspect-data-append data) - (progn - (inspect-data-append data) - (fg42/devtools-dispatch-callback (plist-get data :id) - (plist-get data :result)))))))) + (fg42/devtools-dispatch-callback (plist-get data :id) + (plist-get data :result))))))) (defun fg42/devtools-call-rpc (method &optional params callback) @@ -165,8 +164,7 @@ "Connect to the given URL and return a socket." (websocket-open url :on-open #'fg42/devtools-on-open - :on-message (lambda (x y) - (fg42/devtools-on-message x y)) + :on-message #'fg42/devtools-on-message :on-close #'fg42/devtools-on-close)) diff --git a/lib/fg42/devtools/core.el b/lib/fg42/devtools/core.el new file mode 100644 index 0000000..a18f745 --- /dev/null +++ b/lib/fg42/devtools/core.el @@ -0,0 +1,315 @@ +;;; fg42-devtools --- Webkit devtool driver for FG42 +;; +;; Copyright (c) 2019 Sameer Rahmani +;; +;; Author: Sameer Rahmani +;; URL: https://gitlab.com/FG42/FG42 +;; Keywords: webkit +;; Version: 0.1.0 +;; Package-Requires: ((dash "2.11.0") (websocket "1.5")) +;; +;; 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 . +;; +;;; Acknoledgement: +;; This library is heavily inspired by Kite mini library. Kudos Tung Dao +;; for his great work. +;; +;;; Commentary: +;;; Code: + + +(require 'url) +(require 'dash) +(require 'websocket) + +(require 'fg42/utils) + +;;; Customs & Vars ------------------------------------------------------------ +(defcustom fg42/devtools-remote-host "127.0.0.1" + "Default host for connection to WebKit remote debugging API." + :group 'fg42/devtools) + + +(defcustom fg42/devtools-remote-port 9222 + "Default port for connection to WebKit remote debugging API." + :group 'fg42/devtools) + + +(defvar fg42/devtools-socket nil + "Websocket connection to WebKit remote debugging API.") + + +(defvar fg42/devtools-rpc-id 0) +(defvar fg42/devtools-rpc-callbacks nil) +(defvar fg42/devtools-rpc-scripts nil + "List of JavaScript files available for live editing.") + +;;; Functions ----------------------------------------------------------------- +(defun fg42/devtools-next-rpc-id () + "Retun the next RPC call id to be used." + (setq fg42/devtools-rpc-id (+ 1 fg42/devtools-rpc-id))) + + +(defun fg42/devtools-register-callback (id fn) + "Register the given FN function with the given ID as rpc Callback." + (let ((hook (intern (number-to-string id) fg42/devtools-rpc-callbacks))) + (add-hook hook fn t))) + +(defun fg42/devtools-dispatch-callback (id data) + "Execute the callback registered by the given ID with the given DATA." + (let ((hook (intern (number-to-string id) fg42/devtools-rpc-callbacks))) + (when hook + (run-hook-with-args hook data) + (unintern hook fg42/devtools-rpc-callbacks)))) + + +(defun fg42/devtools-on-open (socket) + "Connect to the given SOCKET." + (message "FG42: connected to devtools.")) + + +(defun fg42/devtools-on-close (socket) + "Disconnect from the given SOCKET." + (message "FG42: disconnected from devtools.")) + + +(defun fg42/devtools-on-script-parsed (data) + (let ((extension? (plist-get data :isContentScript)) + (url (plist-get data :url)) + (id (plist-get data :scriptId))) + (when (and (eq extension? :json-false) (not (string-equal "" url))) + (add-to-list 'fg42/devtools-rpc-scripts (list :id id :url url))))) + + +(defun fg42/devtools-on-script-failed-to-parse (data) + (fg42/devtools-console-append (format "%s" data))) + + +(defun fg42/devtools-on-message-added (data) + (let* ((message (plist-get data :message)) + (url (plist-get message :url)) + (column (plist-get message :column)) + (line (plist-get message :line)) + (type (plist-get message :type)) + (level (plist-get message :level)) + (text (plist-get message :text))) + ;; TODO: add colors based on level + (fg42/devtools-console-append + (propertize + (format "%s: %s\t%s (line: %s column: %s)" + level text url line column) + 'font-lock-face (intern (format "fg42/devtools-log-%s" level)))))) + + +(defun fg42/devtools-on-message (socket data) + "The on message callback that gets the incoming DATA from the SOCKET." + (let* ((data (<-json (websocket-frame-payload data))) + (method (plist-get data :method)) + (params (plist-get data :params))) + (inspect-data-append data) + (pcase method + ("Debugger.scriptParsed" (fg42/devtools-on-script-parsed params)) + ;; we are getting an error in Console.messageAdded + ;; ("Debugger.scriptFailedToParse" (fg42/devtools-on-script-failed-to-parse params)) + ("Console.messageAdded" (fg42/devtools-on-message-added params)) + ;; ;; TODO: do something usefull here, possibly great for REPL + ("Console.messageRepeatCountUpdated") + ;; nil -> These are return messages from RPC calls, not notification + (_ (if method + (inspect-data-append data) + (fg42/devtools-dispatch-callback (plist-get data :id) + (plist-get data :result))))))) + + +(defun fg42/devtools-call-rpc (method &optional params callback) + "Call the given METHOD with PARAMS and call CALLBACK with the result." + (let ((id (fg42/devtools-next-rpc-id))) + (when callback + (fg42/devtools-register-callback id callback)) + (websocket-send-text + fg42/devtools-socket + (->json (list :id id + :method method + :params params))))) + + +(defun fg42/devtools-open-socket (url) + "Connect to the given URL and return a socket." + (websocket-open url + :on-open #'fg42/devtools-on-open + :on-message #'fg42/devtools-on-message + :on-close #'fg42/devtools-on-close)) + + +(defun fg42/devtools-get-json (url) + "Fetch the json data of the given URL using a GET request." + (let* ((url-request-method "GET") + (url-http-attempt-keepalives nil) + (json-array-type 'list) + (json-object-type 'plist)) + (with-current-buffer (url-retrieve-synchronously url) + (if (not (eq 200 (url-http-parse-response))) + (error "FG42: Unable to connect to devtools host") + (goto-char (+ 1 url-http-end-of-headers)) + (json-read))))) + + +(defun fg42/devtools-get-tabs (host port) + "Read the list of open tabs from the webkit instance at HOST:PORT." + (let* ((url (url-parse-make-urlobj + "http" nil nil host port "/json")) + (tabs (fg42/devtools-get-json url))) + (-filter (lambda (tab) + (and (plist-get tab :webSocketDebuggerUrl) + (string-equal (plist-get tab :type) "page"))) + tabs))) + + +(defun fg42/devtools-tab-completion (tab) + "A simple completion backend for the given TAB." + (let ((title (plist-get tab :title)) + (url (plist-get tab :url))) + (cons (format "%s" title) tab))) + + +(defun fg42/devtools-select-tab (host port) + "Print out the list of tabs from HOST:PORT for the user to choose from." + (let* ((tabs (mapcar #'fg42/devtools-tab-completion + (fg42/devtools-get-tabs host port))) + (selection (completing-read + "Tab: " tabs nil t "" nil (caar tabs))) + (tab (cdr (assoc selection tabs)))) + (plist-get tab :webSocketDebuggerUrl))) + + +(defun fg42/devtools-connect () + "Conntect to the Webkit devtools." + (interactive) + (message "FG42: Disconnect from any previous connection.") + (fg42/devtools-disconnect) + (let* ((socket-url (fg42/devtools-select-tab fg42/devtools-remote-host + fg42/devtools-remote-port))) + (message (format "FG42: Connecting to %s" socket-url)) + (setq fg42/devtools-socket (fg42/devtools-open-socket socket-url)) + (message "Sending initial RPC calls...") + (fg42/devtools-call-rpc "Console.enable") + (fg42/devtools-call-rpc "Debugger.enable") + (fg42/devtools-call-rpc "Network.setCacheDisabled" '(:cacheDisabled t)))) + + +(defun fg42/devtools-disconnect () + "Disconnect from the Webkit devtools." + (interactive) + (when (websocket-openp fg42/devtools-socket) + (websocket-close fg42/devtools-socket) + (setq fg42/devtools-socket nil + fg42/devtools-rpc-scripts nil))) + + +(defun fg42/devtools-send-eval (code &optional callback) + "Send the given CODE to the devtools for evaluation and call the CALLBACK." + (fg42/devtools-call-rpc + "Runtime.evaluate" + (list :expression code + :returnByValue t) + callback)) + + +(defun fg42/devtools-remove-script (script) + (setq fg42/devtools-rpc-scripts + (delete script fg42/devtools-rpc-scripts))) + + +(defun fg42/devtools-script-id (file) + (let* ((name (file-name-nondirectory file)) + (script (--find (string-suffix-p name (plist-get it :url)) + fg42/devtools-rpc-scripts))) + (when script (plist-get script :id)))) + + +(defun fg42/devtools-update () + (interactive) + (let ((id (fg42/devtools-script-id (buffer-file-name))) + (source (buffer-substring-no-properties + (point-min) (point-max)))) + (if id + (fg42/devtools-call-rpc + "Debugger.setScriptSource" + (list :scriptId id :scriptSource source)) + (message "No matching script for current buffer.")))) + + +(defun fg42/devtools-reload () + "Reload the tab." + (interactive) + (fg42/devtools-call-rpc + "Page.reload" + (list :ignoreCache t))) + + +(defun fg42/devtools-evaluate-region-or-line (&optional args) + (interactive "*P") + (let ((start (if (region-active-p) + (region-beginning) + (line-beginning-position))) + (end (if (region-active-p) + (region-end) + (line-end-position)))) + (fg42/devtools-send-eval (buffer-substring-no-properties start end)))) + + +(defvar fg42/devtools-mode-map + (let ((map (make-sparse-keymap))) + (prog1 map + (define-key map (kbd "C-c C-c") #'fg42/devtools-evaluate-region-or-line) + (define-key map (kbd "C-c C-k") #'fg42/devtools-update) + (define-key map (kbd "C-c C-r") #'fg42/devtools-reload))) + "Keymap for FG42 devtools mode.") + +;;;###autoload +(defun turn-on-fg42/devtools-mode () + "Turn on FG42 devtools mode.") + +;;;###autoload +(defun turn-off-fg42/devtools-mode () + "Turn off FG42 devtools mode.") + +;;;###autoload +(define-minor-mode fg42/devtools-mode + "Minor mode for interact with WebKit remote debugging API." + :global nil + :group 'fg42/devtools + :init-value nil + :lighter "" + :keymap fg42/devtools-mode-map + (if fg42/devtools-mode + (turn-on-fg42/devtools-mode) + (turn-off-fg42/devtools-mode))) + +(defun fg42/devtools-debug-on () + (interactive) + (setq websocket-callback-debug-on-error t)) + +(defun fg42/devtools-debug-restart () + (interactive) + (fg42/devtools-debug-on) + (message "D: disconnect") + (fg42/devtools-disconnect) + (fg42/devtools-connect)) + +;; (fg42/devtools-debug-restart) + + +(provide 'fg42/devtools) +;;; devtools.el ends here diff --git a/lib/fg42/devtools/network.el b/lib/fg42/devtools/network.el new file mode 100644 index 0000000..e69de29 diff --git a/lib/fg42/devtools/utils/json.el b/lib/fg42/devtools/utils/json.el new file mode 100644 index 0000000..95535fa --- /dev/null +++ b/lib/fg42/devtools/utils/json.el @@ -0,0 +1,49 @@ +;;; json --- FG42 json helpers +;; +;; Copyright (c) 2019 Sameer Rahmani +;; +;; Author: Sameer Rahmani +;; URL: https://gitlab.com/FG42/FG42 +;; Keywords: webkit +;; Version: 0.1.0 +;; Package-Requires: ((dash "2.11.0") (websocket "1.5")) +;; +;; 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 . +;; +;;; Acknoledgement: +;; This library is heavily inspired by Kite mini library. Kudos Tung Dao +;; for his great work. +;; +;;; Commentary: +;;; Code: + +(require 'json) + + +(defun ->json (data) + "Convert the given DATA to json." + (let ((json-array-type 'list) + (json-object-type 'plist)) + (json-encode data))) + + +(defun <-json (data) + "Convert the given json DATA to elisp data structure." + (let ((json-array-type 'list) + (json-object-type 'plist)) + (json-read-from-string data))) + + +(provide 'fg42/utils/json) +;;; json.el ends here diff --git a/lib/fg42/utils.el b/lib/fg42/utils.el index 31d69bb..299f30d 100644 --- a/lib/fg42/utils.el +++ b/lib/fg42/utils.el @@ -3,25 +3,9 @@ ;;; Code: (require 'cl) -(require 'json) (require 'fg42/vars) - - -;;; JSON ---------------------------------------------------------------------- -(defun ->json (data) - "Convert the given DATA to json." - (let ((json-array-type 'list) - (json-object-type 'plist)) - (json-encode data))) - - -(defun <-json (data) - "Convert the given json DATA to elisp data structure." - (let ((json-array-type 'list) - (json-object-type 'plist)) - (json-read-from-string data))) - +(require 'fg42/utils/json) ;;; Buffer helpers ------------------------------------------------------------ (defun buffer-mode (buffer-or-string) @@ -67,10 +51,13 @@ with is the buffer." `(pp-display-expression ,@body (get-buffer-create fg42/inspect-buffer))) (defun inspect-data-append (data) - (->buffer fg42/inspect-buffer - "START ========================================================\n") - (->buffer fg42/inspect-buffer (pp-to-string data)) - (->buffer fg42/inspect-buffer "END.\n")) + "Append the given DATA to the inspection buffer with padding." + (->buffer + fg42/inspect-buffer + (format + "\n;; START ======================================================\n%s%s" + (pp-to-string data) + ";; END.\n"))) (provide 'fg42/utils)