There are a number of small irritants that need to be fixed:
[x] Fix orderless so it does case-insensitive and regexp matches.
(use-package gwb-fix-obsolescence
:demand t)
(setq ring-bell-function 'ignore)
(fset 'yes-or-no-p 'y-or-n-p)
(show-paren-mode)
(set-fill-column 80)
(setq column-number-mode t)
(setq-default indent-tabs-mode nil)
(setq uniquify-buffer-name-style 'forward)
(setq eshell-cmpl-ignore-case t) ;; ignore case when completing filename
(setq initial-major-mode 'fundamental-mode) ; Scratch buffer default mode => faster load
(global-set-key (kbd "C-x C-b") 'ibuffer)
(global-set-key (kbd "M-<down>") 'scroll-other-window)
(global-set-key (kbd "M-<up>") 'scroll-other-window-down)
(setq disabled-command-function nil) ; re-enable disabled commands
Make sure emacs treats everything as unicode by default.
(set-language-environment "utf-8")
Fix osx issue when using dired.
(when (string= system-type "darwin")
(setq dired-use-ls-dired nil))
This is useful only in the terminal – not the GUI
(xterm-mouse-mode t)
; Mouse wheel: scroll up/down; control-wheel for pgup/pgdn.
(defun wheel-scroll-up () (lambda () (interactive) (scroll-up 2)))
(defun wheel-scroll-down () (lambda () (interactive) (scroll-down 2)))
(defun wheel-scroll-pgup () (lambda () (interactive) (scroll-up 20)))
(defun wheel-scroll-pgdown () (lambda () (interactive) (scroll-down 20)))
(define-key global-map [mouse-5] (wheel-scroll-up))
(define-key global-map [mouse-4] (wheel-scroll-down))
(define-key global-map [C-mouse-5] (wheel-scroll-pgup))
(define-key global-map [C-mouse-4] (wheel-scroll-pgdown))
When modifying variables defined in packages, the right approach is to use the function `custom-set-variables’, not `setq’. See the following comment on stackoverflow. The downside is that variables set that way are then appended automatically to the init.el file, making things untidy. The following dumps this “automatically generated code” in a different file and loads it.
(setq-default custom-file (expand-file-name ".custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
(load custom-file))
For fonts and faces, we need to deal with two cases:
- when emacs is started with a daemon
- when emacs is started without a daemon
Depending on which it is, the frame will be created at different times, so we need to apply modifications at different times.
First we define the attributes to be applied
(defun gwb/after-frame-set-face (frame)
(when (display-graphic-p frame)
(message "Faces were set for default, fixed and variable pitch for this frame")
(set-face-attribute 'default frame :font "Fira Code Retina" :height 120)
(set-face-attribute 'fixed-pitch frame :font "Fira Code Retina" :height 120)
(set-face-attribute 'variable-pitch frame :font "Cantarell" :height 130 :weight 'regular)))
When started with a daemon, no frame is created so need to add hook for when frame is created.
(add-hook 'after-make-frame-functions 'gwb/after-frame-set-face)
When created without a daemon, frame is created before the above hook is setup so this wouldn’t work. Instead we need to apply the function to whatever frames already exist.
(mapc 'gwb/after-frame-set-face (frame-list))
To switch themes during usage: M-X counsel-load-theme I was worreid that using doom-themes would massively slow down startup time, but I guess the folks working on Doom are doing a very good job because this is adding only 30ms to load time (compared to using whatever emacs’ default theme is), which I can live with.
(use-package doom-themes
:ensure t
:init (load-theme 'doom-tomorrow-day t)
:config
(doom-themes-org-config) ; font-locks code blocks in org mode
)
(use-package ace-window
:ensure t
:bind ("M-o" . 'ace-window)
:config
;; increase size of leading char
(custom-set-faces
'(aw-leading-char-face
((t (:foreground "red" :height 3.0))))))
Alternatively, you can also test regexps interactively using emacs’s built-in regexp builder (M-x re-builder)
(use-package visual-regexp
:ensure t
:bind (("C-c r" . 'vr/replace)
("C-c q" . 'vr/query-replace)))
(use-package isearch
:bind
(:map isearch-mode-map
("M-m" . gwb/isearch-yank-region)
([remap isearch-yank-word-or-char] . gwb/isearch-yank-word-at-point))
:init
;; custom functions
(defun gwb/region-text ()
(interactive)
(buffer-substring (region-beginning) (region-end)))
(defun gwb/isearch-yank-region ()
"Yanks the current active region to the isearch minibuffer.
The point is moved to the beginning of the region at the end of
the operation, so the first match is always the current region."
(interactive)
(let ((word (gwb/region-text))
(end-word (region-end)))
(deactivate-mark)
(goto-char end-word)
(backward-word)
(isearch-yank-string word)))
(defun gwb/isearch-yank-word-at-point ()
"Yanks the word at point to the isearch minibuffer. This is
intended to replace the functionality of `isearch-yank-word-or-char'
mapped to C-s C-w, the behavior of which I don't like."
(interactive)
(let ((word (word-at-point t)))
(forward-word)
(backward-word)
(isearch-yank-string word)))
(defun gwb/goto-other-end ()
"If search forward, return to beginning of match. If search backward, do
nothing (already goes to beginning automatically"
(if (< isearch-other-end (point))
(goto-char isearch-other-end)))
(defun gwb/isearch-exit ()
"Modifies the isearch-exit function to return to beginning of
word if succesful match"
(interactive)
(if (and search-nonincremental-instead
(= 0 (length isearch-string)))
(let ((isearch-nonincremental t))
(isearch-edit-string)) ;; this calls isearch-done as well
(isearch-done))
(gwb/goto-other-end)
(isearch-clean-overlays))
(add-hook 'isearch-mode-hook
(lambda ()
(define-key isearch-mode-map "\r"
'gwb/isearch-exit)))
:config
;; changes highlighting for active and passive matches
(set-face-attribute 'lazy-highlight nil :background "tan1")
(set-face-attribute 'isearch nil :background "SkyBlue1")
;; spaces in search separate different search terms instead
;; instad of being interpreted literally
(setq search-whitespace-regexp ".*")
(setq isearch-lax-whitespace t))
I added the option to specify the directory where the search should be performed. If you prefix the search command by C-u, you will prompted for a directory.
(use-package deadgrep
:ensure t
:demand t
:init
(defun gwb-deadgrep (search-term)
(interactive (list (deadgrep--read-search-term)))
(let ((dir (when current-prefix-arg
(message (read-directory-name "where? " default-directory))))
(current-prefix-arg nil))
(deadgrep search-term dir)))
:bind
(("M-s g" . gwb-deadgrep)))
(use-package avy
:ensure t
:bind (("M-j" . avy-goto-char-timer)))
The following package help with emacs’s ergnomics, discoverability, etc..
which-key provides key-binding completion in mini buffer.
(use-package which-key
:ensure t
:config
(which-key-mode))
(use-package vertico
:ensure t
:init
(setq completion-in-region-function
(lambda (&rest args)
(apply (if vertico-mode
#'consult-completion-in-region
#'completion--in-region)
args)))
(vertico-mode))
(use-package vertico-directory
:after vertico
:ensure nil
;; More convenient directory navigation commands
:bind (:map vertico-map
("RET" . vertico-directory-enter)
("DEL" . vertico-directory-delete-char)
("M-DEL" . vertico-directory-delete-word))
;; Tidy shadowed file names
:hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
;; allows vertico to sort by history position
(use-package savehist
:init
(savehist-mode))
(use-package orderless
:ensure t
:demand t
:custom
(setq completion-styles '(orderless basic)
completion-category-defaults nil
completion-category-overrides '((file (styles partial-completion))
(symbol (styles
;; orderless-strict-leading-initialism
orderless-literal
orderless-regexp))))
:config
(setq completion-styles '(orderless)))
(use-package marginalia
:ensure t
:init
(marginalia-mode)
:bind
(:map minibuffer-local-map
("M-A" . marginalia-cycle)))
(use-package embark
:ensure t
:bind
(("C-." . embark-act)
("M-." . embark-dwim)))
(use-package recentf
:commands (recentf-mode
recentf-add-file
recentf-apply-filename-handlers))
(use-package consult
:ensure t
:config ;; or :init?
(recentf-mode)
:bind
(("C-x b" . consult-buffer)
("M-y" . consult-yank-pop)
("M-g g" . consult-goto-line)
("M-g o" . consult-outline)))
(use-package embark-consult
:ensure t
:after (embark consult))
(use-package corfu
:ensure t
:init
(global-corfu-mode)
:custom
(corfu-cycle t)
:bind
(:map corfu-map
("TAB" . corfu-next)
([tab] . corfu-next)
("S-TAB" . corfu-previous)
([backtab] . corfu-previous)))
(use-package helpful
:ensure t
:bind
([remap describe-function] . helpful-callable)
([remap describe-command] . helpful-command)
([remap describe-variable] . helpful-variable)
([remap describe-key] . helpful-key))
(use-package projectile
:ensure t
:bind-keymap
("C-c p" . projectile-command-map)
:config
(projectile-mode +1)
)
Make grep buffer writable. Allows one to edit occur buffers by:
- Running C-x C-q to make occur buffer writable
- … making whatever change
- Running C-x C-s to save changes. The changes will be written in to the source files.
(use-package wgrep :ensure t :defer 5)
Displays all emacs commands used during usage. Useful for debugging and learning.
Usage:
- First: M-x command-log-mode
- Then: “C-c x l” to display log in different buffer
(use-package command-log-mode
:ensure t
:commands (command-log-mode)
:bind ("C-c x l" . clm/toggle-command-log-buffer))
(use-package yasnippet
:ensure t
:hook (python-mode . yas-minor-mode))
This needs to be loaded early
(use-package hydra
:ensure t
:demand t)
(use-package magit
:ensure t
:defer 5
:bind ("C-x g" . 'magit-status))
(use-package org
:defer t
:config
;; indent mode
(add-hook 'org-mode-hook 'org-indent-mode)
;; line wrap
(add-hook 'org-mode-hook
(lambda ()
(visual-line-mode 1)))
;; some basic directories
(setq org-directory "~/org")
(setq org-default-notes-file "~/org/refile.org")
(setq org-agenda-files (quote ("~/org")))
;;keybindings
(global-set-key (kbd "C-c a") 'org-agenda)
(global-set-key (kbd "C-c b") 'org-switchb)
(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "C-c i") 'org-indent-mode)
(global-set-key (kbd "C-c c") 'org-capture)
;; some basic configs
(setq org-loop-over-headlines-in-active-region t)
(setq org-log-done t)
(setq org-archive-mark-done nil)
(setq org-archive-location "~/org/archive/%s_archive::")
;; custom keywords + selection
(setq org-todo-keywords
'((sequence "TODO(t)" "|" "POSTPONED(p)" "CANCELLED(c)" "DONE(d)")
(sequence "IDEA(i)" "|" "IMPLEMENTED")
(sequence "TO-READ(r)" "|" "READ")))
(setq org-use-fast-todo-selection t)
;; fonts
(defun gwb/org-font-setup ()
(dolist (face '((org-level-1 . 1.5)
(org-level-2 . 1.3)
(org-level-3 . 1.1)
(org-level-4 . 1.0)
(org-level-5 . 1.1)
(org-level-6 . 1.1)
(org-level-7 . 1.1)
(org-level-8 . 1.1)))
(set-face-attribute (car face) nil :font "Cantarell" :weight 'regular :height (cdr face)))
;; Ensure that anything that should be fixed-pitch in Org files appears that way
(set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
(set-face-attribute 'org-table nil :inherit 'fixed-pitch)
(set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
(set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-table nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch))
(defun gwb/org-font-setup-daemon (frame)
(gwb/org-font-setup))
(add-hook 'after-make-frame-functions 'gwb/org-font-setup-daemon)
(gwb/org-font-setup)
;; capture
(setq org-capture-templates
(quote (("t" "todo" entry (file+headline "~/org/refile.org" "Tasks")
"* TODO %?\n %i\n (%U) %a")
("b" "book to read" entry (file+headline "~/org/books.org" "To read")
"* TO-READ %?\n %i\n")
("i" "idea" entry (file+headline "~/org/refile.org" "Ideas")
"* IDEA %?\n %i\n")
("n" "note" entry (file+headline "~/org/refile.org" "Notes")
"* %? :NOTE:\n (%U) %a"))))
;; refiling
(setq org-refile-targets (quote ((nil :maxlevel . 9)
(org-agenda-files :maxlevel . 9))))
(setq org-refile-use-outline-path 'file)
(setq org-goto-interface 'outline-path-completion)
(setq org-outline-path-complete-in-steps nil)
(setq org-refile-allow-creating-parent-nodes 'confirm)
(org-babel-do-load-languages
'org-babel-load-languages
'((R . t)
(emacs-lisp . t)
(dot . t)
(latex . t)))
)
(use-package org-bullets
:ensure t
:after org
:hook (org-mode . org-bullets-mode)
:custom
(org-bullets-bullet-list '("◉" "○" "●" "○" "●" "○" "●")))
(defun gwb-dired-kill-hidden nil
(interactive)
(dired-mark-files-regexp "^\\.")
(dired-do-kill-lines))
(use-package dired
:bind
(:map dired-mode-map
("." . gwb-dired-kill-hidden))
:config
(setq insert-directory-program "gls")
(setq dired-listing-switches "-alh --group-directories-first")
(setq dired-dwim-target t) ;; dired will try to gess target directory when copying, etc...
)
(use-package company
:ensure t
:hook ((c-mode . company-mode)
;(ess-r-mode . company-mode)
;(inferior-ess-r-mode . company-mode)
)
:bind (:map company-active-map
("C-n" . company-select-next-or-abort)
("C-p" . company-select-previous-or-abort))
:config
(setq company-idle-delay nil))
(use-package company-c-headers
:ensure t
:after (company)
:config
(add-to-list 'company-backends 'company-c-headers)
(add-to-list 'company-c-headers-path-system "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include"))
(use-package gwb-indent
:after company)
(use-package outline
:commands outline-minor-mode
:init
(add-hook 'emacs-lisp-mode-hook
#'(lambda () (setq-local outline-regexp "[;\f][;\f] [*\f]+"))))
(add-hook 'prog-mode-hook 'outline-minor-mode)
(add-hook 'prog-mode-hook 'hs-minor-mode)
(use-package bicycle
:ensure t
:after outline
:bind (:map outline-minor-mode-map
("M-S-]" . bicycle-cycle)
("C-M-]" . bicycle-cycle-global)))
(add-hook 'prog-mode-hook #'(lambda () (setq-local tab-always-indent 'complete)))
(use-package eglot
:ensure t
:config
(add-to-list 'eglot-server-programs
'((rust-ts-mode rust-mode) .
("rust-analyzer" :initializationOptions (:check (:command "clippy")))))
)
I have played around, at some point or another, with many programming languages. This section deals with the languages for which the config is minimal (or non-existent).
;; (use-package julia-mode
;; :mode "\\.jl\\'")
(use-package markdown-mode
:ensure t
:mode (("\\.md\\'" . markdown-mode)
("\\.Rmd\\'" . markdown-mode)))
;; (use-package elm-mode
;; :mode "\\.elm\\'")
;; (use-package haskell-mode
;; :mode "\\.hs\\'"
;; :hook (haskell-mode . interactive-haskell-mode))
(use-package rust-mode
:mode "\\.rs\\'"
:hook (rust-mode . eglot-ensure)
:init
(add-hook 'rust-mode (lambda () (setq indent-tabs-mode nil))))
(use-package lispy
:ensure t
:hook ((emacs-lisp-mode . lispy-mode)
(scheme-mode . lispy-mode)
(gerbil-mode .lispy-mode))
:bind (:map lispy-mode-map
("M-o" . nil)))
;; (use-package racket-mode
;; :ensure t
;; :mode "\\.rkt\\'"
;; :config
;; (setq tab-always-indent 'complete)
;; (require 'racket-xp)
;; (add-hook 'racket-mode-hook #'racket-xp-mode))
(use-package slime
:commands slime
:init
(setq slime-lisp-modes nil)
(setq inferior-lisp-program "sbcl")
:config
(remove-hook 'lisp-mode-hook 'slime-lisp-mode-hook)
(load (expand-file-name "~/quicklisp/slime-helper.el"))
)
(use-package sly
:commands sly
:init
(setq inferior-lisp-program "sbcl")
:config
(setq lispy-use-sly t)
)
(use-package emacs-lisp-mode
:hook (emacs-lisp-mode . hs-minor-mode)
:bind (:map emacs-lisp-mode-map
("M-[" . hs-hide-all)
("M-]" . hs-show-all)))
(use-package lisp-mode
:hook (lisp-mode . lispy-mode))
I’ll be using Gerbil as my default scheme for now:
- I’ve installed it with homebrew (see: brew info gerbil-scheme)
- Executable is /usr/loca/bin/gxi
Since Gerbil piggy backs on Gambit, you need that mode as well.
The relevant .el files are in .emacs.d/copy-lisp as gerbil-mode.el and gambit.el. They were copied from the gerbil / gambit installs:
- /usr/local/share/emacs/site-lisp/gambit-scheme/gambit.el
- /usr/local/share/emacs/site-lisp/gerbil-scheme/gerbil-mode.el
To start a repl when editing gerbil code (.ss) just do M-x run-scheme. Do C-c C-c to eval sexp. Check C-h v gerbil-mode-map for commands.
See also https://gerbil.scheme.org/guide/emacs.html#use-package-example-configuration for info on how to use gerbil with emacs.
(use-package gerbil-mode
:mode "\\.ss\\'"
:hook
(inferior-scheme-mode-hook . gambit-inferior-mode)
:config
(require 'gambit)
(setf scheme-program-name "/usr/local/bin/gxi")
(add-hook 'inferior-scheme-mode-hook 'gambit-inferior-mode)
:bind (:map gerbil-mode-map ("C-c C-c" . scheme-send-definition))
)
(use-package gambit
:bind (:map inferior-scheme-mode-map ("C-c C--" . gambit-kill-last-popup)))
(use-package bqn-mode
:ensure t
:init (require 'gwb-bqn)
:commands (bqn-comint-buffer)
:config (add-hook 'bqn-comint-mode-hook #'gwb-amend-bqn-comint-mode)
:bind (:map bqn-mode-map
("C-c C-c" . bqn-comint-send-dwim)))
(use-package k-mode
:mode "\\.k\\'"
:commands (k-mode-run-k)
:custom
(k-mode-repl-bin-path "/Users/gwb/Hacks/repos/k/k")
)
(use-package j-mode
:ensure t
:custom
(j-console-cmd "jconsole"))
ESS is (used to be?) fiddly to setup correctly.
- I used to have both a version installed from the website, and one from MELPA… this was creating all sorts of issues. I have now removed the version from the website (it was a very old version), and kept only the MELPA version. NOTE: the version on the website is very very old (2019) while the MELPA version (i.e. the devel version) is updated very regularly. => make sure to stick to the MELPA version
- To load ESS, we used to need to include a (require ‘ess-site) statement. This is no longer the case, as per the documentation (see here page 7 – or search ‘use-package’ in the ESS manual).
- The first (use-package ess :defer t) sets up ESS, deferring the loading. When the loading is triggered by an autoload event (e.g. visiting an R file), ESS loads the ess-r-mode. The binding needs to be set in a separate ess-r-mode use-package because the ess-r-mode-map is defined by the ess-r-mode package, so if we put the bindings in the first one, the mode maps are not defined at the moment when they are evaluated.
(use-package ess
:ensure t
:defer t)
(use-package ess-r-mode
:hook
((ess-r-mode . hs-minor-mode)
(ess-r-mode . outline-minor-mode))
:bind
(:map
ess-r-mode-map
;("TAB" . gwb-indent-for-tab-command)
("_" . ess-insert-assign)
("M-[" . hs-hide-all)
("M-]" . hs-show-all)
:map
inferior-ess-r-mode-map
;("TAB" . gwb-indent-for-tab-command)
("_" . ess-insert-assign)
("M-[" . hs-hide-all)
("M-]" . hs-show-all)))
Below are my customizations for ESS. A few comments:
(use-package gwb-essr
:after ess-r-mode
;; :demand t
:commands (gwb-essr-configure-iess gwb-essr-configure-ess-r)
:hook
((inferior-ess-r-mode . gwb-essr-configure-iess)
(ess-r-mode . gwb-essr-configure-ess-r))
:bind
(:map
ess-r-mode-map
("%" . gwb-essr-insert-pipe-maybe)
("M-TAB" . gwb-essr-toggle-hide-function)
:map
inferior-ess-r-mode-map
("%" . gwb-essr-insert-pipe-maybe)
("M-TAB" . gwb-essr-toggle-hide-function))
:config
(advice-add 'ess-r-object-completion :filter-return #'gwb-essr--add-docsig))
(defhydra dumb-jump-hydra (:hint nil :color blue)
"
Dumb jump
"
("j" dumb-jump-go "Go")
("o" dumb-jump-go-other-window "Other window")
("e" dumb-jump-go-prefer-external "Go external")
("x" dumb-jump-go-prefer-external-other-window "Go external other window")
("i" dumb-jump-go-prompt "Prompt")
("l" dumb-jump-quick-look "Quick look")
("b" dumb-jump-back "Back"))
(use-package cc-mode
:init
(defun gwb/clang-capf-init ()
(add-hook 'completion-at-point-functions #'clang-capf nil t))
(defun gwb/dumb-jump-init ()
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
(setq xref-show-definitions-function #'xref-show-definitions-completing-read))
:defer t
:config
(setq c-default-style "linux")
(setq c-basic-offset 4)
(add-hook 'c-mode-hook #'gwb/clang-capf-init)
(add-hook 'c-mode-hook #'gwb/dumb-jump-init)
:bind (:map c-mode-map
("TAB" . indent-for-tab-command)
("C-j" . dumb-jump-hydra/body)
("M-[" . hs-show-all)
("M-]" . hs-hide-all)
("C-]" . hs-toggle-hiding)))
(use-package c++-mode
:hook ((c++-mode . eglot-ensure)))
;; (use-package eglot
;; :defer t
;; :config
;; (add-to-list 'eglot-server-programs '(c++-mode . ("/usr/local/opt/llvm/bin/clangd"))))
(use-package zig-mode :ensure t)
(use-package auctex
:mode ("\\.tex\\'" . TeX-latex-mode)
:config
(require 'reftex)
(add-hook 'LaTeX-mode-hook 'turn-on-reftex)
(setq reftex-plug-into-AUCTeX t)
;; Auxtex
(setq TeX-auto-save t)
(setq TeX-parse-self t)
;; PDF search
(add-hook 'LaTeX-mode-hook 'TeX-source-correlate-mode)
(add-hook 'LaTeX-mode-hook 'LaTeX-math-mode)
(setq TeX-PDF-mode t)
(when (eq system-type 'darwin)
(setq TeX-view-program-selection '((output-pdf "PDF Viewer")))
(setq TeX-view-program-list
'(("PDF Viewer" "/Applications/Skim.app/Contents/SharedSupport/displayline -b -g %n %o %b")))
)
;; (use-packag auctex-latexmk)
(require 'auctex-latexmk)
(auctex-latexmk-setup)
(setq auctex-latexmk-inherit-TeX-PDF-mode t)
;; Only change sectioning colour
(setq font-latex-fontify-sectioning 'color)
;; super-/sub-script on baseline
(setq font-latex-fontify-script nil) ; might not keep this line.. I like smaller {sub/super}scripts
(setq font-latex-script-display (quote (nil)))
;; Do not change super-/sub-script font
(setq font-latex-deactivated-keyword-classes
'("italic-command" "bold-command" "italic-declaration" "bold-declaration"))
)
The python setup in emacs is a wee bit complicated. There are generally roughly 4 parts:
- The major mode that provides things like syntax highlighting, bindings to the interpreter, etc…
- An “orchestrating minor mode” that communicates with outside binaries or other minor modes to provide things “ide-like” features like completion, linting, formatting, etc…
- The suite of binaries, other minor modes providing the individual functionalities listed above
- The minor modes that deal with the display of information (corfu/company, flymake/flycheck, etc…)
There are a few options for each of these: picking the right components and having them working nicely in concert requires some fiddling.
Major mode:
I’m using emacs’s built-in `python-mode`. I’m not sure there are any popular alternatives at the moment. The syntax highlighting it provides is not great. A specific alternative for syntax-highlighting uses `tree-sitter`. I should investigate that at some point.
TODO: investigate `tree-sitter`
Orchestrating minor mode:
This is the biggest decision-point. I used rely on `elpy` for it, but it was fiddly and unreliable for me. I stopped using it in frustration and went back to using only python-mode but I missed having things like code signature in the minibuffer, decent auto-completion, etc..
I have now switched to `lsp-mode` which is a client interface for the Language Server Protocol. It provides a generic client infrastructure that simplifies the task of writing concrete clients for specific languages. The important thing to understand about the lsp setup is that it involves 3 components:
- lsp-mode: the emacs package that provides the generic interface
- the language server. This is not an emacs-specific thing: these are external programs that provides language services for specific languages, and that the client connects to. For popular languages, there may be a few different server options to choose from. For python, there are 3 options:
- pyls
- pylsp
- pyright
The `pyls` server was developped by Palantir and seems to have been abandonned – or at least, it seems to no longer be supported. The `pylsp` server is a fork of `pyls` that is still maintained. The `pyright` server is supported by microsoft.
- The concrete client implementation. As I said, `lsp-mode` provides the generic client interface (i.e. you can use lsp-mode for many different languages) but you still need a specific client implementation. Generally you need an specific implementation for each language server. There can be different implementations for a given server, but that doesn’t seem to be the case at the moment. The clients are:
- lsp-pyls => included in lsp-mode
- lsp-pylsp => included in lsp-mode
- lsp-pyright => provided by the `lsp-pyright` package
I am currently using `pylsp` server (installed via `pip3 install python-lsp-server`) and the `lsp-pylsp` client included in the `lsp-mode` emacs package that also provides `lsp-mode`.
Additional tools
The lsp server (so in my case `pylsp`) relies on external tools to deliver some of its optional functionalities. To complicate things further, there may be several tools to choose from for given functionalitites: it really is an embarassement of riches! Below are some of the optional functionalities (non-exhaustive) that `pylsp` can provide and some of the tools that can be used to provide them:
- type-checking: pylsp-mypy. Note: the `pyright` server seems to do type-checking out of the box, but pylsp requires the `pylsp-mypy`, which can be installed with a simple `pip install pylsp-mypy` (make sure its in the same environment).
- Error + Pep8 style checking: flake8 or pylint (there are in fact many other options, but these are the main ones)
I’ve decided to go with flake8 for now because the `lsp-pylsp` client shipped with `lsp-mode` has better support for it than for pylint – by which I mean that it makes it easier to specify configs for flake8 that will then be sent to the server.
- Reformatting: autopep8 vs yapf (again, many more options)
I’m currently not using an automatic formatter. If decide to do so, `yapf` seems to be the preferred option.
- Completion and refactoring: jedi or rope.
I’ve been using the default, which is `jedi`, although I’ve been using it for completion mostly, not refactoring. From the `rope` website, it seems that `rope` is focused on recactoring. At this point, I mostly care about completion so I’ll stick to `jedi`.
- Poetry: I’m experimenting with poetry as a dependency manager.
- M-x poetry will start the menu with options, etc..
- To use the packages installed with poetry in the repl, you need to activate the virtualenv (M-x poetry activate) then just M-x run-python. Note that you need to install ipython in the virtualenv for it to work (M-x poetry add ipython). Preferably, install it as a dev dependency.
- To use the lsp server (which gives you completion, etc..) with poetry, you need to install the python-sp-server as a dev dependency in poetry, then activate the virtualenv, then visit a python file, etc…
Complementary emacs modes
Finally, `lsp-mode` relies on other emacs packages for certain functionalities. E.g.
- Completion: can use company or the built-in completion-at-point facilities (in which case, we can use things like corfu, etc…)
- Flycheck or Flymake. `Flymake` is built into emacs but `Flycheck` is the recommended option for `lsp-mode`.
I’ve tried both and I like `Flycheck`:
- It has nice introspection facilities so you can see what’s happening with the mode. E.g. `M-X flycheck-verify-setup` is very informative. Note that when used in concert with `lsp-mode`, flycheck basically uses `lsp` as it’s “checking” backend.
- You can list all errors easily `M-x list-flycheck-errors` (flymake can probably do that as well)
- You can jump to the next error `C-c ! n`
As described above, some additional packages, modules must exist for my config to work optimally. Thankfully, nothing breaks if I don’t have things installed: the optional features just won’t be turned on. So in addition to ‘lsp-mode’, I rely on the following:
- [emacs package] flycheck
- [pip install] flake8
- [pip install] pylsp-mypy
- [pip install] jedi
;; (use-package lsp-mode
;; :ensure t
;; :defer t ; ok to defer, will be loaded when needed
;; :config
;; ;; the pyright server has higher precedence than pylsp so need to disable it so
;; ;; lsp uses pylsp
;; ;; (setq lsp-clangd-binary-path "/usr/local/opt/llvm/bin/clangd")
;; (setq lsp-disabled-clients (cons 'pyright lsp-disabled-clients)))
Additional notes on lsp-mode:
The `lsp-mode` package provides helpful messages / debugging facilities to see what’s happening with the server / client communication. e.g.
- The lsp-log buffer
- The lsp-stderr buffer
- M-x lsp-describe-session
A note about completion: it works by adding its own backend in the list of `completion-at-point-functions`. The backend is called `lsp-completion-at-point`. You should see it if you type `C-h v completion-at-point-functions`.
(defun gwb-py-get-menu nil
(interactive)
(occur "# \\*"))
(use-package python
:ensure nil
:mode ("\\.py\\'" . python-mode)
:interpreter ("python" "python3")
;;:hook
;; (python-mode . (lambda ()
;; ;(require 'lsp-pyright)
;; (lsp)))
;; ;:bind
;; ;(:map python-mode-map
;; ; ("TAB" . gwb-indent-for-tab-command))
:config
;; => uncomment two below
(setq python-shell-interpreter "ipython3")
(setq python-shell-interpreter-args "-i --simple-prompt")
:bind
(:map python-mode-map
("C-c =" . gwb-py-get-menu))
)
**Pesky warning in repl**
When upgrading ipython to use python 3.11, I started seeing the following error:
Warning (python): Your ‘python-shell-interpreter’ doesn’t seem to support readline, yet ‘python-shell-completion-native-enable’ was t and “ipython3” is not part of the ‘python-shell-completion-native-disabled-interpreters’ list. Native completions have been disabled locally. Consider installing the python package “readline”.
I’ve had similar issues in the past. I took the time to track this down, and apparently it comes down to the `readline` module that is built into python. Different pythons use readline modules linking to different c readline libraries: `libedit` or `gnureadline`. You can check which version your ipython uses (assuming that you want to use ipython as your repl)
import readline print(readline.__doc__)
If you see:
Importing this module enables command line editing using libedit readline.
Then you’ll see warning: you need to make sure your interpreter uses the gnureadline version. I don’t know how to force pick a version of ipython3 that has gnureadline. Instead, I’m using the following trick:
- pip install gnureadline
- automatically run some code on ipython startup so it tricks python into importing
gnureadline when it wants to import readline. The python code you want to run is
import gnureadline import sys sys.modules[“readline”] = gnureadline
To make sure ipython runs it on startup you need to to run
ipython3 profile create
from the command line. This will create a ~.ipython/profile_default/ipython_config.py file with a bunch of stuff commented out in it. You just need to make sure that you have the following in the file:
c = get_config() #noqa c.InteractiveShellApp.exec_lines = [‘import gnureadline’, ‘import sys’, ‘sys.modules[“readline”] = gnureadline’]
You also need to make sure that you’ve passe the “-i –simple-prompt” arguments to python-shell-interpreter-args (as in the above config).
Useful keybindings for terminal:
- C-c C-k: term-char-mode (can’t use usual emacs bindings)
- C-c C-j: term-line-mode (can use emacs bindings)
- C-c C-p: jump to last prompt
- M-p: travel history
(use-package term
:commands term
:config
(setq term-prompt-regexp "^[^#$%>\\n]*[#$%>] *"))
(use-package eterm-256color
:hook (term-mode . eterm-256color-mode))
Spotify custom utilities
(use-package elspot
:commands hydra-spotify/body)
Hugo utilities
(use-package gwb-hugo
:commands gwb-run-hugo-server) ;; allows to quickly start and kill hugo servers
Better Occur behavior
(defun gwb/kill-occur-buffer-window (&rest args)
(delete-window (get-buffer-window "*Occur*")))
(defun gwb/switch-to-occur-buffer (&rest args)
(let ((buffer-window (get-buffer-window "*Occur*")))
(when buffer-window
(select-window buffer-window))))
(advice-add 'occur-mode-goto-occurrence :after #'gwb/kill-occur-buffer-window)
(advice-add 'occur :after #'gwb/switch-to-occur-buffer)
Line movement
(defun gwb/move-beginning-of-line (arg)
"moves first to first non-whitespace characters. If already there moves to
to beginning of line"
(interactive "^p")
(setq arg (or arg 1))
(when (/= arg 1)
(let ((line-move-visual nil))
(forward-line (1- arg))))
(let ((orig-point (point)))
(back-to-indentation)
(when (= orig-point (point))
(move-beginning-of-line 1))))
(global-set-key [remap move-beginning-of-line]
'gwb/move-beginning-of-line)
Note: interestingly, CMD + SHIFT is mapped to super (s). e.g. CMD+SHIFT+SPC maps to s-SPC.
(defun gwb/display-this-buffer-other-window ()
(interactive)
(switch-to-buffer-other-window (buffer-name)))
(defun gwb/mark-word-at-point ()
(interactive)
(let ((word (word-at-point t)))
(progn
(forward-word)
(backward-word)
(set-mark-command 'nil)
(search-forward word))))
(defun gwb/latex-note ()
"Inserts my `note' template, and automatically turns on latex (auctex) mode"
(interactive)
(insert-file-contents-literally "~/.emacs.default/my-latex-templates/note.tex")
(latex-mode))
(defun gwb/flip-windows ()
"flips the buffers in split-screen windows"
(interactive)
(unless (= 2 (count-windows))
(error "Only works with two windows."))
(let ((this-buffer (window-buffer (selected-window)))
(alt-buffer (window-buffer (previous-window))))
(set-window-buffer (previous-window) this-buffer)
(set-window-buffer (selected-window) alt-buffer)
(select-window (previous-window))))
(defun gwb/edit-config ()
"edits README.org"
(interactive)
(find-file "~/.emacs.d/README.org"))
(defun gwb/copy-to-osx (start end)
(interactive "r")
(shell-command-on-region start end "pbcopy"))
;; For some reason, the ring bell function gets reactivated
;; every time my laptop goes to sleep on mac os Big Sur... Need
;; a shortcut to quickly set this.
(defun gwb-mute-alerts ()
(interactive)
(setq ring-bell-function 'ignore))
;; custom function
(defun gwb/indent-org-block ()
(interactive)
(when (org-in-src-block-p)
(org-edit-special)
(indent-region (point-min) (point-max))
(org-edit-src-exit)))
(defvar gwb-custom-keymap nil "my keymap..")
(setq gwb-custom-keymap (make-sparse-keymap))
(global-set-key (kbd "C-c x") gwb-custom-keymap)
(global-set-key (kbd "M-SPC") gwb-custom-keymap)
(define-key gwb-custom-keymap (kbd "m") 'gwb/mark-word-at-point)
(define-key gwb-custom-keymap (kbd "n") 'gwb/latex-note)
(define-key gwb-custom-keymap (kbd "o") 'gwb/flip-windows)
(define-key gwb-custom-keymap (kbd ".") 'gwb/edit-config)
(define-key gwb-custom-keymap (kbd "w") 'gwb/copy-to-osx)
(define-key gwb-custom-keymap (kbd "<") 'gwb-mute-alerts)
(define-key gwb-custom-keymap (kbd "TAB") 'gwb/indent-org-block)
(define-key gwb-custom-keymap (kbd "s") 'hydra-spotify/body)
(define-key gwb-custom-keymap (kbd "u") 'undo-tree-visualize)
(define-key gwb-custom-keymap (kbd "d") 'gwb/display-this-buffer-other-window)