| 1 | ;;; init-lang.el -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;;; Syntax highlighting: tree-sitter |
| 4 | (setopt treesit-enabled-modes t |
| 5 | treesit-auto-install-grammar 'always |
| 6 | treesit-font-lock-level 4) ; 4 = everything (calls, properties, operators) |
| 7 | |
| 8 | ;;; LSP: eglot |
| 9 | (defvar my/lsp-servers |
| 10 | '(((typescript-ts-mode tsx-ts-mode js-ts-mode) "vtsls" "--stdio") |
| 11 | (rust-ts-mode "rust-analyzer") |
| 12 | ((go-ts-mode go-mod-ts-mode) "gopls") |
| 13 | ((c-ts-mode c++-ts-mode) "clangd") |
| 14 | (lua-ts-mode "lua-language-server") |
| 15 | ;;(css-ts-mode "vscode-css-language-server" "--stdio") |
| 16 | ((mhtml-ts-mode html-ts-mode) "vscode-html-language-server" "--stdio") |
| 17 | (json-ts-mode "vscode-json-language-server" "--stdio") |
| 18 | (python-ts-mode "pylsp") |
| 19 | (python-ts-mode "basedpyright-langserver" "--stdio") ; wins if both installed |
| 20 | (bash-ts-mode "bash-language-server" "start") |
| 21 | (scheme-mode "guile-lsp-server")) |
| 22 | "Alist of (MODES SERVER ARGS...) used to auto-start eglot.") |
| 23 | |
| 24 | (use-package eglot |
| 25 | :ensure nil |
| 26 | :defer t |
| 27 | :custom |
| 28 | (eglot-autoshutdown t) ; kill server when last buffer closes |
| 29 | (eglot-events-buffer-config '(:size 0 :format full)) ; no event logging |
| 30 | (eglot-extend-to-xref t) ; stay in eglot when jumping into deps |
| 31 | :config |
| 32 | (dolist (entry my/lsp-servers) |
| 33 | (when (executable-find (cadr entry)) |
| 34 | (add-to-list 'eglot-server-programs |
| 35 | (cons (ensure-list (car entry)) (cdr entry)))))) |
| 36 | |
| 37 | ;; Hooks go in now so eglot itself only loads when a matching buffer opens |
| 38 | (dolist (entry my/lsp-servers) |
| 39 | (when (executable-find (cadr entry)) |
| 40 | (dolist (mode (ensure-list (car entry))) |
| 41 | (add-hook (intern (format "%s-hook" mode)) #'eglot-ensure)))) |
| 42 | |
| 43 | ;;; Indentation per language |
| 44 | (defvar my/indent-settings |
| 45 | '(((typescript-ts-mode tsx-ts-mode) 2 nil typescript-ts-mode-indent-offset) |
| 46 | (js-ts-mode 2 nil js-indent-level) |
| 47 | (rust-ts-mode 4 nil rust-ts-mode-indent-offset) |
| 48 | (go-ts-mode 4 t go-ts-mode-indent-offset) |
| 49 | ((c-ts-mode c++-ts-mode) 4 nil c-ts-mode-indent-offset) |
| 50 | (lua-ts-mode 2 nil lua-ts-indent-offset) |
| 51 | (css-ts-mode 2 nil css-indent-offset) |
| 52 | ((mhtml-ts-mode html-ts-mode) 2 nil html-ts-mode-indent-offset) |
| 53 | (json-ts-mode 2 nil json-ts-mode-indent-offset) |
| 54 | (python-ts-mode 4 nil python-indent-offset) |
| 55 | (bash-ts-mode 2 nil sh-basic-offset) |
| 56 | ((emacs-lisp-mode scheme-mode) 2 nil nil)) |
| 57 | "List of (MODES WIDTH TABS OFFSET-VAR) applied in each mode's hook.") |
| 58 | |
| 59 | (dolist (entry my/indent-settings) |
| 60 | (pcase-let ((`(,modes ,width ,tabs ,var) entry)) |
| 61 | (dolist (mode (ensure-list modes)) |
| 62 | (add-hook (intern (format "%s-hook" mode)) |
| 63 | (lambda () |
| 64 | (setq-local tab-width width |
| 65 | indent-tabs-mode tabs) |
| 66 | (when var (set (make-local-variable var) width))))))) |
| 67 | |
| 68 | ;;; Format on save |
| 69 | ;; Uses eglot when the server can format, else re-indents lisp buffers. Off for a |
| 70 | ;; buffer with SPC c F, or per project via .dir-locals.el: |
| 71 | (defvar my/format-on-save t |
| 72 | "Non-nil formats eglot-managed buffers before saving.") |
| 73 | (put 'my/format-on-save 'safe-local-variable #'booleanp) |
| 74 | |
| 75 | (defvar my/reindent-modes '(emacs-lisp-mode scheme-mode) |
| 76 | "Modes without an LSP formatter that format by re-indenting the buffer.") |
| 77 | |
| 78 | (defun my/format-buffer () |
| 79 | "Format with eglot if the server can, else re-indent in `my/reindent-modes'." |
| 80 | (interactive) |
| 81 | (cond ((and (fboundp 'eglot-managed-p) (eglot-managed-p) |
| 82 | (eglot-server-capable :documentFormattingProvider)) |
| 83 | (eglot-format-buffer)) |
| 84 | ((apply #'derived-mode-p my/reindent-modes) |
| 85 | (indent-region (point-min) (point-max))) |
| 86 | ((called-interactively-p 'any) |
| 87 | (message "No formatter for %s" major-mode)))) |
| 88 | |
| 89 | (defun my/format-on-save () |
| 90 | (when my/format-on-save (my/format-buffer))) |
| 91 | |
| 92 | (add-hook 'before-save-hook #'my/format-on-save) |
| 93 | |
| 94 | (defun my/toggle-format-on-save () |
| 95 | "Toggle format on save for the current buffer." |
| 96 | (interactive) |
| 97 | (setq-local my/format-on-save (not my/format-on-save)) |
| 98 | (message "Format on save %s" (if my/format-on-save "on" "off"))) |
| 99 | |
| 100 | ;; Diagnostics |
| 101 | (setq flymake-show-diagnostics-at-end-of-line 'short) |
| 102 | |
| 103 | (with-eval-after-load 'general |
| 104 | (my/leader |
| 105 | "c" '(:ignore t :wk "code") |
| 106 | "ca" '(eglot-code-actions :wk "actions") |
| 107 | "cr" '(eglot-rename :wk "rename") |
| 108 | "cf" '(my/format-buffer :wk "format") |
| 109 | "cF" '(my/toggle-format-on-save :wk "toggle format on save") |
| 110 | "cd" '(xref-find-definitions :wk "definition") |
| 111 | "cR" '(xref-find-references :wk "references") |
| 112 | "ci" '(eglot-find-implementation :wk "implementation") |
| 113 | "ct" '(eglot-find-typeDefinition :wk "type def") |
| 114 | "ce" '(flymake-show-buffer-diagnostics :wk "errors") |
| 115 | "cE" '(flymake-show-project-diagnostics :wk "project errors") |
| 116 | "cn" '(flymake-goto-next-error :wk "next error") |
| 117 | "cp" '(flymake-goto-prev-error :wk "prev error") |
| 118 | "cl" '(eglot :wk "start lsp") |
| 119 | "cL" '(eglot-shutdown :wk "stop lsp"))) |
| 120 | |
| 121 | (provide 'init-lang) |