This is my Emacs configuration file. It’s built using org-mode. GitHub can render org files and I’ve included a symlink README.org -> config.org so GitHub renders my config directly on the repository page.
This is a work-in-progress and I’m trying to adopt Emacs after a relatively long time using Neovim. Both are great projects and I’m having a difficult time abandoning either for the other, so you should check them out. I made a blog post about switching from Neovim to Emacs. Maybe you’ll find it interesting, as it narrates the creation of this file.
(setq user-full-name "Fausto Núñez Alberro")
(setq user-mail-address "fausto.nunez@mailbox.org")
(load "package")
(add-to-list 'package-archives
'("marmalade" . "http://marmalade-repo.org/packages/"))
(add-to-list 'package-archives
'("melpa" . "http://melpa.milkbox.net/packages/") t)
(add-to-list 'package-archives
'("org" . "http://orgmode.org/elpa/") t)
(package-initialize)
With this EDITOR env variable, Emacs can lazy start the server and connect to it if it exists. My git configuration uses the global $EDITOR too.
export EDITOR='emacsclient -nw -c -a ""'
At the end of the config file we include some hooks to reload eyecandy that gets reset when connecting a client to the server.
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(require 'use-package)
(setq use-package-always-ensure t)
(setq my-theme 'nord)
(use-package nord-theme
:ensure t)
(load-theme my-theme t)
(use-package powerline
:ensure t
:config (powerline-center-evil-theme))
(menu-bar-mode -1)
(scroll-bar-mode 0)
(tool-bar-mode 0)
(defalias 'yes-or-no-p 'y-or-n-p)
(global-linum-mode 1)
(setq linum-format " %4d ")
hlinum-mode highlights the current line for linum
(use-package hlinum
:ensure t)
(set-face-foreground 'linum-highlight-face "white")
(set-face-background 'linum-highlight-face nil)
(hlinum-activate)
(line-number-mode 1)
(column-number-mode 1)
(setq inhibit-startup-message t)
(setq initial-scratch-message nil)
I find it annoying because it makes it hard to find the cursor after it appears. Solution found on this StackOverflow question.
(defun my-command-error-function (data context caller)
"Ignore the buffer-read-only signal; pass the rest to the default handler."
(when (not (eq (car data) 'text-read-only))
(command-error-default-function data context caller)))
(setq command-error-function #'my-command-error-function)
(use-package xclip
:config (xclip-mode 1))
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(defconst emacs-tmp-dir (expand-file-name (format "emacs%d" (user-uid)) temporary-file-directory))
(setq backup-directory-alist
`((".*" . ,emacs-tmp-dir)))
(setq auto-save-file-name-transforms
`((".*" ,emacs-tmp-dir t)))
(setq auto-save-list-file-prefix
emacs-tmp-dir)
I’m not sure about the rationale behind this setting, but the auto-generated files are an annoyance, so they walk the plank.
(setq create-lockfiles nil)
Sorry Richard
(defun add-kbd (key) (kbd key))
(defvar keybindings-to-unset '("M-k" "M-j"))
(dolist (key (mapcar 'add-kbd keybindings-to-unset))
(global-unset-key key))
(electric-pair-mode 1)
(global-visual-line-mode t)
(setq standard-indent 2)
(setq-default indent-tabs-mode nil)
(use-package drag-stuff
:ensure t)
(drag-stuff-global-mode 1)
(global-set-key (kbd "M-k") 'drag-stuff-up)
(global-set-key (kbd "M-j") 'drag-stuff-down)
(setq show-paren-delay 0)
(show-paren-mode 1)
(setq evil-want-C-i-jump nil)
(setq evil-want-C-u-scroll t)
(use-package evil
:ensure t
:init
(setq evil-vsplit-window-right t)
:config
(evil-mode 1)
(use-package evil-leader
:ensure t
:config
(global-evil-leader-mode))
(use-package evil-surround
:ensure t
:config
(global-evil-surround-mode))
(use-package evil-org
:ensure t
:after org
:config
(add-hook 'org-mode-hook 'evil-org-mode)
(add-hook 'evil-org-mode-hook
(lambda () (evil-org-set-key-theme))))
(use-package evil-indent-textobject
:ensure t)
(use-package evil-commentary
:ensure t
:config
(evil-commentary-mode)))
(use-package evil-terminal-cursor-changer
:ensure t
:init
(setq evil-motion-state-cursor 'box) ; █
(setq evil-visual-state-cursor 'box) ; █
(setq evil-normal-state-cursor 'box) ; █
(setq evil-insert-state-cursor 'bar) ; ⎸
(setq evil-emacs-state-cursor 'hbar) ; _
:config
(evil-terminal-cursor-changer-activate))
In Delete Selection mode, if the mark is active, just deactivate it then it takes a second `keyboard-quit` to abort the minibuffer.
(defun minibuffer-keyboard-quit ()
(interactive)
(if (and delete-selection-mode transient-mark-mode mark-active)
(setq deactivate-mark t)
(when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
(abort-recursive-edit)))
(define-key evil-visual-state-map [escape] 'keyboard-quit)
(define-key minibuffer-local-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-ns-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-completion-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-must-match-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-isearch-map [escape] 'minibuffer-keyboard-quit)
(use-package navigate
:ensure t)
This package enables seamless C-[hjkl] movement through tmux panes and Emacs windows. The following commands are required to be present in your tmux config:
bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iqE '(^|\/)n?vim(diff)?$|emacs.*$' && tmux send-keys C-h) || tmux select-pane -L" bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iqE '(^|\/)n?vim(diff)?$|emacs.*$' && tmux send-keys C-j) || tmux select-pane -D" bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iqE '(^|\/)n?vim(diff)?$|emacs.*$' && tmux send-keys C-k) || tmux select-pane -U" bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iqE '(^|\/)n?vim(diff)?$|emacs.*$' && tmux send-keys C-l) || tmux select-pane -R"
(define-key evil-normal-state-map (kbd "j") 'evil-next-visual-line)
(define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line)
(define-key evil-motion-state-map (kbd "0") 'evil-first-non-blank)
(define-key evil-motion-state-map (kbd "^") 'evil-beginning-of-line)
(evil-leader/set-leader "<SPC>")
(evil-leader/set-key
"f" 'helm-projectile-find-file
"F" 'helm-projectile-ag
"q" 'evil-quit
"w" 'save-buffer
"t" 'neotree-toggle
"e" 'emojify-insert-emoji
"g" 'magit)
(evil-leader/set-key-for-mode 'org-mode
"A" 'org-archive-subtree
"a" 'org-agenda
"c" 'org-capture
"d" 'org-deadline
"l" 'evil-org-open-links
"s" 'org-schedule
"t" 'org-todo)
(setq org-startup-indented t
org-ellipsis " "
org-hide-leading-stars t
org-src-fontify-natively t
org-src-tab-acts-natively t
org-pretty-entities t
org-hide-emphasis-markers t
org-agenda-block-separator ""
org-fontify-whole-heading-line t
org-fontify-done-headline t
org-fontify-quote-and-verse-blocks t)
(use-package org
:ensure org-plus-contrib)
(use-package org-bullets
:ensure t
:config
(add-hook 'org-mode-hook (lambda () (org-bullets-mode 1))))
Usually when I use the agenda I concentrate only on what’s inside and I don’t need to cross-reference with other windows.
(setq org-agenda-window-setup 'only-window)
Setting the foreground color to nil
causes the ellipsis to take the color of its heading.
(custom-set-faces
'(org-ellipsis ((t (:foreground nil)))))
For starters, I’ll be using a setup similar to the one specified in this post by Nicolas Petton.
(setq gtd-base-path (expand-file-name "~/Projects/"))
(defun gtd-path (sub-path) (concat gtd-base-path sub-path))
(defvar inbox (gtd-path "inbox.org"))
(defvar projects (gtd-path "projects.org"))
(defvar someday (gtd-path "someday.org"))
(defvar tickler (gtd-path "tickler.org"))
(setq org-agenda-files (list inbox projects tickler))
(setq org-refile-targets `((,projects :maxlevel . 3)
(,someday :level . 1)
(,tickler :maxlevel . 2)))
(add-hook 'org-capture-mode-hook 'evil-insert-state)
(defvar inbox-capture-template "* %i%?")
(defvar todo-capture-template "* TODO %i%?")
(defvar tickler-capture-template "* %i%?")
(setq org-capture-templates `(("i" "Inbox" entry (file+headline inbox "Inbox") ,inbox-capture-template)
("t" "Inbox [TODO]" entry (file+headline inbox "Inbox") ,todo-capture-template)
("T" "Tickler" entry (file+headline tickler "Tickler") ,tickler-capture-template)))
Documentation about tracking state changes in TODOs can be found here.
(setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w!)" "|" "DONE(d!)" "CANCELLED(c!)")))
(setq org-log-into-drawer 'LOGBOOK)
Documentation on available colors can be found here.
(setq org-todo-keyword-faces
'(("WAITING" . "grey20") ("CANCELED" . "darkred") ("NEXT" . "orange")))
(setq org-tag-alist
'(("work" . ?w)
("home" . ?h)
("computer" . ?c)
("phone" . ?p)
("brain" . ?b)
("out" . ?o)))
(use-package helm
:ensure t
:config (helm-mode t))
(use-package projectile
:ensure projectile
:config
(setq projectile-indexing-method 'git))
(use-package helm-projectile
:ensure t)
(use-package helm-ag
:ensure t)
(use-package neotree :ensure t)
If you use evil-mode, by default some of evil key bindings conflict with neotree-mode keys.
(evil-define-key 'normal neotree-mode-map (kbd "TAB") 'neotree-enter)
(evil-define-key 'normal neotree-mode-map (kbd "SPC") 'neotree-quick-look)
(evil-define-key 'normal neotree-mode-map (kbd "q") 'neotree-hide)
(evil-define-key 'normal neotree-mode-map (kbd "RET") 'neotree-enter)
(use-package company
:ensure t
:config
(global-company-mode t)
(setq company-global-modes '(not org-mode)))
(define-key company-mode-map (kbd "TAB") 'company-complete)
(use-package emojify
:ensure t
:init
(add-hook 'after-init-hook #'global-emojify-mode)
(setq emojify-display-style 'unicode))
(use-package rainbow-delimiters
:init
(add-hook 'web-mode-hook #'rainbow-delimiters-mode)
(add-hook 'rust-mode-hook #'rainbow-delimiters-mode))
(use-package magit
:ensure t
:config (setq magit-diff-refine-hunk 'all))
(use-package evil-magit :ensure t)
(use-package magit-gh-pulls
:ensure t
:config (add-hook 'magit-mode-hook 'turn-on-magit-gh-pulls))
(use-package diff-hl
:ensure t
:init
(setq diff-hl-side 'right))
(global-diff-hl-mode 1)
(diff-hl-margin-mode 1)
(diff-hl-flydiff-mode 1)
Initialize web-mode and recognize extensions. Also consider the possibility of JSX files with a .js extension istead of .jsx.
(use-package web-mode
:ensure t
:init
(setq web-mode-content-types-alist '(("jsx" . "\\.tsx\\'")))
(setq web-mode-content-types-alist '(("jsx" . "\\.js\\'")))
:config
(add-to-list 'auto-mode-alist '("\\.erb?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.js[x]?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.ts[x]?\\'" . web-mode)))
Add node_modules
path
(use-package add-node-modules-path
:ensure t)
Run prettier
on save if web-mode
(eval-after-load 'web-mode
'(progn
(add-hook 'web-mode-hook #'add-node-modules-path)
(add-hook 'web-mode-hook #'prettier-js-mode)))
(use-package yaml-mode :ensure t)
(use-package haml-mode :ensure t)
(use-package scss-mode
:mode (("\.scss\'" . scss-mode)))
(use-package tide
:ensure t)
(defun setup-tide-mode ()
(interactive)
(tide-setup)
(flycheck-mode +1)
(setq flycheck-check-syntax-automatically '(save mode-enabled))
(eldoc-mode +1)
(tide-hl-identifier-mode +1)
;; company is an optional dependency. You have to
;; install it separately via package-install
;; `M-x package-install [ret] company`
(company-mode +1))
;; aligns annotation to the right hand side
(setq company-tooltip-align-annotations t)
(setq tide-tsserver-executable "node_modules/.bin/tsserver")
(add-hook 'web-mode-hook #'setup-tide-mode)
(use-package graphql-mode
:ensure t)
(use-package rust-mode
:ensure t)
(use-package markdown-mode
:ensure t
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init (setq markdown-command "multimarkdown"))
(add-to-list 'auto-mode-alist '("\\.tex.tera\\'" . latex-mode))
Disable adding magit comments in ruby-mode
.
(setq ruby-insert-encoding-magic-comment nil)
(use-package editorconfig
:ensure t
:config
(editorconfig-mode 1))
Flycheck is used for on-the-fly linting. We set the indication mode to nil, because otherwise it conflicts with the line numbers.
This disables the indicators in the fringe, but still shows the marked errors in the buffer.
We set a zero delay to show the error message on the status bar below, and set a 0.2 second delay to avoid machine-gunning eslint
.
(use-package flycheck
:ensure t
:init
(setq flycheck-indication-mode nil)
(setq flycheck-display-errors-delay nil)
(setq flycheck-idle-change-delay 2)
(global-flycheck-mode))
(flycheck-add-mode 'javascript-eslint 'web-mode)
Make sure eslint
does not try to --print-config
after each buffer opens. Here’s a related Flycheck issue.
(with-eval-after-load 'flycheck
(advice-add 'flycheck-eslint-config-exists-p :override (lambda() t)))
(defun my/use-eslint-from-node-modules ()
(let* ((root (locate-dominating-file
(or (buffer-file-name) default-directory)
"node_modules"))
(eslint (and root
(expand-file-name "node_modules/eslint/bin/eslint.js"
root))))
(when (and eslint (file-executable-p eslint))
(setq-local flycheck-javascript-eslint-executable eslint))))
(add-hook 'flycheck-mode-hook #'my/use-eslint-from-node-modules)
(evil-leader/set-key
"j" 'flycheck-next-error
"k" 'flycheck-previous-error)
Make a list of things we want to reevaluate when connecting to the server
(defun reevaluate-eyecandy ()
(load-theme my-theme t))
Reload the theme and eyecandy settings when a new frame opens if running a server
(if (daemonp)
(add-hook 'after-make-frame-functions
(lambda (frame)
(select-frame frame)
(reevaluate-eyecandy))))