├── config.el ├── local ├── gnome-shell-mode │ ├── gnome-shell-mode@hedning:matrix.org │ │ ├── metadata.json │ │ ├── extension.js │ │ └── emacs.js │ ├── gnome-shell-mode-pkg.el │ ├── bootstrap.js │ ├── session.sh │ └── gnome-shell-mode.el └── company-gnome-shell │ └── company-gnome-shell.el ├── packages.el ├── README.md └── LICENSE /config.el: -------------------------------------------------------------------------------- 1 | 2 | (spacemacs|define-jump-handlers gnome-shell-mode gnome-shell-goto-definition) 3 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/gnome-shell-mode@hedning:matrix.org/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "gnome-shell-mode@hedning:matrix.org", 3 | "name": "gnome-shell-mode", 4 | "description": "Javascript server for emacs", 5 | "url": "https://github.com/paperwm/gnome-shell-mode", 6 | "shell-version": [ "3.22", "3.24", "3.26" ] 7 | } 8 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/gnome-shell-mode-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "gnome-shell-mode" "0.1" 2 | "Major mode for developing gnome-shell javascript" 3 | '((emacs "24") (js2-mode "20180724.801") (flycheck "20181018.1021")) 4 | :authors 5 | '(("Tor Hedin Brønner", "torhedinbronner@gmail.com") 6 | ("Ole Jørgen Brønner", "olejorgenb@yahoo.no")) 7 | :url "https://github.com/paperwm/gnome-shell-mode") 8 | 9 | 10 | ;; Local Variables: 11 | ;; no-byte-compile: t 12 | ;; End: 13 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/bootstrap.js: -------------------------------------------------------------------------------- 1 | (function (path) { 2 | const Gio = imports.gi.Gio; 3 | let extensionUtils; 4 | let extensionSystem; 5 | // Work around differences between 3.32 and 3.34 6 | if (imports.misc.extensionUtils.createExtensionObject) { 7 | extensionSystem = imports.ui.extensionSystem; 8 | extensionUtils = imports.misc.extensionUtils; 9 | } else { 10 | extensionSystem = imports.ui.main.extensionManager; 11 | extensionUtils = extensionSystem; 12 | } 13 | 14 | const uuid = "gnome-shell-mode@hedning:matrix.org"; 15 | let dir = Gio.File.new_for_path(`${path}${uuid}`); 16 | let extension = extensionUtils.createExtensionObject(uuid, dir, 1); 17 | 18 | extensionSystem.loadExtension(extension); 19 | extensionSystem.enableExtension(uuid); 20 | }) 21 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/session.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OLD_DISPLAY=$DISPLAY 4 | 5 | TYPE=${1:-$XDG_SESSION_TYPE}; shift 6 | ROOT=$1; shift 7 | UUID=$1; shift 8 | 9 | d=0 10 | while [ -e /tmp/.X11-unix/X${d} ]; do 11 | d=$((d + 1)) 12 | done 13 | NEW_DISPLAY=:$d 14 | 15 | XDG_RUNTIME_DIR=$(mktemp -d) 16 | 17 | CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/${UUID}${SUFFIX} 18 | mkdir -p $CACHE 19 | export XDG_CONFIG_HOME=${CACHE}/config 20 | export XDG_DATA_HOME=${CACHE}/local 21 | mkdir -p $XDG_DATA_HOME/gnome-shell/extensions 22 | ln -fsn $ROOT $XDG_DATA_HOME/gnome-shell/extensions/${UUID} 23 | export XDG_CACHE_HOME=${CACHE}/cache 24 | 25 | DISPLAY=$NEW_DISPLAY 26 | eval $(dbus-launch --exit-with-session --sh-syntax) 27 | echo $DBUS_SESSION_BUS_ADDRESS 28 | 29 | DISPLAY=$OLD_DISPLAY 30 | args=() 31 | case "$TYPE" in 32 | wayland) 33 | args=(--nested --wayland) 34 | ;; 35 | x11) 36 | Xephyr $NEW_DISPLAY & 37 | DISPLAY=$NEW_DISPLAY 38 | args=--x11 39 | ;; 40 | esac 41 | 42 | # dconf reset -f / # Reset settings 43 | dconf write /org/gnome/shell/enabled-extensions "['${UUID}']" 44 | 45 | # export CLUTTER_SHOW_FPS=1 46 | export SHELL_DEBUG=all 47 | export MUTTER_DEBUG=1 48 | export MUTTER_DEBUG_NUM_DUMMY_MONITORS=1 49 | export MUTTER_DEBUG_DUMMY_MONITOR_SCALES=1 50 | export MUTTER_DEBUG_TILED_DUMMY_MONITORS=1 51 | gnome-shell ${args[*]} 2>&1 | sed 's/\x1b\[[0-9;]*m//g' 52 | -------------------------------------------------------------------------------- /local/company-gnome-shell/company-gnome-shell.el: -------------------------------------------------------------------------------- 1 | ;;; company-gnome-shell.el --- Gnome Shell runtime js completion -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2017-2019 Tor Hedin Brønner and Ole Jørgen Brønner 4 | 5 | ;; Author: Tor Hedin Brønner 6 | ;; Ole Jørgen Brønner 7 | ;; Homepage: https://github.com/paperwm/gnome-shell-mode 8 | ;; Version: 0.1 9 | ;; Package-Requires: ((emacs "24") (company "20181105") (gnome-shell-mode "0.1")) 10 | 11 | ;; This file is not part of GNU Emacs 12 | 13 | ;;; Code: 14 | 15 | (require 'gnome-shell-mode) 16 | (require 'company) 17 | 18 | (defun company-gnome-shell--candidates () 19 | "Candidates handler for the company backend." 20 | (cons :async 21 | (lambda (cb) 22 | (let* ((context (gnome-shell--name-at-point)) 23 | (candidates (gnome-shell--dbus-complete context))) 24 | (funcall cb candidates))))) 25 | 26 | (defun company-gnome-shell--prefix () 27 | (unless (company-in-string-or-comment) 28 | ;; Trigger completion at once if the immediate left char is '.' or ':' 29 | ;; (ignoring company-minimum-prefix-length). 30 | ;; See 'prefix' documentation in company.el 31 | (or (company-grab-symbol-cons "[.:]" 1) 32 | 'stop))) 33 | 34 | (defun company-gnome-shell (command &optional arg &rest ignored) 35 | (interactive (list 'interactive)) 36 | (cl-case command 37 | (interactive (company-begin-backend 'company-gnome-shell)) 38 | (prefix (company-gnome-shell--prefix)) 39 | (candidates (company-gnome-shell--candidates)) 40 | (duplicates t) 41 | (sorted nil))) 42 | 43 | (provide 'company-gnome-shell) 44 | 45 | ;;; company-gnome-shell.el ends here 46 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/gnome-shell-mode@hedning:matrix.org/extension.js: -------------------------------------------------------------------------------- 1 | const uuid = "gnome-shell-mode@hedning:matrix.org"; 2 | var Extension; 3 | if (imports.misc.extensionUtils.extensions) { 4 | Extension = imports.misc.extensionUtils.extensions[uuid]; 5 | } else { 6 | Extension = imports.ui.main.extensionManager.lookup(uuid); 7 | } 8 | const Emacs = Extension.imports.emacs; 9 | const Gio = imports.gi.Gio; 10 | const GObject = imports.gi.GObject; 11 | 12 | function init() { 13 | print('init gnome-shell-mode server') 14 | window.emacs = {}; 15 | 16 | emacs.verbose = true; 17 | 18 | emacs.find_property = GObject.Object.find_property; 19 | emacs.list_properties = GObject.Object.list_properties; 20 | 21 | // Probably possible to extract from the error stack, but hardcode for now 22 | // Note: will change if newEval is redefined, restart gnome-shell when making 23 | // changes to this code for now 24 | // in gnome-shell 3.24.3 eval lineNumber is correct! 25 | emacs.eval_line_offset = 0; 26 | } 27 | 28 | const EvalIface = 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 | let dbusImpl; 58 | function enable() { 59 | print('enable gnome-shell-mode server') 60 | 61 | let DbusObject = { 62 | Eval: function (code, path) { 63 | return Emacs.Eval(code, path); 64 | }, 65 | Reload: function (code, path) { 66 | return Emacs.Reload(code, path); 67 | }, 68 | Complete: function (code, path) { 69 | return Emacs.completionCandidates(code, path); 70 | }, 71 | Restart: function (path) { 72 | Emacs.Restart(path); 73 | } 74 | }; 75 | 76 | dbusImpl = Gio.DBusExportedObject.wrapJSObject(EvalIface, DbusObject); 77 | dbusImpl.export(Gio.DBus.session, '/gnome/shell/mode'); 78 | } 79 | function disable() { 80 | print('disable gnome-shell-mode server'); 81 | dbusImpl.unexport(); 82 | } 83 | -------------------------------------------------------------------------------- /packages.el: -------------------------------------------------------------------------------- 1 | ;;; packages.el --- gnome-shell layer packages file for Spacemacs. 2 | ;; 3 | ;; Copyright (c) 2012-2016 Sylvain Benner & Contributors 4 | ;; 5 | ;; Author: Ole Jørgen and Tor Hedin 6 | ;; URL: https://github.com/syl20bnr/spacemacs 7 | ;; 8 | ;; This file is not part of GNU Emacs. 9 | ;; 10 | ;;; License: GPLv3 11 | 12 | ;;; Commentary: 13 | 14 | ;; See the Spacemacs documentation and FAQs for instructions on how to implement 15 | ;; a new layer: 16 | ;; 17 | ;; SPC h SPC layers RET 18 | ;; 19 | ;; 20 | ;; Briefly, each package to be installed or configured by this layer should be 21 | ;; added to `gnome-shell-packages'. Then, for each package PACKAGE: 22 | ;; 23 | ;; - If PACKAGE is not referenced by any other Spacemacs layer, define a 24 | ;; function `gnome-shell/init-PACKAGE' to load and initialize the package. 25 | 26 | ;; - Otherwise, PACKAGE is already referenced by another Spacemacs layer, so 27 | ;; define the functions `gnome-shell/pre-init-PACKAGE' and/or 28 | ;; `gnome-shell/post-init-PACKAGE' to customize the package as it is loaded. 29 | 30 | ;;; Code: 31 | 32 | (defconst gnome-shell-packages 33 | '(company 34 | js2-mode 35 | dbus 36 | (gnome-shell-mode :location local) 37 | (company-gnome-shell :location local) 38 | flycheck) 39 | "The list of Lisp packages required by the gnome-shell layer. 40 | 41 | Each entry is either: 42 | 43 | 1. A symbol, which is interpreted as a package to be installed, or 44 | 45 | 2. A list of the form (PACKAGE KEYS...), where PACKAGE is the 46 | name of the package to be installed or loaded, and KEYS are 47 | any number of keyword-value-pairs. 48 | 49 | The following keys are accepted: 50 | 51 | - :excluded (t or nil): Prevent the package from being loaded 52 | if value is non-nil 53 | 54 | - :location: Specify a custom installation location. 55 | The following values are legal: 56 | 57 | - The symbol `elpa' (default) means PACKAGE will be 58 | installed using the Emacs package manager. 59 | 60 | - The symbol `local' directs Spacemacs to load the file at 61 | `./local/PACKAGE/PACKAGE.el' 62 | 63 | - A list beginning with the symbol `recipe' is a melpa 64 | recipe. See: https://github.com/milkypostman/melpa#recipe-format") 65 | 66 | (defun gnome-shell/init-dbus () 67 | (use-package dbus 68 | :commands (dbus-call-method))) 69 | 70 | (defun gnome-shell/init-gnome-shell-mode () 71 | (use-package gnome-shell-mode 72 | :commands (gnome-shell-mode) 73 | :config 74 | (progn 75 | ;; (spacemacs/set-leader-keys-for-major-mode 'gnome-shell-mode "db" 'gnome-shell-send-buffer) 76 | (spacemacs/set-leader-keys-for-major-mode 'gnome-shell-mode 77 | "sb" 'gnome-shell-send-buffer 78 | "sf" 'gnome-shell-send-proc 79 | "sl" 'gnome-shell-send-current-line 80 | "sr" 'gnome-shell-send-region 81 | 82 | "eb" 'gnome-shell-send-buffer 83 | "ef" 'gnome-shell-send-proc 84 | "el" 'gnome-shell-send-current-line 85 | "er" 'gnome-shell-send-region 86 | 87 | "r" 'gnome-shell-reload 88 | "R" 'gnome-shell-restart 89 | "l" 'gnome-shell-launch-session 90 | "c" 'gnome-shell-clear-output-at-point 91 | "oc" 'gnome-shell-clear-output 92 | "oy" 'gnome-shell-copy-output 93 | "hh" 'gnome-shell-look-up-function-at-point) 94 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "mh" "documentation") 95 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "ms" "eval in session") 96 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "me" "eval in session") 97 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "mo" "output") 98 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "moc" "clear all output") 99 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "mc" "clear output") 100 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "mr" "reload buffer") 101 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "mR" "restart session") 102 | (spacemacs/declare-prefix-for-mode 'gnome-shell-mode "ml" "launch session") 103 | 104 | (evil-define-key 'visual gnome-shell-mode-map (kbd "") 'gnome-shell-send-region) 105 | ) 106 | ) 107 | ) 108 | 109 | (defun gnome-shell/init-company-gnome-shell () 110 | (use-package company-gnome-shell 111 | :if (configuration-layer/package-usedp 'company) 112 | :commands (company-gnome-shell) 113 | :init 114 | (progn 115 | ;; (require 'company) 116 | (spacemacs|add-company-backends :backends company-gnome-shell :modes gnome-shell-mode) 117 | 118 | ))) 119 | 120 | (defun gnome-shell/post-init-js2-mode ()) 121 | 122 | (defun gnome-shell/post-init-company ()) 123 | 124 | (defun gnome-shell/post-init-flycheck () 125 | (spacemacs/enable-flycheck 'gnome-shell-mode)) 126 | 127 | ;;; packages.el ends here 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gnome-shell-mode - Looking Glass for Emacs 2 | 3 | gnome-shell-mode makes it easy to interactively explore and evaluate javascript in a [Gnome Shell](https://wiki.gnome.org/Projects/GnomeShell) session. It supports file local evaluation and auto-completion when working on a loaded Gnome Shell extension. 4 | 5 | ## Installation 6 | 7 | There's no melpa package yet, but it's quite easy to install the package(s) manually. 8 | 9 | ### Doom emacs 10 | 11 | In your `packages.el`: 12 | 13 | ```emacs-lisp 14 | (package! gnome-shell-mode 15 | :recipe (:host github :repo "paperwm/gnome-shell-mode" 16 | :files ("local/gnome-shell-mode/*"))) 17 | (package! company-gnome-shell 18 | :recipe (:host github :repo "paperwm/gnome-shell-mode" 19 | :files ("local/company-gnome-shell/*.el"))) 20 | ``` 21 | 22 | and in your `config.el`: 23 | 24 | ```emacs-lisp 25 | (use-package! gnome-shell-mode 26 | :defer t 27 | :commands (gnome-shell-mode) 28 | :config 29 | (setq-hook! 'gnome-shell-mode-hook 30 | mode-name "GJS") 31 | 32 | (map! 33 | :map gnome-shell-mode-map 34 | :v "" 'gnome-shell-send-region 35 | :gvni "C-" 'gnome-shell-repl 36 | 37 | :map gnome-shell-mode-map 38 | :localleader 39 | :gnv :desc "Reload buffer" "r" 'gnome-shell-reload 40 | :desc "Reload session" "R" 'gnome-shell-restart 41 | :desc "Launch session" "l" 'gnome-shell-launch-session 42 | :desc "Clear output" "c" 'gnome-shell-clear-output-at-point 43 | 44 | (:prefix ("g" . "jump") 45 | :desc "Jump to definition" "g" '+lookup/definition) 46 | 47 | (:prefix ("s" . "eval in session") 48 | :desc "Eval buffer" "b" 'gnome-shell-send-buffer 49 | :desc "Eval function" "f" 'gnome-shell-send-proc 50 | :desc "Eval function" "d" 'gnome-shell-send-proc 51 | :desc "Eval line" "l" 'gnome-shell-send-current-line 52 | :desc "Eval region" "r" 'gnome-shell-send-region) 53 | 54 | (:prefix ("e" . "eval in session") 55 | :desc "Eval buffer" "b" 'gnome-shell-send-buffer 56 | :desc "Eval function" "f" 'gnome-shell-send-proc 57 | :desc "Eval function" "d" 'gnome-shell-send-proc 58 | :desc "Eval line" "l" 'gnome-shell-send-current-line 59 | :desc "Eval region" "r" 'gnome-shell-send-region) 60 | 61 | (:prefix ("o" . "output") 62 | :desc "Clear all output" "c" 'gnome-shell-clear-output 63 | :desc "Copy output" "y" 'gnome-shell-copy-output) 64 | 65 | (:prefix ("h" . "help") 66 | :desc "Lookup at point" "h" 'gnome-shell-look-up-function-at-point 67 | ) 68 | ) 69 | ) 70 | 71 | (use-package! company-gnome-shell 72 | :defer t 73 | :commands (company-gnome-shell) 74 | :init 75 | (set-company-backend! 'gnome-shell-mode 'company-gnome-shell)) 76 | ``` 77 | 78 | ### Spacemacs 79 | 80 | Clone the repo and create a symlink named `gnome-shell` in the spacemacs `private` folder: 81 | ```shell 82 | git clone https://github.com/paperwm/gnome-shell-mode.git ~/the/destination 83 | ln -s ~/the/destination /.emacs.d/private/gnome-shell 84 | ``` 85 | Add gnome-shell to your list of Spacemacs layers: 86 | 87 | ```emacs-lisp 88 | dotspacemacs-configuration-layers 89 | '( 90 | ... 91 | gnome-shell 92 | ... 93 | ) 94 | ``` 95 | 96 | Restart emacs and you're ready to go. 97 | 98 | ### Vanilla emacs 99 | 100 | Add both `local/gnome-shell-mode` and `local/company-gnome-shell` to the `load-path`. 101 | 102 | Then add this to `init.el`: 103 | 104 | ```emacs-lisp 105 | (require 'company) 106 | 107 | (require 'gnome-shell-mode) 108 | (require 'company-gnome-shell) 109 | 110 | ;; Most staight forward but might mess up company in other modes? 111 | (eval-after-load "company" 112 | (add-to-list 'company-backends 'company-gnome-shell)) 113 | ``` 114 | 115 | See `gnome-shell-mode-pkg.el` and `company-gnome-shell.el` for list of dependencies. 116 | 117 | NB: The rest of the readme describe the keybindings defined by the spacemacs layer. Some vanilla emacs bindings are also defined by default. See the bottom of `gnome-shell-mode.el`. 118 | 119 | ## Usage 120 | 121 | Make sure you're in gnome-shell-mode (eg. by using M-x gnome-shell-mode). All the actions will then be under the major-mode leader key (M-m or ,). 122 | 123 | For instance ,sf will evaluate the surrounding function and the evaluated region will pulse green or red depending on the success of the evaluation. If an error occurred, the position reported by gjs will be marked as a flycheck error. 124 | 125 | There's two non-standard keybindings: 126 | - Return will evaluate the active region (if evil is used), the result will be shown in the minibuffer. 127 | - C-Return will evaluate the active region, or the current line if there's no region active. The result will be added in an overlay ala. magit-blame. The overlay can be cleared by ,c or by deleting the input. 128 | 129 | The global variable `$` contains the value of the most recent evaluation. 130 | 131 | ### Launch session 132 | 133 | By default gnome-shell-mode connects to the live Gnome Shell session. This can be a bit risky however, especially on Wayland where restart doesn't work. 134 | 135 | Run `M-x gnome-shell-launch-session`, (`, l` if using spacemacs), to launch and connect to a nested session, the session's log will popup in a new buffer too. When a session is already running `, l` will simply take you to the log. To launch a clean session close the nested Gnome Shell window first. 136 | 137 | If the nested session encounter runtime errors they will be reported as errors in the correct buffer (using flycheck). 138 | 139 | ### Reload 140 | 141 | The mode supports reloading buffers with , r. This works by first disabling the extension, re-evaluating the whole buffer in the correct scope, and then enabling the extension again. 142 | 143 | To get full use of this, `enable` and `disable` need to assemble and disassemble all the state in the extension. A good way to handle this is having `enable` and `disable` functions in every module, making the exension's `enable` and `disable` just call out to the module's functions. 144 | 145 | ### Restart 146 | 147 | Pressing , R in spacemacs will disable the extension the current buffer is part of and then restart Gnome Shell. This gives the extension a change to clean up and save any state making the restart less disruptive. This can also be accessed through the interactive function `gnome-shell-restart`. Note that restart is only supported on X11. 148 | 149 | ### Documentation lookup 150 | 151 | There's basic support for documentation lookup using , h h. This will prompt you with a list of known symbols matching the current word, selecting one will open the documentation of that symbol in your browser. 152 | 153 | ### Gnome Shell extension support 154 | 155 | Auto-completion and evaluation happens in the file local scope when editing a loaded extension, or a file in the Gnome Shell source tree. When editing a file not part of an extension the system creates an ad-hoc scope for the file. 156 | 157 | More specifically, if there's an `imports.some.path` object corresponding to the file being edited the scope of evaluation will be `imports.some.path` (or `someExtension.imports.some.path` in the case of extension code). 158 | 159 | A small example of how this works in practice. Lets say you have a successfully loaded extension in the directory `MyExtension/`and you have some silly functions in `MyExtension/functions.js`: 160 | 161 | ```javascript 162 | function helloWorld (hello, world) { 163 | return `${hello} ${world}`; 164 | } 165 | 166 | function printHelloWorld() { 167 | print(helloWorld('hello', 'world')); 168 | } 169 | 170 | ``` 171 | 172 | Now yout want `helloWorld` to also add some exclamation marks: 173 | ```javascript 174 | function helloWorld (hello, world) { 175 | return `${hello} ${world}!!!`; 176 | } 177 | ``` 178 | 179 | After having made this change you can simply re-evaluate the function (eg. by , s f) and `printHelloWorld` will pick up the change. 180 | 181 | This is done by looking up the extension object through the `uuid` from the `metadata.json` file, and then looking up the module object through the extension relative file path: 182 | ```javascript 183 | let Extension = imports.misc.extensionUtils.extensions[uuid]; 184 | let module = Extension.imports.path.to.current.file; 185 | ``` 186 | 187 | Having the module object we can simply use ``eval(`with(module) { ${code} }`)`` so re-evaluated code will have the correct closure. 188 | 189 | Reassignment relies on SpiderMonkey's built in parser. We traverse the top level statements, replacing all variable and function declarations. So eg. `function name () {}` gets translated to `module.name = function () {}` and `var foo = 'bar';` to `module.foo = 'bar';`. Having a proper parse tree means we can handle complex assignments with descructuring too (eg. 'let [foo, bar] = ...'). 190 | 191 | ## Caveats 192 | 193 | Not all methods of GObjects (g-object-introspected classes) complete before they're used the first time. This include a lot of classes you'll interact with. eg. `MetaWindow`. Fixed in [gjs 1.55.1](https://gitlab.gnome.org/GNOME/gjs/commit/8e982d37e9fd9adcf9e87573d91cbffaf1e7b509) 194 | 195 | While gnome-shell-mode shouldn't cause any crashes by itself, evaluating javascript in Gnome Shell is not completely safe, some code will result in a crash. Eg. looking up a non-existing dconf/schema name will cause a crash. 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/gnome-shell-mode.el: -------------------------------------------------------------------------------- 1 | ;;; gnome-shell-mode.el --- Tight integration of emacs with gnome-shell -*- lexical-binding: t -*- 2 | 3 | ;; Filename: gnome-shell-mode.el 4 | ;; Authors: 5 | ;; - Tor Hedin Brønner 6 | ;; - Ole Jørgen Brønner 7 | 8 | ;; gnome-shell-mode.el is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation; either version 2, or (at your option) 11 | ;; any later version. 12 | 13 | ;; gnome-shell-mode.el is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with GNU Emacs; see the file COPYING. If not, write to 20 | ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 21 | ;; Boston, MA 02111-1307, USA. 22 | 23 | ;;; Commentary 24 | 25 | ;; gnome-shell-mode.el is an emacs interface to gnome-shell 26 | 27 | ;; Put the following in your .emacs to make the gnome-shell-mode function available: 28 | ;; (autoload 'gnome-shell-mode "gnome-shell" "Major mode to edit gnome-shell javascript files" t) 29 | 30 | ;; The latest version of gnome-shell-mode.el can be found at 31 | ;; https://github.com/paperwm/gnome-shell-mode 32 | 33 | ;; Comments / suggestions welcome! 34 | 35 | ;;; History: 36 | 37 | ;; Based on notion-wm-mode which again is loosely based on notion.el by Stefan 38 | ;; Reichör 39 | 40 | ;;; Code: 41 | 42 | (require 'cl) 43 | 44 | (eval-when-compile (require 'rx)) 45 | 46 | (require 'js2-mode) 47 | (require 'dbus) 48 | (require 'flycheck) 49 | (require 'json) 50 | 51 | (defconst gnome-shell--helper-path 52 | (file-name-directory (or load-file-name buffer-file-name))) 53 | 54 | (defvar gnome-shell-dbus-address :session 55 | "The dbus address used for connections (value of DBUS_SESSION_BUS_ADDRESS) 56 | NB: don't set directly, use `gnome-shell-set-dbus-address'") 57 | 58 | (defconst gnome-shell-gjs-documentation-url 59 | "https://people.gnome.org/~gcampagna/docs/") 60 | (defconst gnome-symbol-query-url 61 | "https://developer.gnome.org/symbols/") 62 | 63 | (defun gnome-shell-set-dbus-address (address) 64 | "Set the dbus address. (set to :session for the default bus)" 65 | (interactive "sDBus address: ") 66 | (if (or (equal address "") 67 | (equal address ":session")) 68 | (setq address :session)) 69 | (dbus-init-bus address) 70 | (setq gnome-shell-dbus-address address)) 71 | 72 | (defun gnome-shell--name-at-point () 73 | "Get current Name { ['.'|[|]} Name } sequence." 74 | ;; Taken from lua-mode.el 75 | ;; NB: copying/modifying syntax table for each call may incur a penalty 76 | (with-syntax-table (copy-syntax-table) 77 | (modify-syntax-entry ?. "_") 78 | (modify-syntax-entry ?\[ "_") 79 | (modify-syntax-entry ?\] "_") 80 | (current-word t))) 81 | 82 | (defun gnome-shell-run-interactively (start end insert-result show-result) 83 | "Helper that handles common options relevant for interactive commands" 84 | (let* ((result-obj (gnome-shell-eval (buffer-substring start end))) 85 | (successp (eq (alist-get 'success result-obj) t)) ;; false -> :json-false.. 86 | (result (alist-get 'value result-obj)) 87 | (is-undefined (alist-get 'undefined result-obj)) 88 | ;; The result is already reasonable pretty, but we need to represent 89 | ;; null values 90 | (pp-result (if result result 91 | (if is-undefined "undefined" "null")))) 92 | (when insert-result 93 | (save-excursion 94 | (end-of-line) 95 | (forward-line) 96 | 97 | (gnome-shell--clear-output-at (point)) 98 | 99 | (let ((overlay (make-overlay start (point) nil nil nil))) 100 | (overlay-put overlay 'after-string 101 | (propertize 102 | (concat pp-result "\n") 103 | 'face (list (unless successp 'flyspell-incorrect) 104 | ;; Black background 105 | 'secondary-selection 106 | ;; Default foreground 107 | 'default))) 108 | (overlay-put overlay 'gnome-shell-output t) 109 | (overlay-put overlay 'evaporate t)))) 110 | 111 | (when (or show-result insert-result) 112 | ;; This was an interactive action so we give the user appropriate feedback 113 | 114 | ;; pulse.el is part of cedet. An alternative separate package is 115 | ;; flash-region (don't seem to animate the flash) 116 | (pulse-momentary-highlight-region start end 117 | (if successp 118 | 'diff-refine-added 119 | 'diff-refine-removed)) 120 | 121 | (flycheck-clear) ;; wtf, no way to clear by error id?! 122 | (when (not successp) 123 | ;; Ensure that error occured in the evaled code 124 | ;; If not, we can check the stacktrace for where the failing code was 125 | ;; called 126 | (gnome-shell--flycheck-error result-obj start end))) 127 | 128 | (when show-result 129 | (gnome-shell--show-result result-obj)) 130 | 131 | result)) 132 | 133 | (defun gnome-shell-copy-output () 134 | (interactive) 135 | (save-excursion 136 | (end-of-line) 137 | (let ((overlays (-select (lambda (o) (overlay-get o 'gnome-shell-output)) 138 | (overlays-in (- (point) 1) (point))))) 139 | (kill-new (overlay-get (car overlays) 'after-string))))) 140 | 141 | (defun gnome-shell-clear-output () 142 | (interactive) 143 | (remove-overlays (point-min) (point-max) 'gnome-shell-output t)) 144 | 145 | (defun gnome-shell-clear-output-at-point () 146 | (interactive) 147 | (save-excursion 148 | (end-of-line) 149 | (gnome-shell--clear-output-at (point)))) 150 | 151 | (defun gnome-shell--clear-output-at (p) 152 | (seq-map 'delete-overlay 153 | (-select (lambda (o) (overlay-get o 'gnome-shell-output)) 154 | (overlays-in (- p 1) p)))) 155 | 156 | (defun gnome-shell--flycheck-error (result-obj start end) 157 | (let* ((result (alist-get 'value result-obj)) 158 | (column (alist-get 'columnNumber result-obj)) 159 | (line (alist-get 'lineNumber result-obj)) 160 | (buf-line (+ (line-number-at-pos start) line -1)) ;; 1 vs 0 indexed 161 | (buf-column (+ (save-excursion 162 | (goto-char start) 163 | (current-column)) 164 | column))) 165 | ;; FIXME: When using gnome-shell-repl the flycheck error is cleared for some reason? 166 | (flycheck-add-overlay 167 | (flycheck-error-new-at buf-line buf-column 'error result 168 | :id 'gnome-shell-repl-error)))) 169 | 170 | (defun gnome-shell--show-result (result-obj) 171 | (let* ((successp (eq (alist-get 'success result-obj) t)) 172 | (result (alist-get 'value result-obj)) 173 | (is-undefined (alist-get 'undefined result-obj)) 174 | (pp-result (if result result 175 | (if is-undefined "undefined" "null"))) 176 | (presented-result pp-result) 177 | presented-face) 178 | 179 | (cond ((and successp is-undefined) 180 | (setq presented-face 'success) 181 | (setq presented-result "[No return value]")) 182 | ((not successp) 183 | (setq presented-face 'error) 184 | (when is-undefined 185 | (setq presented-result "[Error without message]")))) 186 | 187 | (message (if presented-face (propertize presented-result 'face presented-face) 188 | presented-result)))) 189 | 190 | (defun gnome-shell--dbus-bootstrap-eval (cmd) 191 | "Function to bootstrap our own Eval" 192 | (dbus-call-method gnome-shell-dbus-address "org.gnome.Shell" "/org/gnome/Shell" 193 | "org.gnome.Shell" "Eval" cmd)) 194 | 195 | (defun gnome-shell--ensure-bootstrap () 196 | (unless (dbus-introspect-get-interface gnome-shell-dbus-address 197 | "org.gnome.Shell" "/gnome/shell/mode" 198 | "gnome.shell.mode") 199 | ;; send init code 200 | (message "sending init code") 201 | (with-temp-buffer 202 | (insert-file-contents 203 | (concat gnome-shell--helper-path "bootstrap.js")) 204 | (gnome-shell--dbus-bootstrap-eval 205 | (concat (buffer-string) "('" gnome-shell--helper-path "')"))))) 206 | 207 | (defun gnome-shell--dbus-call-mode (&rest args) 208 | (apply #'dbus-call-method gnome-shell-dbus-address 209 | "org.gnome.Shell" "/gnome/shell/mode" "gnome.shell.mode" 210 | args)) 211 | 212 | 213 | (defun gnome-shell--dbus-eval (cmd) 214 | "Raw dbus eval call. Returns a list: (success/boolean result/string)" 215 | (gnome-shell--dbus-call-mode "Eval" 216 | cmd 217 | (or (buffer-file-name) ""))) 218 | 219 | (defun gnome-shell--dbus-reload () 220 | "Ask dbus to reload the extension." 221 | (gnome-shell--dbus-call-mode "Reload" 222 | (buffer-string) 223 | (or (buffer-file-name) ""))) 224 | 225 | (defun gnome-shell--dbus-complete (context) 226 | "Ask dbus to reload the extension." 227 | (gnome-shell--ensure-bootstrap) 228 | (gnome-shell--dbus-call-mode "Complete" 229 | context 230 | (or (buffer-file-name) ""))) 231 | 232 | (defun gnome-shell-restart () 233 | "Disable the extension that the current buffer is part of and restart Gnome 234 | Shell afterwards. This can make restarts a bit more controlled as the extension 235 | is given a chance to clean things up etc." 236 | (interactive) 237 | (gnome-shell--ensure-bootstrap) 238 | (gnome-shell--dbus-call-mode "Restart" 239 | (or (buffer-file-name) ""))) 240 | 241 | (defun gnome-shell-reload () 242 | "Reload the current buffer. 243 | 244 | Disables the extension, evaluates the buffer and enables the extension again." 245 | (interactive) 246 | (gnome-shell--ensure-bootstrap) 247 | (let* ((result-obj (destructuring-bind (successp jsonres) 248 | (gnome-shell--dbus-reload) 249 | (json-read-from-string jsonres))) 250 | (successp (eq (alist-get 'success result-obj) t)) 251 | (start (point-min)) 252 | (end (point-max))) 253 | 254 | (pulse-momentary-highlight-region start end 255 | (if successp 256 | 'diff-refine-added 257 | 'diff-refine-removed)) 258 | (unless successp 259 | (gnome-shell--flycheck-error result-obj start end)) 260 | (gnome-shell--show-result result-obj))) 261 | 262 | (defun gnome-shell-eval (code) 263 | "Evaluates `code' in gnome-shell and returns an alist: 264 | 'success : true if no error occured 265 | If success: 266 | 'value : semi-pretty result value 267 | 'raw_value : The raw value serialized to JSON and decoded to lisp equivalent values 268 | If error: 269 | Most properties from Error. Eg. 'message, 'stack, 'lineNumber, 'columnNumber, 270 | 'file" 271 | (gnome-shell--ensure-bootstrap) 272 | 273 | (destructuring-bind (successp jsonres) 274 | (gnome-shell--dbus-eval code) 275 | (json-read-from-string jsonres))) 276 | 277 | (defun gnome-shell-send-region (start end &optional insert-result interactively) 278 | "Send send the region to gnome-shell, using the dbus Eval method." 279 | (interactive "r\nP\np") 280 | (gnome-shell-run-interactively start end insert-result interactively)) 281 | 282 | (defun gnome-shell-send-current-line (&optional insert-result interactively) 283 | "Send send the actual line to gnome-shell, using the dbus Eval method." 284 | (interactive "P\np") 285 | (gnome-shell-run-interactively (line-beginning-position) (line-end-position) 286 | insert-result interactively)) 287 | 288 | (defun gnome-shell-repl () 289 | (interactive) 290 | (let (a b) 291 | (if (region-active-p) 292 | (setq a (min (point) (mark)) 293 | b (max (point) (mark))) 294 | (setq a (line-beginning-position) 295 | b (line-end-position))) 296 | 297 | (save-excursion 298 | (goto-char b) 299 | 300 | (when (eq b (line-beginning-position)) 301 | ;; What I actually want to check for is if the region is active and is 302 | ;; in "line mode". Then b will be at the line _after_ the last code 303 | ;; line selected 304 | 305 | ;; Maybe simply back up all blank lines too? 306 | (forward-line -1)) 307 | 308 | (beginning-of-line) 309 | 310 | (gnome-shell-run-interactively a b t nil)))) 311 | 312 | (defun gnome-shell-send-proc (&optional interactively) 313 | "Send proc around point to gnome-shell." 314 | (interactive "p") 315 | (let (start end) 316 | (save-excursion 317 | (beginning-of-defun) 318 | (setq start (point)) 319 | (end-of-defun) 320 | (setq end (point))) 321 | (gnome-shell-send-region start end nil interactively))) 322 | 323 | (defun gnome-shell-send-buffer (&optional interactively) 324 | "Send send the buffer content to gnome-shell, using the dbus Eval method." 325 | (interactive "p") 326 | (gnome-shell-send-region (point-min) (point-max) nil interactively)) 327 | 328 | 329 | (defun gnome-shell-cmd (cmd &optional insert-result interactively) 330 | "Send a expression to gnome-shell." 331 | (interactive "sGnome shell cmd: \nPp") 332 | ;; FIXME: respect insert-result and interactively arguments 333 | (gnome-shell-eval cmd)) 334 | 335 | (defun gnome-shell--lookup-symbol-candidates (partial-symbol) 336 | ;; Endpoint source: https://git.gnome.org/browse/library-web/tree/web/api.py 337 | (let ((query (concat gnome-symbol-query-url "lookup/" partial-symbol "?"))) 338 | (with-current-buffer (url-retrieve-synchronously query) 339 | (progn 340 | ;; WTF!!.. I have to parse the response myself?! 341 | ;; I don't really have time for that shit, so just assume things went well 342 | ;; Note: Seems the async method at least provides a parsed status.. 343 | (goto-char (point-min)) 344 | (forward-evil-paragraph) 345 | (forward-line) 346 | (let ((lines 347 | (split-string (buffer-substring (point) (point-max)) "\n"))) 348 | (kill-buffer) 349 | lines)))) 350 | ) 351 | 352 | (defun gnome-shell-look-up-function-at-point () 353 | (interactive) 354 | (let* ((funcname (car (last (split-string (gnome-shell--name-at-point) 355 | "\\.")))) 356 | (candidates (gnome-shell--lookup-symbol-candidates funcname)) 357 | (candidate-count (length candidates)) 358 | (selected-candidate)) 359 | ;; (message "candidate-count %s" candidate-count) 360 | ;; (message "funcname %s" funcname) 361 | ;; (message "candidates %s" candidates) 362 | (cond ((= 1 candidate-count) 363 | (setq selected-candidate (first candidates)) 364 | (message "Opening reference page")) 365 | ((>= candidate-count 50) 366 | ;; /symbols/lookup return max 50 results 367 | (message "Too many results, redirect to search page") 368 | (setq selected-candidate funcname)) 369 | ((= 0 candidate-count) 370 | (message "No results")) 371 | (t 372 | (setq selected-candidate (completing-read "Candidates: " candidates)) 373 | (when selected-candidate (message "Opening reference page")))) 374 | (when selected-candidate 375 | (browse-url (concat gnome-symbol-query-url "?q=" selected-candidate)) 376 | ))) 377 | 378 | (defvar gnome-shell--process nil 379 | "The gnome shell process, if running a nested shell") 380 | 381 | (defun gnome-shell--get-extension () 382 | (let* ((buffer (current-buffer)) 383 | (dir (file-name-directory (buffer-file-name buffer)))) 384 | (while (not (or (equal dir "/") 385 | (file-exists-p (concat dir "metadata.json")))) 386 | ;; this doesn't make sense, but returns the parent directory 387 | (setq dir (file-name-directory (directory-file-name dir)))) 388 | (cons (cons 'root dir) 389 | (json-read-file (concat dir "metadata.json"))))) 390 | 391 | 392 | (defun gnome-shell-launch-session (&optional wayland extensions) 393 | "Launch a nested X11/wayland session or show the log if a session is already 394 | running" 395 | (interactive) 396 | (unless (process-live-p gnome-shell--process) 397 | (dolist (err gnome-shell--errors) 398 | (when-let ((filename (flycheck-error-filename err)) 399 | (buffer (find-buffer-visiting filename))) 400 | (with-current-buffer buffer 401 | (flycheck-teardown)))) 402 | (setq gnome-shell--errors nil) 403 | (gnome-shell-set-dbus-address :session) 404 | (let* ((name "gnome-session") 405 | (bus-address nil) 406 | (buffer (create-file-buffer " *gnome-session*")) 407 | (extension (gnome-shell--get-extension)) 408 | (root (alist-get 'root extension)) 409 | (uuid (alist-get 'uuid extension))) 410 | 411 | (setq gnome-shell--process 412 | (start-process 413 | name buffer 414 | (concat gnome-shell--helper-path "session.sh") 415 | "" root uuid)) 416 | 417 | (set-process-filter 418 | gnome-shell--process 419 | (lambda (process string) 420 | (with-current-buffer (process-buffer process) 421 | (let ((scroll nil)) 422 | (save-excursion 423 | (end-of-line) 424 | (setq scroll (eobp)) 425 | (goto-char (point-max)) 426 | (insert string)) 427 | (when scroll 428 | (goto-char (point-max))))) 429 | (when (string-match "unix:abstract" string) 430 | (setq string (substring string 0 (string-match "\n" string))) 431 | (gnome-shell-set-dbus-address string)) 432 | (when (string-match "JS ERROR: " string) 433 | (gnome-shell--flycheck-log process string)))))) 434 | 435 | ;; Always show the log when launching 436 | (gnome-shell-session-log)) 437 | 438 | 439 | (defvar gnome-shell--errors nil 440 | "The gnome shell process, if running a nested shell") 441 | 442 | (defun gnome-shell--add-all-errors () 443 | "Add any existing errors association with buffer's file." 444 | (when (and flycheck-mode 445 | (equal major-mode 'gnome-shell-mode)) 446 | ;; Prevent flycheck from killing errors 447 | (setq flycheck-check-syntax-automatically nil) 448 | (dolist (err gnome-shell--errors) 449 | (when-let ((buffer (find-buffer-visiting (flycheck-error-filename err)))) 450 | (when (equal (current-buffer) buffer) 451 | (setf (flycheck-error-buffer err) (current-buffer)) 452 | (flycheck-report-current-errors (list err))))))) 453 | 454 | (defun gnome-shell--flycheck-log (process string) 455 | (let* ((m (string-match (rx "JS ERROR: " (group (+ nonl)) line-end) string)) 456 | (message (match-string 1 string)) 457 | (next (match-end 0)) 458 | 459 | (loc-regex (rx "@" (group (+? nonl)) ;; filename 460 | ":" (group (+ num)) ;; line nr. 461 | (? ":") (group (* num)) line-end)) ;; col nr. 462 | (m (string-match loc-regex string next)) 463 | (file (match-string 1 string)) 464 | (buffer (find-buffer-visiting file)) 465 | ;; We should always have a line 466 | (line (string-to-number (match-string 2 string))) 467 | ;; But not always a column 468 | (column (when (match-string 3 string) 469 | (string-to-number(match-string 3 string)))) 470 | (err (flycheck-error-new-at 471 | line column 472 | 'error message :filename file 473 | :id 'gnome-shell-log-error))) 474 | (setq gnome-shell--errors (cons err gnome-shell--errors)) 475 | (when buffer 476 | (setf (flycheck-error-buffer err) buffer) 477 | (with-current-buffer buffer 478 | (flycheck-report-current-errors (list err)))))) 479 | 480 | (defun gnome-shell-session-log () 481 | "Show the output of current session" 482 | (interactive) 483 | (pop-to-buffer (process-buffer gnome-shell--process))) 484 | 485 | ;; -------------------------------------------------------------------------------- 486 | ;; The gnome-shell edit mode, based on js2-mode 487 | ;; -------------------------------------------------------------------------------- 488 | 489 | (defvar gnome-shell-mode-map () "Keymap used in `gnome-shell-mode' buffers.") 490 | 491 | (when (not gnome-shell-mode-map) 492 | (setq gnome-shell-mode-map (make-sparse-keymap)) 493 | (define-key gnome-shell-mode-map [(control ?c) (control ?p)] 'gnome-shell-send-proc) 494 | (define-key gnome-shell-mode-map [(control ?c) (control ?r)] 'gnome-shell-send-region) 495 | (define-key gnome-shell-mode-map [(control ?c) (control ?b)] 'gnome-shell-send-buffer) 496 | (define-key gnome-shell-mode-map [(control ?c) (control ?l)] 'gnome-shell-send-line) 497 | (define-key gnome-shell-mode-map (kbd "C-") 'gnome-shell-repl) 498 | ) 499 | 500 | (easy-menu-define gnome-shell-mode-menu gnome-shell-mode-map 501 | "'gnome-shell-mode' menu" 502 | '("Gnome-Shell" 503 | ("Interaction" 504 | ["Send Procedure" gnome-shell-send-proc t] 505 | ["Send Region" gnome-shell-send-region t] 506 | ["Send Buffer" gnome-shell-send-buffer t] 507 | ["Send String" gnome-shell-send-string t] 508 | ["Send Line" gnome-shell-send-line t] 509 | ) 510 | )) 511 | 512 | (define-derived-mode gnome-shell-mode js2-mode "gnome-shell" 513 | "gnome-shell-mode provides tight integration of emacs and gnome-shell. 514 | " 515 | (use-local-map gnome-shell-mode-map) 516 | 517 | (add-hook 518 | 'flycheck-mode-hook 519 | #'gnome-shell--add-all-errors) 520 | 521 | (add-hook 'gnome-shell-mode-hook 522 | #'flycheck-mode)) 523 | 524 | (provide 'gnome-shell-mode) 525 | 526 | ;;; gnome-shell-mode.el ends here 527 | -------------------------------------------------------------------------------- /local/gnome-shell-mode/gnome-shell-mode@hedning:matrix.org/emacs.js: -------------------------------------------------------------------------------- 1 | // -*- mode: gnome-shell; -*- 2 | 3 | const Gio = imports.gi.Gio; 4 | const GLib = imports.gi.GLib; 5 | const GObject = imports.gi.GObject; 6 | const JsParse = imports.misc.jsParse; 7 | 8 | let verbose = false; 9 | 10 | function verboseLog() { 11 | if (verbose) { 12 | log("gnome-shell-mode", ...arguments); 13 | } 14 | } 15 | 16 | /// Add custom printers here indexed by constructor name 17 | // (obj, key) => String or primitive to be serialized further to JSON 18 | // `key` is undefined if top-level object 19 | // IMPROVEMENT: Use a proper map instead and the actual constructors as keys.. 20 | // IMPROVEMENT: What about inheritance? Follow superclasses until a pp is found? 21 | let prettyPrinters = {}; 22 | 23 | function ppListing(list) { 24 | // IMPROVEMENT: use single line for short enough entries 25 | // IMPROVEMENT: what about nested listings.. (accept an indent level) 26 | return "{\n " + list.join(",\n ") + "\n}"; 27 | } 28 | 29 | function ppRect(xywh) { 30 | let [x,y,w,h] = xywh.map( v => v.toFixed(1) ); 31 | return `{ x: ${x}, y: ${y}, w: ${w}, h: ${h} }`; 32 | } 33 | 34 | prettyPrinters["Object"] = function(obj, key) { 35 | if (obj.toString !== Object.prototype.toString) { 36 | // The object have a custom toString method 37 | return obj.toString(); 38 | } else { 39 | // Let JSON handle it 40 | return obj; 41 | } 42 | } 43 | 44 | prettyPrinters["Array"] = function(obj, key) { 45 | // Let JSON handle it 46 | return obj; 47 | } 48 | 49 | 50 | prettyPrinters["Clutter_ActorBox"] = function(box, key) { 51 | let x = box.get_x(); 52 | let y = box.get_y(); 53 | let w = box.get_width(); 54 | let h = box.get_height(); 55 | return `ActorBox ${ppRect([x,y,w,h])}`; 56 | } 57 | 58 | prettyPrinters["Meta_Rectangle"] = function(box, key) { 59 | return `Meta_Rectangle ${ppRect([box.x, box.y, box.width, box.height])}`; 60 | 61 | } 62 | 63 | prettyPrinters["Set"] = function(set, key) { 64 | let members = []; 65 | for (let m of set) { 66 | // WEAKNESS: ppObject doesn't put quotes around strings, so strings and numbers is not distinguishable 67 | members.push(" " + ppObject(m)); 68 | } 69 | return "set " + ppListing(members); 70 | } 71 | 72 | prettyPrinters["Map"] = function(map, key) { 73 | let entries = []; 74 | for (let [k, v] of map) { 75 | // WEAKNESS: ppObject doesn't put quotes around strings, so strings and numbers is not distinguishable 76 | entries.push(`${k.toString()} -> ${ppObject(v)}`); 77 | } 78 | return "map " + ppListing(entries); 79 | } 80 | 81 | function ppObject(obj, key) { 82 | let customPPFn; 83 | if (hasConstuctor(obj)) { 84 | customPPFn = prettyPrinters[obj.constructor.name]; 85 | } 86 | if (customPPFn) { 87 | return customPPFn(obj, key); 88 | } else { 89 | return obj.toString(); 90 | } 91 | } 92 | 93 | 94 | function ppHelper(root) { 95 | let seen = new Map(); 96 | seen.set(root, "") 97 | function cycleDetectingPP(key, obj) { 98 | 99 | if(key === "") { 100 | // obj is the "root object": 101 | // JSON.stringify(X, helper) 102 | // When key is "", obj is X 103 | // Always recurse in this case. 104 | return obj 105 | } 106 | 107 | let type = typeof(obj); 108 | let pretty; 109 | if (type === "undefined") { 110 | pretty = "undefined"; 111 | } else if (obj === null) { 112 | pretty = "null"; 113 | } else if (type === "object") { 114 | let prettyMaybe = ppObject(obj); 115 | 116 | if(typeof(prettyMaybe) === 'object') { 117 | // We allow pretty printers to return a object instead of a 118 | // string to pretty print recursively. 119 | // Normally its only the plain object printer that rely in this. 120 | if (seen.get(obj)) { 121 | pretty = seen.get(obj); 122 | } else { 123 | seen.set(prettyMaybe, prettyMaybe.toString()); 124 | // In the rare case prettyMaybe !== obj : 125 | seen.set(obj, prettyMaybe.toString()); 126 | 127 | // Recursively pretty print. 128 | // Use a separate stringify call (as opposed to simply 129 | // returning the object) so we can cache the result and use 130 | // it if we see the object again. 131 | // 132 | // Note that we can't return the _string_ from the recursive 133 | // stringify call as the parent stringify would escape it. 134 | // By convert back to a plain object we get the wanted 135 | // effect! 136 | let prettyTree = 137 | eval(`(${JSON.stringify(prettyMaybe, cycleDetectingPP)})`); 138 | seen.set(obj, prettyTree) 139 | 140 | pretty = prettyTree; 141 | } 142 | } else { 143 | pretty = prettyMaybe; 144 | } 145 | } else if (type === "function") { 146 | // Just print the whole definition 147 | pretty = obj.toString(); 148 | } else if (type === "string") { 149 | // Could special case string so we're sure it's easier to 150 | // differentiate between a string and a custom string representation 151 | // of an object. Eg. by surrounding it by single quotes. 152 | pretty = obj 153 | } else { 154 | // Let JSON handle it (Numbers, etc.) 155 | pretty = obj; 156 | } 157 | 158 | return pretty; 159 | } 160 | 161 | return cycleDetectingPP; 162 | } 163 | 164 | function hasConstuctor(obj, exactConstructor) { 165 | // gjs "imports objects" fails when evaluation .constructor so we need this 166 | // special check 167 | try { 168 | if(!exactConstructor) { 169 | // Check if there is _any_ constructor at all 170 | return !!obj.constructor 171 | } else { 172 | return exactConstructor === obj.constructor; 173 | } 174 | } catch(e) { 175 | return false; 176 | } 177 | } 178 | 179 | function prettyPrint(obj) { 180 | if (obj !== null && typeof(obj) === "object" 181 | && (hasConstuctor(obj, Object) || hasConstuctor(obj, Array))) 182 | { 183 | // Use JSON.stringify as a poor man's pretty printer for simple 184 | // composite objects 185 | return JSON.stringify(obj, ppHelper(obj)); 186 | } else if(typeof(obj) === "string") { 187 | // A pretty string have quotes around it to not conceal it's true nature 188 | return `"${obj.replace(/"/g, '\\"')}"`; 189 | } else { 190 | // Top level simple or complex constructor 191 | let pretty = ppHelper(obj)(undefined, obj); 192 | if(typeof(pretty) !== "string") { 193 | // Emacs expects a string, even for numbers 194 | pretty = JSON.stringify(pretty); 195 | } 196 | return pretty; 197 | } 198 | } 199 | 200 | /** pathString: absolute path to a js file descending from the extension root */ 201 | function findExtensionRoot(pathString) { 202 | let path = Gio.file_new_for_path(pathString); 203 | let dir = path.get_parent(); 204 | 205 | while (dir !== null) { 206 | let metadata = dir.get_child("metadata.json"); 207 | let jsResource = dir.get_child("js-resources.gresource.xml"); 208 | if (metadata.query_exists(null)) { 209 | return ['extension', dir.get_path()]; 210 | } else if (jsResource.query_exists(null)) { 211 | // Indicate that we're in a the gnome-shell js file 212 | return ['shell', dir.get_path()]; 213 | } 214 | dir = dir.get_parent(); 215 | } 216 | return [null, null]; 217 | } 218 | 219 | /** 220 | * Desctructively apply lines[i].replace(regex, replacement) 221 | */ 222 | function replaceAll(lines, regex, replacement) { 223 | lines.forEach((line, i) => { 224 | lines[i] = lines[i].replace(regex, replacement); 225 | }) 226 | } 227 | 228 | /** 229 | * Go through the ast, building replacements for top level declarations. 230 | * For example `let foo = bar` gets translated to `<{prefix}>foo=bar`. 231 | */ 232 | function parseAndReplace(code, prefix) { 233 | // Let line 0 be the start so the line numbers are aligned with lines indexing 234 | let ast = Reflect.parse(code, {line: 0}); 235 | let lines = code.split('\n'); 236 | let statements = []; 237 | let sourceMap = []; 238 | let linebreaks = 0; 239 | // Loop over all toplevel statements 240 | let length = ast.body.length; 241 | for (let i = 0; i < length; i++) { 242 | let statement = ast.body[i]; 243 | let newStatement; 244 | switch (statement.type) { 245 | case 'VariableDeclaration': 246 | newStatement = variableDeclaration(lines, statement, prefix); 247 | break; 248 | case 'ClassStatement': 249 | case 'FunctionDeclaration': 250 | newStatement = prefix + statement.id.name + ' = '; 251 | newStatement += getStatement(lines, statement); 252 | break; 253 | default: 254 | newStatement = getStatement(lines, statement); 255 | } 256 | // Always add a semicolon to the built statement for safety 257 | newStatement += ';\n'; 258 | 259 | sourceMap.push( 260 | {source: statement.loc.start.line, 261 | sink: statements.length + linebreaks}); 262 | linebreaks += Math.max(0, newStatement.split('\n').length - 1); 263 | statements.push(newStatement); 264 | } 265 | return [statements.join('\n'), sourceMap]; 266 | } 267 | 268 | // Gnome Shell 3.30 removed global.screen, so we're using it here as a way to 269 | // check if we're dealing with spdiermonkey 60 or not. 270 | var getStatement = global.screen ? 271 | getStatementMoz52 : 272 | getStatementMoz60; 273 | 274 | // Spidermonkey 60 fixes most of the weird loc problems 275 | function getStatementMoz60(lines, statement) { 276 | switch (statement.type) { 277 | 278 | case 'FunctionExpression': 279 | var a = statement.async ? 'async ' : ''; 280 | return `${a}function ${span(lines, statement.loc)}`; 281 | 282 | case 'FunctionDeclaration': 283 | var g = statement.generator ? '*' : ''; 284 | var a = statement.async ? 'async ' : ''; 285 | return `${a}function${g} ${span(lines, statement.loc)}`; 286 | 287 | case 'ArrowFunctionExpression': 288 | // statement.loc produces things like `= () => {}` 289 | var params = statement.params 290 | .map(p => getStatement(lines, p)).toString(); 291 | var body = getStatement(lines, statement.body); 292 | return `(${params}) => ${body}`; 293 | 294 | default: 295 | return span(lines, statement.loc); 296 | } 297 | } 298 | 299 | /** 300 | Workarounds for the somewhat weird locs Reflect.parse returns in spidermonkey 301 | 52, mostly due to block statements not including their closing bracket. 302 | */ 303 | function getStatementMoz52(lines, statement) { 304 | switch (statement.type) { 305 | case 'BlockStatement': 306 | // Block AST nodes omits the ending '}' 307 | var block = `${span(lines, statement.loc)} }`; 308 | if (statement.body[-1].type === 'BlockStatement') { 309 | // Omission of ending '}' "nests" (but only one level..) 310 | return block + " }" 311 | } 312 | return block; 313 | 314 | case 'IfStatement': 315 | var test = getStatement(lines, statement.test); 316 | var consquence = getStatement(lines, statement.consequent); 317 | var alternate = statement.alternate !== null ? 318 | `else ${getStatement(lines, statement.alternate)}` : ""; 319 | return `if (${test}) ${consquence} ${alternate}`; 320 | 321 | case 'WithStatement': 322 | var object = getStatement(lines, statement.object); 323 | var body = getStatement(lines, statement.body); 324 | return `with (${object}) ${body}`; 325 | 326 | case 'WhileStatement': 327 | var test = getStatement(lines, statement.test); 328 | var body = getStatement(lines, statement.body); 329 | return `while (${test}) ${body}`; 330 | 331 | case 'ForStatement': 332 | var test = statement.test !== null ? 333 | getStatement(lines, statement.test) : ""; 334 | var init = statement.init ? 335 | getStatement(lines, statement.init) : ""; 336 | var update = statement.update ? 337 | getStatement(lines, statement.update) : ""; 338 | var body = getStatement(lines, statement.body); 339 | return `for (${init}; ${test}; ${update}) ${body}`; 340 | 341 | case 'ForInStatement': 342 | var left = getStatement(lines, statement.left); 343 | var right = getStatement(lines, statement.right); 344 | var body = getStatement(lines, statement.body); 345 | return `for (${left} in ${right}) ${body}`; 346 | 347 | case 'ForOfStatement': 348 | var left = getStatement(lines, statement.left); 349 | var right = getStatement(lines, statement.right); 350 | var body = getStatement(lines, statement.body); 351 | return `for (${left} of ${right}) ${body}`; 352 | 353 | case 'FunctionExpression': 354 | var a = statement.async ? 'async ' : ''; 355 | return `${a}function ${span(lines, statement.loc)}`; 356 | 357 | case 'FunctionDeclaration': 358 | var g = (statement.generator && !statement.async) ? '*' : ''; 359 | var a = statement.async ? 'async ' : ''; 360 | return `${a}function${g} ${span(lines, statement.loc)}`; 361 | 362 | case 'ClassExpression': 363 | case 'ClassStatement': 364 | return `class ${span(lines, statement.loc)} }`; 365 | 366 | case 'ArrowFunctionExpression': 367 | // statement.loc produces things like `= () => {}` 368 | var params = statement.params 369 | .map(p => getStatement(lines, p)).toString(); 370 | var body = getStatement(lines, statement.body); 371 | return `(${params}) => ${body}`; 372 | 373 | case 'UpdateExpression': 374 | // ++/-- isn't included in statement.loc 375 | var argument = getStatement(lines, statement.argument); 376 | if (statement.prefix) 377 | return `${statement.operator}${argument}`; 378 | else 379 | return `${argument}${statement.operator}`; 380 | 381 | default: 382 | return span(lines, statement.loc); 383 | } 384 | } 385 | 386 | /** 387 | * Retrieve a span from lines using loc.start: {line: .., column: ..} syntax 388 | */ 389 | function span(lines, loc) { 390 | let start = loc.start; 391 | let end = loc.end; 392 | let slice = lines.slice(start.line, end.line + 1); 393 | if (start.line === end.line) { 394 | slice[0] = slice[0].substring(start.column, end.column); 395 | } else { 396 | slice[0] = slice[0].substring(start.column); 397 | slice[slice.length-1] = slice[slice.length-1].substring(0, end.column); 398 | } 399 | return slice.join('\n'); 400 | } 401 | 402 | function variableDeclaration (lines, statement, prefix) { 403 | let replacement = ''; 404 | for (let declaration of statement.declarations) { 405 | replacement += '('; 406 | replacement += pattern(lines, declaration.id, prefix); 407 | 408 | let init = declaration.init; 409 | if (init) { 410 | replacement += `= ${getStatement(lines, init)}`; 411 | } else { 412 | // Handle cases like 'let foo' 413 | replacement += `= ${prefix}${declaration.id.name}`; 414 | } 415 | replacement += ')'; 416 | replacement += ','; 417 | } 418 | replacement = replacement.replace(/,$/, ''); 419 | return replacement; 420 | } 421 | 422 | // Rebuild a pattern, prefixing when appropriate. 423 | function pattern(lines, ptrn, prefix) { 424 | if (!ptrn) 425 | return ''; 426 | switch (ptrn.type) { 427 | case 'Identifier': 428 | return prefix + ptrn.name; 429 | case 'ArrayPattern': 430 | return arrayPattern(lines, ptrn, prefix); 431 | case 'ObjectPattern': 432 | return objectPattern(lines, ptrn, prefix); 433 | case 'AssignmentExpression': 434 | return pattern(lines, ptrn.left, prefix) + ptrn.operator 435 | + pattern(lines, ptrn.right, prefix); 436 | default: 437 | return span(lines, ptrn.loc); 438 | } 439 | } 440 | 441 | function arrayPattern(lines, arraypattern, prefix) { 442 | let replacement = '['; 443 | for (let element of arraypattern.elements) { 444 | replacement += pattern(lines, element, prefix); 445 | replacement += ','; 446 | } 447 | replacement = replacement.replace(/,$/, ']'); 448 | return replacement; 449 | } 450 | 451 | function objectPattern(lines, objpattern, prefix) { 452 | let replacement = '{'; 453 | for (let property of objpattern.properties) { 454 | replacement += property.key.name + ':'; 455 | replacement += pattern(lines, property.value, prefix); 456 | replacement += ','; 457 | } 458 | replacement = replacement.replace(/,$/, '}'); 459 | return replacement; 460 | } 461 | 462 | function mapLine(sourceMap, line) { 463 | let i = sourceMap.length-1; 464 | while (i > 0 && sourceMap[i].sink > line) { 465 | i--; 466 | } 467 | return sourceMap[i].source + (line - sourceMap[i].sink); 468 | } 469 | 470 | let fileScopes = {}; 471 | function findScope(path) { 472 | let scope; 473 | try { 474 | // (We try in case the module has syntax errors) 475 | scope = findModule(path); 476 | } catch(e) { 477 | scope = null; 478 | print(`Couldn't load module, fall back to default scope: ${e.message}`) 479 | } 480 | 481 | // Create a new scope, indexed by the path 482 | if (scope === null) { 483 | if (!fileScopes[path]) 484 | fileScopes[path] = {}; 485 | scope = fileScopes[path]; 486 | } 487 | return scope; 488 | } 489 | 490 | function Eval(code, path) { 491 | emacs.module = findScope(path); 492 | 493 | let sourceMap; 494 | let evalResult; 495 | let result; 496 | let success = true; 497 | try { 498 | try { 499 | [code, sourceMap] = parseAndReplace(code, 'emacs.module.'); 500 | } catch(e) { 501 | // Let eval take care of syntax errors too 502 | } 503 | evalResult = (0, eval)(`with(emacs.module){ ${code} }`); 504 | window.$ = evalResult; 505 | 506 | result = { 507 | success: true, 508 | }; 509 | 510 | try { 511 | result.value = prettyPrint(evalResult) 512 | } catch(e) { 513 | result.value = "Error during pretty printing: " + e.message; 514 | } 515 | 516 | } catch(e) { 517 | window.$ = e; 518 | 519 | if (sourceMap) { 520 | // lineNumber is one indexed, and sourceMap expect zero indexing 521 | // it also returns a zero indexed line, so we need to add 1. 522 | e.lineNumber = mapLine(sourceMap, e.lineNumber - 1) + 1; 523 | } 524 | result = { 525 | success: false, 526 | value: e.message, 527 | stack: e.stack, 528 | lineNumber: e.lineNumber - emacs.eval_line_offset, 529 | columnNumber: e.columnNumber, 530 | // e.constructor 531 | file: e.file 532 | } 533 | 534 | success = false; 535 | } 536 | return [success, JSON.stringify(result)]; 537 | } 538 | 539 | /** 540 | * Reload the the path by disabling the extension, re-evaluate the code in 541 | * the path and enable the extension again. 542 | */ 543 | function Reload(code, path) { 544 | // Make sure that we're in an ext 545 | let [type, extensionImports, root] = findExtensionImports(path); 546 | 547 | if (type !== 'extension') { 548 | return [false, 'Not in a valid extension']; 549 | } 550 | 551 | // Disable the extension 552 | extensionImports.extension.disable(); 553 | 554 | // Reload the code 555 | const [evalSuccess, result] = Eval(code, path); 556 | // Enable the extension again 557 | extensionImports.extension.enable(); 558 | return [evalSuccess, result]; 559 | } 560 | 561 | /** 562 | Run extension.disable and then restart Gnome Shell 563 | */ 564 | function Restart(path) { 565 | let [type, extensionImports, _] = findExtensionImports(path); 566 | if (type !== 'extension') 567 | return; 568 | extensionImports.extension.disable(); 569 | imports.gi.Meta.restart( 570 | `Restarting (disabled ${extensionImports.__moduleName__} first)`); 571 | } 572 | 573 | function findModule(path) { 574 | let [type, extensionImports, projectRoot] = findExtensionImports(path); 575 | 576 | // (projectRoot does not end with slash) 577 | let relPath = path.slice(projectRoot.length+1); 578 | 579 | // Find the module object we're in 580 | if (relPath.endsWith('.js')) { 581 | relPath = relPath.substring(0, relPath.length - 3); 582 | return relPath.split('/').reduce((module, name) => { 583 | if (module[name]) { 584 | return module[name]; 585 | } 586 | return empty; 587 | }, extensionImports); 588 | } else { 589 | return null; 590 | } 591 | } 592 | 593 | function findExtensionImports(path) { 594 | let [type, projectRoot] = findExtensionRoot(path); 595 | if (projectRoot === null || type === null) { 596 | return [null, null, null]; 597 | } 598 | 599 | if (type === 'extension') { 600 | let extension = findExtension(projectRoot); 601 | if (extension) { 602 | return [type, extension.imports, projectRoot]; 603 | } 604 | } else if (type === 'shell') { 605 | return [type, imports, projectRoot]; 606 | } 607 | } 608 | 609 | function findExtension(projectRoot) { 610 | let metadataFile = `${projectRoot}/metadata.json`; 611 | if (GLib.file_test(metadataFile, GLib.FileTest.IS_REGULAR)) { 612 | const [success, metadata] = GLib.file_get_contents(metadataFile); 613 | let uuid = JSON.parse(metadata.toString()).uuid; 614 | if (uuid === undefined) 615 | return false; 616 | if (imports.misc.extensionUtils.extensions) { 617 | return imports.misc.extensionUtils.extensions[uuid]; 618 | } else { 619 | return imports.ui.main.extensionManager.lookup(uuid); 620 | } 621 | } 622 | } 623 | 624 | function getGlobalCompletionsAndKeywords() { 625 | const keywords = ['true', 'false', 'null', 'new', 'typeof', 'function', 626 | 'throw', 'catch', 'try', 'const', 'let', 'var']; 627 | const windowProperties = Object.getOwnPropertyNames(window); 628 | 629 | return keywords.concat(windowProperties); 630 | } 631 | 632 | /** 633 | * Find the suffix of text that makes most sense as completion context 634 | * Eg. "foo(bar.b" -> "bar.b" 635 | */ 636 | function findExpressionToComplete(text) { 637 | const begin = JsParse.getExpressionOffset(text, text.length-1); 638 | if (begin < 0) { 639 | return null; 640 | } 641 | 642 | return text.slice(begin) 643 | } 644 | 645 | /** 646 | * "foo[0].bar.ba" -> ["foo[0].bar", "ba"] 647 | * "fooo" -> [null, "fooo"] 648 | */ 649 | function splitIntoBaseAndHead(expr) { 650 | let base = null, attrHead = null; 651 | // Look for expressions like "Main.panel.foo" and match Main.panel and foo 652 | let matches = expr.match(/(.+)\.(.*)/); 653 | if (matches) { 654 | [base, attrHead] = matches.slice(1); // (first item is whole match) 655 | } else { 656 | attrHead = expr; 657 | } 658 | 659 | return [base, attrHead] 660 | } 661 | 662 | function isGObject(object) { 663 | try { 664 | // NB: not a 100% sure this test is accurate 665 | return object && typeof(object) === 'object' && object.__metaclass__; 666 | } catch(e) { 667 | // GjsFileImporter (eg. `imports`) throw when accessing non-existent property 668 | return false; 669 | } 670 | } 671 | 672 | function completionCandidates(text, path) { 673 | let completions = []; 674 | 675 | let scope = findScope(path); 676 | 677 | let expr = findExpressionToComplete(text); // Note: In emacs atm. `text` will usually equal `expr` 678 | if (expr === null) { 679 | verboseLog("Trying to complete invalid expression", text) 680 | return []; 681 | } 682 | 683 | let [base, attrHead] = splitIntoBaseAndHead(expr); 684 | 685 | if (base === null) { 686 | // Complete scope variables, global variables and keywords 687 | completions = completions.concat( 688 | getGlobalCompletionsAndKeywords(), 689 | JsParse.getAllProps(scope) 690 | ); 691 | } else { 692 | // Need to evaluate the owner of `attrHead` 693 | if (JsParse.isUnsafeExpression(base)) { 694 | verboseLog("Unsafe expr", base, attrHead); 695 | return []; 696 | } 697 | 698 | emacs.module = scope; 699 | let baseObj = null; 700 | try { 701 | baseObj = (0, eval)(`with(emacs.module) { ${base} }`); 702 | } catch(e) { 703 | // Some objects, eg. `imports`, throw when trying to access missing 704 | // properties. (Completing `imports.not_exist.foo` will throw) 705 | verboseLog("Failed to eval base", base); 706 | return []; 707 | } 708 | 709 | if (baseObj === null || baseObj === undefined) { 710 | return []; 711 | } 712 | 713 | completions = completions.concat(JsParse.getAllProps(baseObj)); 714 | 715 | if (Object.getPrototypeOf(baseObj).constructor === imports.gi.GIRepository.constructor) { 716 | const repo = imports.gi.GIRepository.Repository.get_default(); 717 | const namespace = baseObj.__name__; 718 | const n = repo.get_n_infos(namespace); 719 | for (let i = 0; i < n; i++) { 720 | completions.push(repo.get_info(namespace, i).get_name()); 721 | } 722 | } 723 | 724 | if (isGObject(baseObj)) { 725 | // NB: emacs.list_properties.call(x) crashes gnome-shell when x is a 726 | // (non-empty) string or number 727 | emacs.list_properties.call(baseObj) 728 | .forEach((x) => { 729 | // list_properties gives names with "-" not "_" 730 | completions.push(x.name.replace(/-/g, "_")) 731 | }); 732 | } 733 | } 734 | 735 | return completions.filter((x) => 736 | typeof(x) === 'string' && 737 | x.startsWith(attrHead) && 738 | JsParse.isValidPropertyName(x)); 739 | }; 740 | --------------------------------------------------------------------------------