Emacs as a Multi Mode Web Dev IDE is Now Possible

Through web-mode the possibility of using Emacs as a web dev IDE with full multi mode capabilities a la Aptana Studio or PHPStorm is finally here. But it will take some tweaking to get things perfect so read on…

Note that various modifications that change appearances in various ways are currently breaking the syntax highlighting of web-mode, they include idle highligt and show whitespace. If you’re using Prelude or the Starter Kit you will run into problems at the time of writing.

Let’s begin with my package related lines:

(require 'package)
(add-to-list 'package-archives
             '("marmalade" . "http://marmalade-repo.org/packages/") t)

(package-initialize)

(defvar my-packages '(better-defaults paredit clojure-mode nrepl zenburn-theme ecb auto-complete exec-path-from-shell js2-mode yasnippet autopair flx-ido flymake))

(dolist (p my-packages)
  (when (not (package-installed-p p))
    (package-install p)))

Note that web-mode is not in the list, that’s because I’ve chosen to use the bleeding edge so I’ve downloaded it and included it manually by way of (add-to-list ‘load-path “/home/henrik/.emacs.d/henrik”) and then (require ‘web-mode).

We need to be able to easily jump to PHP documentation and definitions:

(defun get-current-word ()
  (if (and transient-mark-mode mark-active)
      (buffer-substring-no-properties (region-beginning) (region-end))
    (thing-at-point 'word)))

(defun jump-to-current-word ()
  (interactive)
  (find-tag (car (last (delq "" (split-string (get-current-word) "\\b"))))))

(defun php-jump ()
  (interactive)
  (let (myword myurl)
    (setq myword
          (if (and transient-mark-mode mark-active)
              (buffer-substring-no-properties (region-beginning) (region-end))
            (thing-at-point 'symbol)))
    (setq myurl (concat "http://php.net/manual/en/function." (replace-regexp-in-string "_" "-" myword) ".php"))
    (browse-url myurl)))

Not much to say, when the key combo we have assigned to php-jump is pressed we open the documentation for the function the cursor is currently on.

ECB is handy:

(setq ecb-tree-buffer-style 'ascii-guides)
(setq ecb-tip-of-the-day nil)
(setq ecb-layout-name "left15")
(setq my-projects '("/var/www/proj1" "/var/www/proj2"))
(setq ecb-source-path my-projects)

So is auto complete:

(require 'auto-complete)
(require 'auto-complete-config)
(ac-config-default)
(setq ac-auto-start 3)
(add-to-list 'ac-modes 'web-mode)
(setq ac-ignore-case t)
(setq ac-auto-start nil)
(global-set-key (kbd "C-<tab>") 'auto-complete)

Some nice custom shortcuts and variables:

(global-set-key [f11] 'switch-full-screen)
(global-set-key [f5] 'refresh-file)
(global-set-key (kbd "C-z") 'undo)
(global-set-key (kbd "M-.") 'jump-to-current-word)
(global-set-key [f1] 'menu-bar-mode)
(global-set-key [f6] 'split-window-horizontally)
(global-set-key [f7] 'split-window-vertically)
(global-set-key [f8] 'delete-window)
(global-set-key (kbd "C-c c")        'comment-region)
(global-set-key (kbd "C-c C-u c")    'uncomment-region)
(global-set-key (kbd "C-x C-b") 'buffer-menu)
(global-set-key (kbd "M-x") 'smex)

(eval-after-load 'web-mode '(define-key web-mode-map (kbd "s-d") 'php-jump))

(custom-set-variables
 '(ecb-layout-window-sizes (quote (("left15" (ecb-directories-buffer-name 0.10830324909747292 . 0.4864864864864865) (ecb-methods-buffer-name 0.10830324909747292 . 0.5)))))
 '(ecb-options-version "2.40")
 '(js2-basic-offset 2)
 '(js2-bounce-indent-p t))

(add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))

(ecb-activate)

Note the s-d combo for jumping to the PHP documentation, that is Super (AKA the windows key)-d. Note also that I’m using js2-mode for pure javascript files, not web-mode.

Yasnippet is a must with all the goodies from php-mode moved to the web-mode folder :)

(require 'yasnippet)
(yas-global-mode 1)

;;js2-mode steals TAB, let's steal it back for yasnippet
(defun js2-tab-properly ()
  (interactive)
  (let ((yas/fallback-behavior 'return-nil))
    (unless (yas/expand)
      (indent-for-tab-command)
      (if (looking-back "^\s*")
          (back-to-indentation)))))

(eval-after-load 'js2-mode
  '(define-key js2-mode-map (kbd "TAB") 'js2-tab-properly))

We need autopair enabled for web-mode and js2-mode, we also need flx-ido for projectile. The handiest projectile features are C-c p f for find file in project, lovely. We also have C-c p g for grep project, also great. The flx-ido related lines are needed to setup projectile properly. Finally we enable frame switching by way of Shift - arrow keys.

(require 'autopair)
(autopair-global-mode) ;; enable autopair in all buffers
(add-hook 'js2-mode-hook 'autopair-mode)
(set-cursor-color "#aaaaaa") 

(require 'flx-ido)
(ido-mode 1)
(ido-everywhere 1)
(flx-ido-mode 1)
;; disable ido faces to see flx highlights.
(setq ido-use-faces nil)

(require 'projectile)
(projectile-global-mode)

(require 'web-mode) 
(add-hook 'web-mode-hook 'autopair-mode)
(add-to-list 'auto-mode-alist '("\\.php$" . web-mode))

(windmove-default-keybindings)

Syntax errors need to be nipped in the bud:

(require 'flymake)
(defun flymake-php-init ()
  "Use php to check the syntax of the current file."
  (let* ((temp (flymake-init-create-temp-buffer-copy 'flymake-create-temp-inplace))
	 (local (file-relative-name temp (file-name-directory buffer-file-name))))
    (list "php" (list "-f" local "-l"))))
(add-to-list 'flymake-err-line-patterns 
  '("\\(Parse\\|Fatal\\) error: +\\(.*?\\) in \\(.*?\\) on line \\([0-9]+\\)$" 3 4 nil 2))
(add-to-list 'flymake-allowed-file-name-masks '("\\.php$" flymake-php-init))
(add-hook 'web-mode-hook (lambda () (flymake-mode 1)))

(electric-indent-mode 1)

Electric indent mode is also a given.

Displaying the full path to the current file in the title frame can be handy. So can easy access to scrolling buffers up and down without moving the cursor:

(setq uniquify-buffer-name-style 'reverse)
(setq inhibit-default-init t)
(setq-default frame-title-format "%b (%f)")

(global-set-key "\M-n" "\C-u1\C-v")
(global-set-key "\M-p" "\C-u1\M-v")

Moving lines up and down like in Aptana Studio:

(defun move-line-down ()
  (interactive)
  (let ((col (current-column)))
    (save-excursion
      (forward-line)
      (transpose-lines 1))
    (forward-line)
    (move-to-column col)))

(defun move-line-up ()
  (interactive)
  (let ((col (current-column)))
    (save-excursion
      (forward-line)
      (transpose-lines -1))
    (move-to-column col)))

(eval-after-load 'web-mode '(define-key web-mode-map (kbd "<M-down>") 'move-line-down))
(eval-after-load 'web-mode '(define-key web-mode-map (kbd "<M-up>") 'move-line-up))

Finally a creation of my own:

(defun newline-and-indent-as-above ()
  (interactive)
  (let* ((cline (thing-at-point 'line))
         (start (string-match "\\`[ \t]+" cline))
         (end (match-end 0))
         (indent (substring cline start end )))
    (newline)
    (insert indent)))

(global-set-key (kbd "<C-return>") 'newline-and-indent-as-above)

This one takes some explaining. When Ctrl-Enter is pressed it will create a new line at point and indent the new line exactly like the one above. This makes it easy to indent big multi-line SQL statements in a good looking way, like this:

function foo($where, $date){
  $sql_str = "SELECT * FROM table 
    WHERE created_at > '$date'
    $where
    LIMIT 0,50";
}

Finally don’t forget to generate TAGS files with exuberant etags like this in your project folder(s):

ctags-exuberant -e -R --languages=php

Without the TAGS files code jumping by way of M-. won’t work. Autocomplete will also have a hard time.

Related Posts

Tags: , , ,