Using Emacs for Javascript development

PDF (drucken)

When I started into web development at work, I took the chance to do the work with Emacs, because different from Java, there are no ultimate tools that offer reliable capabilities that are required for work but are not provided sufficiently well in Emacs.

I build on js2-mode, js2-refactor-mode, company-mode, and dumb-jump.

For a basic setup, see Setting up Emacs for JavaScript (part 1 and part 2).

However we’re using a nice advanced Javascript environment that provides fledging features that are not yet in js2-mode, so I need some adaptions.

private class fields

Private class fields are currently in stage 3. I added an issue and a pull-request to support them in js2-mode, but since they are not yet in the final specification, these won’t be pulled.

Therefore I patch them in using advice-add.

(advice-add #'js2-identifier-start-p
          (lambda (c) (eq c ?#)))

There are warnings about undeclared private class fields (since it does not catch their definition), but no errors.


I also add customElements as defined externs:

(js2-global-externs '("customElements"))

Final setup with use-package

This is the Javascript-related part of my work-setup for Emacs. I extracted it from my init.el, but it still might be missing parts.

(use-package js2-mode :ensure t :defer 20
  (("\\.js\\'" . js2-mode))
  (js2-include-node-externs t)
  (js2-global-externs '("customElements"))
  (js2-highlight-level 3)
  (js2r-prefer-let-over-var t)
  (js2r-prefered-quote-type 2)
  (js-indent-align-list-continuation t)
  (global-auto-highlight-symbol-mode t)
  (setq js-indent-level 2)
  ;; patch in basic private field support
  (advice-add #'js2-identifier-start-p
            (lambda (c) (eq c ?#))))

(use-package projectile :ensure t :defer 1
  (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
  (bind-key "C-c p s" 'projectile-ripgrep)
  (setq projectile-sort-order 'modification-time))

(use-package which-key :ensure t

(use-package company :ensure t :defer 20
  ;; This is not perfect yet. It completes too quickly outside programming modes, but while programming it is just right.
  (company-idle-delay 0.1)
  (global-company-mode t)
  (debug-on-error nil) ;; otherwise this throws lots of errors on completion errors
  (define-key company-active-map (kbd "TAB") 'company-complete-selection)
  (define-key company-active-map (kbd "<tab>") 'company-complete-selection)
  (define-key company-active-map [return] 'company-complete-selection)
  (define-key company-active-map (kbd "RET") 'company-complete-selection)
  ;; auto-complete compatibility
  (defun my-company-visible-and-explicit-action-p ()
    (and (company-tooltip-visible-p)
  (defun company-ac-setup ()
    "Sets up `company-mode' to behave similarly to `auto-complete-mode'."
    (setq company-require-match nil)
    (setq company-auto-complete #'my-company-visible-and-explicit-action-p)
    (setq company-frontends '(company-echo-metadata-frontend
    (define-key company-active-map [tab]
    (define-key company-active-map (kbd "TAB")

  (add-hook 'js2-mode-hook (lambda () (company-mode))))

(use-package company-quickhelp :ensure t :defer 30
  (company-quickhelp-mode t))

(use-package dumb-jump :ensure t :defer 10
  (dumb-jump-rg-search-args '())
  (defun jump-to-mouse-position (event &optional promote-to-region)
    (interactive "e\np")
    (mouse-set-point event promote-to-region)
  (global-unset-key [C-down-mouse-1])
  (define-key global-map [C-mouse-1] 'jump-to-mouse-position))

;; Highlight TODO, FIXME, ... in any programming mode
(require 'fic-mode)
(add-hook 'prog-mode-hook 'fic-mode)

(use-package flymake-eslint :ensure t :defer 10
  :custom ;; add glasses-mode to bolden capitals in CamelCase here. Could also be done elsewhere.
  (glasses-face (quote bold))
  (glasses-original-separator "")
  (glasses-separate-capital-groups t)
  (glasses-separate-parentheses-p nil)
  (glasses-separator "")
  (add-hook 'js-mode-hook (lambda () (flymake-eslint-enable)(flymake-mode -1)(flycheck-mode 1)(glasses-mode 1)))
  (add-hook 'js2-mode-hook (lambda () (flymake-eslint-enable)(flymake-mode -1)(flycheck-mode 1)(glasses-mode 1)))
     '(help-at-pt-timer-delay 0.3)
     '(help-at-pt-display-when-idle '(flymake-overlay))))
(use-package flymake-diagnostic-at-point :ensure t :defer 20
  (flymake-diagnostic-at-point-mode t))

(use-package tern :ensure t :defer 30
  :if (locate-file "tern" exec-path)
  :hook (js2-mode . tern-mode))
(use-package json-mode :ensure t :defer 20
  (json-reformat:indent-width 2)
  :mode (("\\.bowerrc$"     . json-mode)
         ("\\.jshintrc$"    . json-mode)
         ("\\.json_schema$" . json-mode)
         ("\\.json\\'" . json-mode))
  :bind (:package json-mode-map
         :map json-mode-map
         ("C-c <tab>" . json-mode-beautify)))

(use-package company-tern :ensure t :defer 30
  (add-to-list 'company-backends 'company-tern)
  (define-key tern-mode-keymap (kbd "M-.") nil)
  (define-key tern-mode-keymap (kbd "M-,") nil))

(use-package js2-refactor :ensure t :defer 30
  (add-hook 'js2-mode-hook #'js2-refactor-mode)
  (js2r-add-keybindings-with-prefix "C-c C-m"))
;; context menu for keybindings
(use-package discover :ensure t :defer 30
  (global-discover-mode 1))

;; ido-preview
(use-package ido-preview ;; no need to ensure: it is part of emacs
  (add-hook 'ido-setup-hook
              (define-key ido-completion-map (kbd "C-M-p") (lookup-key ido-completion-map (kbd "C-p")))
              (define-key ido-completion-map (kbd "C-M-n") (lookup-key ido-completion-map (kbd "C-n"))) ; currently, this makes nothing. Maybe they'll make C-n key lately.
              (define-key ido-completion-map (kbd "C-p") 'ido-preview-backward)
              (define-key ido-completion-map (kbd "C-n") 'ido-preview-forward)))
  (defun rebuild-resources (folder)
    "Rebuild resources and generate code in the to-be-selected module."
       (list (ido-read-directory-name
              "Select module: " "~/Cadenza/cadenza-master/cadenza"))
    (async-shell-command (concat "bash -i -c 'cd " folder "; source ~/.bashrc; cd " folder "; Xvfb :3 -screen 0 1024x768x16 & time DISPLAY=:3 LD_LIBRARY_PATH=$HOME/.guix-profile/lib mvn11 generate-sources process-resources install validate -DskipTests=true'")))
  (ido-buffer-disable-smart-matches nil)
  (ido-cr+-auto-update-blacklist t)
  (ido-cr+-function-whitelist nil)
  (ido-cr+-max-items 30000)
  (ido-cr+-replace-completely nil)
  (ido-enable-dot-prefix t)
  (ido-enable-flex-matching t)
  (ido-everywhere t)
  (ido-max-work-file-list 10)
  (ido-mode (quote both) nil (ido))
  (ido-ubiquitous-mode t)
  (ido-use-filename-at-point (quote guess))
  (ido-use-url-at-point t))

(use-package tabbar :ensure t)

(use-package rainbow-identifiers :ensure t
  (defun rainbow-identifiers--bolden-faces ()
    (dotimes (i 15) ;; TODO: use number of faces as customized
      (face-remap-add-relative (intern (format "rainbow-identifiers-identifier-%d" (1+ i))) :weight 'bold)))
  (add-hook 'rainbow-identifiers-mode-hook 'rainbow-identifiers--bolden-faces)


For quick prototypes skewer-mode is pretty nice.

You can reload specific CSS rules and HTML tags with C-M-x and you have a live REPL, so you can tell the browser to reload the whole page from Emacs when needed.

Setup is simple: Just run M-x run-skewer to launch the server and then M-x skewer-repl to get the REPL for live javascript invocation. The REPL is still a bit brittle — I need to reload the website when it breaks — but updating changed parts of the site in the live browser-tab is beautiful.

And it already works really well for CSS development: Just hit C-M-x inside the script tag you just changed to update the style in the browser.

Screenshot of Skewer-mode for writing a website template

(click for full size)

As you see in the screenshot, I used M-x customize-variable http-port to change the skewer port to 9876, because port 8080 interfered with too many other services.

ArneBab 2020-04-23 Do 00:00 - Impressum - GPLv3 or later (code), cc by-sa (rest)