#-*- mode: org -*-
#+TITLE: Emacs Setup for Programming Languages
#+AUTHOR: Volker Edelmann
#+EMAIL: vedelmann@gmx.de
#+STARTUP: indent
#+OPTIONS: toc:nil
#+OPTIONS: ^:{}
#+begin_src emacs-lisp
;; -*- coding: utf-8; lexical-binding: t -*-
#+end_src
* Introduction
- REPL
- editing
- editing paradigm
- code completion
- templates
- moving
- selecting
- code folding
- static syntax checking
- compilation
- debugging
- project-management
* Comments
Builtin!
#+begin_src emacs-lisp
(require 'newcomment)
#+end_src
* Key bindings for all prog-modes
#+begin_src emacs-lisp
(bind-keys :map prog-mode-map
("C-c /" . indent-region)
("C-c C-/" . indent-buffer)
("C-c ;" . comment-dwim)
("C-c i" . consult-imenu)
("C-c c" . consult-flymake)
)
#+end_src
* UI
If there is need for a modal interface, a Major-Mode-Hydra can be defined.
Bind in mayor-mode keymap to:
#+begin_example
:bind ( :map KEYMAP
("M-SPC" . major-mode-hydra))
#+end_example
* Lisp-like (s-expression) languages
- emacs-lisp
- clojure
- [guile (scheme)]
- [hy]
* Languages with Tree-sitter grammars
** Exploring
- combobulate
- Built-In commands: treesit-explore-mode
** Tree-sitter and combobulate
store grammars in a common location
#+begin_src emacs-lisp
(setq treesit-extra-load-path (list (concat emacsen-dir "treesit/grammars/") )
)
#+end_src
#+begin_src emacs-lisp
(use-package treesit
:mode (("\\.tsx\\'" . tsx-ts-mode))
:preface
(defun mp-setup-install-grammars ()
"Install Tree-sitter grammars if they are absent."
(interactive)
(dolist (grammar
'(
(tcl . "https://github.com/tree-sitter-grammars/tree-sitter-tcl")
))
(add-to-list 'treesit-language-source-alist grammar)
;; Only install `grammar' if we don't already have it
;; installed. However, if you want to *update* a grammar then
;; this obviously prevents that from happening.
(unless (treesit-language-available-p (car grammar))
(treesit-install-language-grammar (car grammar)))))
;; Optional, but recommended. Tree-sitter enabled major modes are
;; distinct from their ordinary counterparts.
;;
;; You can remap major modes with `major-mode-remap-alist'. Note
;; that this does *not* extend to hooks! Make sure you migrate them
;; also
(dolist (mapping
'((python-mode . python-ts-mode)
(css-mode . css-ts-mode)
(typescript-mode . typescript-ts-mode)
(js2-mode . js-ts-mode)
(bash-mode . bash-ts-mode)
(css-mode . css-ts-mode)
(json-mode . json-ts-mode)
(js-json-mode . json-ts-mode)))
(add-to-list 'major-mode-remap-alist mapping))
:config
(mp-setup-install-grammars)
;; Do not forget to customize Combobulate to your liking:
;;
;; M-x customize-group RET combobulate RET
;;
(use-package combobulate
:preface
;; You can customize Combobulate's key prefix here.
;; Note that you may have to restart Emacs for this to take effect!
(setq combobulate-key-prefix "C-c o")
;; Optional, but recommended.
;;
;; You can manually enable Combobulate with `M-x
;; combobulate-mode'.
:hook
((python-ts-mode . combobulate-mode)
(js-ts-mode . combobulate-mode)
(html-ts-mode . combobulate-mode)
(css-ts-mode . combobulate-mode)
(yaml-ts-mode . combobulate-mode)
(typescript-ts-mode . combobulate-mode)
(json-ts-mode . combobulate-mode)
(tsx-ts-mode . combobulate-mode))
;; Amend this to the directory where you keep Combobulate's source
;; code.
:load-path ("ve/combobulate")))
#+end_src
*** Additional Grammars
Repos, where grammars are found:
#+begin_src emacs-lisp
(setq treesit-language-source-alist
'(
(awk "https://github.com/Beaglefoot/tree-sitter-awk")
(bash "https://github.com/tree-sitter/tree-sitter-bash")
(c . ("https://github.com/tree-sitter/tree-sitter-c"))
(cpp . ("https://github.com/tree-sitter/tree-sitter-cpp"))
(clojure "https://github.com/sogaiu/tree-sitter-clojure")
(cmake "https://github.com/uyha/tree-sitter-cmake")
(css "https://github.com/tree-sitter/tree-sitter-css")
(d . ("https://github.com/gdamore/tree-sitter-d"))
(dockerfile . ("https://github.com/camdencheek/tree-sitter-dockerfile"))
(elisp "https://github.com/Wilfred/tree-sitter-elisp")
(html "https://github.com/tree-sitter/tree-sitter-html")
(java "https://github.com/tree-sitter/tree-sitter-java")
(javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
(json "https://github.com/tree-sitter/tree-sitter-json")
(julia "https://github.com/tree-sitter/tree-sitter-julia")
(kotlin "https://github.com/fwcd/tree-sitter-kotlin")
(lua . ("https://github.com/Azganoth/tree-sitter-lua"))
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
(perl "https://github.com/tree-sitter-perl/tree-sitter-perl")
(php . ("https://github.com/tree-sitter/tree-sitter-php"))
(python "https://github.com/tree-sitter/tree-sitter-python")
(rust "https://github.com/tree-sitter/tree-sitter-rust")
(sql "https://github.com/m-novikov/tree-sitter-sql")
(scala "https://github.com/tree-sitter/tree-sitter-scala")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/ikatyang/tree-sitter-yaml") )
)
#+end_src
Check if a grammar is available:
(treesit-language-available-p 'd)
*** Textobjects
#+begin_src emacs-lisp
(setq treesit-query-dir (concat emacsen-dir "treesit/queries/"))
;; (setq treesit-query-dir "~/emacsen/treesit/queries/")
#+end_src
*** TODO Movement
Using ts-movement turned out not to be useful.
*** Selections
very important when working with a REPL:
#+begin_src emacs-lisp
(use-package expreg)
#+end_src
Bind keys (example):
(define-key map ("C-+") 'expreg-expand)
(define-key map ("C--") 'expreg-contract)
Easiest solution found: expreg.el. Just 2 commands.
*** Folding
Using tree-sitter-fold turned out not to be useful.
** Others
*** TCL/TK
* Containers
#+begin_src emacs-lisp
(defcustom lemacs-docker-executable 'docker
"The executable to be used with docker-mode."
:type '(choice
(const :tag "podman" podman))
:group 'lemacs)
(use-package docker
:defer t
:ensure t
:bind ("C-c d" . docker)
:config
(pcase lemacs-docker-executable
('docker
(setf docker-command "docker"
docker-compose-command "docker-compose"
docker-container-tramp-method "docker"))
('podman
(setf docker-command "podman"
docker-compose-command "podman-compose"
docker-container-tramp-method "podman"))))
#+end_src
* Visuals
** Parentheses, brackets, ...
#+begin_src emacs-lisp
(require 'elec-pair)
#+end_src
Mark [, {, (:
#+begin_src emacs-lisp
(use-package rainbow-delimiters)
;; (add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
(set-face-attribute 'rainbow-delimiters-unmatched-face nil
:foreground "red"
:height 1.5
:weight 'ultra-bold
:inherit 'error)
(set-face-attribute 'rainbow-delimiters-depth-1-face nil
:foreground "black"
:weight 'extra-bold)
(set-face-attribute 'rainbow-delimiters-depth-2-face nil
:foreground "black"
:weight 'extra-bold)
(set-face-attribute 'rainbow-delimiters-depth-3-face nil
:foreground "black"
:weight 'extra-bold)
#+end_src
** Project Browser
*** DISCARDED dired-sidebar
cannot display fd-dired queries
* Editing
In Lisp-like modes, there are perhaps better alternatives:
- lispy
- paredit
- smartparens
* Source Code Version Control
** Fossil
#+begin_src emacs-lisp
(use-package vc-fossil
:defer t
:init (add-to-list 'vc-handled-backends 'Fossil t)
)
#+end_src
** TODO Pijul
TODO: The package vc-pijul does not yet exist.
#+begin_src emacs-lisp
#+end_src
(use-package vc-pijul
:defer t
:init (add-to-list 'vc-handled-backends 'Pijul t)
)
* Project Management
#+begin_src emacs-lisp
(put 'compile-command 'safe-local-variable t)
(setq enable-local-variables :safe)
(use-package project
:diminish project-mode " Π"
)
#+end_src
* Static Checkers/Linters
Although flymake is an Emacs built-in, flycheck has more checkers and
often works without further configuration when a
checker/linter is available via the OS path settings.
** My default
Flycheck is very well documented:
https://www.flycheck.org/en/latest/user/installation.html
https://www.flycheck.org/en/latest/user/quickstart.html
https://www.flycheck.org/en/latest/community/extensions.html#language-support
flycheck does not explicitly support windows, but there are rarely issues on windows.
Most checkers/linters are external programs, they have to be installed. The
configuration is found in the language specific section,
After installation, it is easy to check the setup. From the docs:
#+begin_verse
Visit a Python or Javascript file and check whether your Flycheck setup is complete with C-c ! v.
#+end_verse
As it is turned on per default, it has to be explicitly turned off in modes,
where this is unwanted (e.g. if a flymake-checker is preferred). This is done
via Hooks.
Diagnostics: list-flycheck-errors
#+begin_src emacs-lisp
(use-package flycheck
:ensure t
)
#+end_src
** Builtin: flymake
Diagnostics: flymake-show-buffer-diagnostics
#+begin_src emacs-lisp
(require 'flymake)
(use-package emacs
:config
(setq flymake-mode-line-lighter t)
)
#+end_src
* Finding and referencing symbols
** Language Servers
https://langserver.org/
*** Built-In eglot
#+begin_src emacs-lisp
(use-package eglot
:custom
(eglot-autoshutdown t)
(eglot-events-buffer-size 0)
(eglot-extend-to-xref nil)
(eglot-ignored-server-capabilities
'(:hoverProvider
:documentHighlightProvider
:documentFormattingProvider
:documentRangeFormattingProvider
:documentOnTypeFormattingProvider
:colorProvider
:foldingRangeProvider))
(eglot-stay-out-of '(yasnippet tempel))
)
#+end_src
** Tags
*** Built-In semantic
#+begin_src emacs-lisp
(require 'semantic)
#+end_src
*** Gnu Global
Global has 5 languages built-in and supports ctags with all its 150+ languages.
A reasonable solution using Gnu Global seems to be:
https://github.com/leoliu/ggtags
- completion
- reference
comes with integration for completion-at-point, eldoc- and xref-integration.
#+begin_src emacs-lisp
(use-package ggtags
:bind ( :map ggtags-navigation-map
("M-o" . nil)
)
)
#+end_src
**** Prerequisites
Install
- Gnu Global
- Python: pygments
* References
#+begin_src emacs-lisp
(use-package xref
:pin gnu
:custom
(xref-auto-jump-to-first-xref t)
(xref-file-name-display 'project-relative)
(xref-search-program 'ripgrep)
:config
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref)
)
#+end_src
* Documentation
#+begin_src emacs-lisp
(use-package eldoc
:pin gnu
:diminish
:bind ("s-d" . #'eldoc)
:custom
(eldoc-echo-area-prefer-doc-buffer t)
(eldoc-echo-area-use-multiline-p t))
#+end_src
* Navigation
add this hook when no eglot or tags-backend is available:
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
#+begin_src emacs-lisp
(use-package dumb-jump)
#+end_src
** Hydra
An xref-Hydra may be more general and preferrable.
#+begin_src emacs-lisp
(pretty-hydra-define hydra-dumb-jump (:color blue)
("Dumb Jump"
(
("b" dumb-jump-back "Back")
("l" dumb-jump-quick-look "Quick look")
("j" dumb-jump-go "Go")
("i" dumb-jump-go-prompt "Prompt")
("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")
)
)
)
#+end_src
* Folding
Depending on the mode, hideshow may be a good solution.
hs-minor-mode creates a menu.
Two functions are defined for cycling visibility:
#+begin_src emacs-lisp
(use-package hideshow
:hook
(prog-mode . hs-minor-mode)
:config
(defun my/hs-cycle (&optional level)
(interactive "p")
(let (message-log-max
(inhibit-message t))
(if (= level 1)
(pcase last-command
('hs-cycle
(hs-hide-level 1)
(setq this-command 'hs-cycle-children))
('hs-cycle-children
;; TODO: Fix this case. `hs-show-block' needs to be
;; called twice to open all folds of the parent
;; block.
(save-excursion (hs-show-block))
(hs-show-block)
(setq this-command 'hs-cycle-subtree))
('hs-cycle-subtree
(hs-hide-block))
(_
(if (not (hs-already-hidden-p))
(hs-hide-block)
(hs-hide-level 1)
(setq this-command 'hs-cycle-children))))
(hs-hide-level level)
(setq this-command 'hs-hide-level))))
(defun my/hs-global-cycle ()
(interactive)
(pcase last-command
('hs-global-cycle
(save-excursion (hs-show-all))
(setq this-command 'hs-global-show))
(_ (hs-hide-all))))
)
#+end_src
#+begin_src emacs-lisp
(defun wrycode/prog-cycle (&optional level)
"Emulates org-mode's tab cycling using builtin hideshow. Tries to cycle
visibility when it makes sense, and otherwise fallback to the default
indent-for-tab-command.
Basically, if your cursor is at the beginning of a line where there is
an opening block, tab will cycle visibility just like org mode
headings. It will also reset the global cycling state for
`wrycode/prog-cycle-global' for the same intuitive feel as the org-mode
cycling commands."
(interactive "p")
(let ((current-line (line-number-at-pos))
(cycled nil))
(if (bolp) ; Only attempt cycling if at the beginning of the line
(save-excursion
;; Move to the end of line to get "inside" any block that might exist on the line
(end-of-line)
;; Only proceed if hideshow finds a block that begins on this
;; line
(when (and (hs-find-block-beginning)
(= current-line (line-number-at-pos (point))))
(setq cycled t) ; Mark that we're cycling
(setq wrycode/global-cycle-state 2) ; resets global cycling state
(pcase last-command
('prog-cycle-children
(save-excursion (hs-show-block))
(setq this-command '_))
(_
;; Initial press. If block is visible, hide it. If we
;; already hid it, show level one
(if (not (hs-already-hidden-p))
(hs-hide-block)
(hs-hide-level 1)
(setq this-command 'prog-cycle-children)))))))
(unless cycled ; fall back to default tab behavior
(call-interactively 'indent-for-tab-command))))
(defvar wrycode/global-cycle-state 0)
(defun wrycode/prog-cycle-global ()
"Org-shifttab, but in prog-mode"
(interactive)
(setq wrycode/global-cycle-state (mod (1+ wrycode/global-cycle-state) 3))
(pcase wrycode/global-cycle-state
(0 (hs-hide-all))
(1 (hs-hide-level 3))
(2 (hs-show-all))))
(add-hook 'prog-mode-hook
(lambda ()
(local-set-key (kbd "TAB") 'wrycode/prog-cycle)
(local-set-key (kbd "<backtab>") 'wrycode/prog-cycle-global)))
#+end_src
*
* Outline
Outline minor mode will work for text- and prog-modes, as long as there are
suitable regexp for detecting the structure. See my TCL config for an example.
Outline mode comes in handy,
- when hideshow does not work properly
- when you aim to shift whole code blocks
#+begin_src emacs-lisp
(setq my/is-outline-overview nil)
(defun my/outline-overview ()
"Show only outline headings."
(interactive)
(outline-show-all)
(outline-hide-body)
(setq my/is-outline-overview t)
)
(defun my/outline-reset ()
"Show only outline headings."
(interactive)
(outline-show-all)
(setq my/is-outline-overview nil)
)
(defun my/toggle-outline-overview ()
"toggle outline headings."
(interactive)
(if my/is-outline-overview
(my/outline-reset)
(my/outline-overview)
)
)
#+end_src
* Narrowing
#+begin_src emacs-lisp
(defun ve/narrow-to-region-indirect (start end)
"Restrict editing in this buffer to the current region, indirectly."
(interactive "r")
(deactivate-mark)
(let ((buf (clone-indirect-buffer nil nil)))
(with-current-buffer buf
(narrow-to-region start end))
(switch-to-buffer buf)))
#+end_src
* Accessing cheat-sheets
The DP-Proxy blocks the requests.
#+begin_src emacs-lisp
(if sys/linuxp
(use-package cheat-sh)
)
#+end_src
* Bye
#+begin_src emacs-lisp
;;; programming-common-setup.el ends here
#+end_src