dotfiles/emacs/.emacs.d/config/init-ide.el

378 lines
16 KiB
EmacsLisp

;; -*- lexical-binding: t; -*-
;; IDE features
;; Corfu-mode provides inline autocompletion
(use-package corfu
:straight (:files (:defaults "extensions/corfu-echo.el"))
:init
(global-corfu-mode)
(corfu-echo-mode)
:config
(defun corfu-move-to-minibuffer ()
(interactive)
(pcase completion-in-region--data
(`(,beg ,end ,table ,pred ,extras)
(let ((completion-extra-properties extras)
completion-cycle-threshold completion-cycling)
(consult-completion-in-region beg end table pred)))))
(defun corfu-enable-in-minibuffer ()
"Enable Corfu in the minibuffer if `completion-at-point' is bound."
(when (where-is-internal #'completion-at-point (list (current-local-map)))
(setq-local corfu-echo-delay nil ;; Disable automatic echo and popup
corfu-popupinfo-delay nil)
(corfu-mode 1)))
(add-hook 'minibuffer-setup-hook #'corfu-enable-in-minibuffer)
:general
(corfu-map "S-SPC" #'corfu-insert-separator
"M-m" #'corfu-move-to-minibuffer)
:custom-face
(corfu-echo ((t :inherit default))))
;; Quick file overview for supported modes
(use-package imenu
:straight (:type built-in)
:general
(leader-map "m" #'imenu))
;; Find definition/references
(use-package xref
:straight (:type built-in)
:general
(normal "M-," #'xref-pop-marker-stack)
(normal "M-r" #'xref-find-references)
:custom
(xref-prompt-for-identifier nil))
;; Inline syntax checking
(use-package flymake
:straight (:type built-in)
:config
(defvar flymake-map (make-sparse-keymap)
"Keymap for flymake commands.")
(general-def flymake-map
"n" #'flymake-goto-next-error
"p" #'flymake-goto-prev-error
"b" #'flymake-show-diagnostics-buffer
"l" #'consult-flymake
"a" #'flymake-show-project-diagnostics)
(leader-def-key "e" flymake-map)
:hook (prog-mode . flymake-mode))
;; LSP client
(use-package eglot
:commands (eglot)
:config
(add-to-list 'eglot-stay-out-of 'flymake)
(defun my-eglot-managed-hook ()
(if (eglot-managed-p)
(setq-local corfu-preview-current nil)
(kill-local-variable 'corfu-preview-current))
(add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t))
(add-hook 'eglot-managed-mode-hook #'my-eglot-managed-hook)
(add-to-list 'eglot-server-programs
'(js-web-mode . ("typescript-language-server" "--stdio")))
(add-to-list 'eglot-server-programs
`(html-web-mode . ,(eglot-alternatives
'(("vscode-html-language-server" "--stdio")
("html-languageserver" "--stdio")))))
(add-to-list 'eglot-server-programs
`((ruby-mode ruby-ts-mode) . ,(eglot-alternatives
'(("srb" "typecheck" "--lsp")
("solargraph" "socket" "--port" :autoport)))))
;; Sorbet passes an out-of-spec key in its connection JSON, filter it out to prevent eglot blowing up
(cl-defun jsonrpc-connection-receive--filter-args ((conn foreign-message) &rest args)
(let ((msg (cl-loop for (k v) on foreign-message by #'cddr
if (-contains? '(:method :id :error :params :result :jsonrpc) k)
collect k and collect v)))
(append (list conn msg) args)))
(advice-add 'jsonrpc-connection-receive :filter-args #'jsonrpc-connection-receive--filter-args)
;; Astro
(add-to-list 'eglot-server-programs
'(astro-ts-mode . ("astro-ls" "--stdio"
:initializationOptions
(:typescript (:tsdk "./node_modules/typescript/lib")))))
;; Custom eglot java server for deeper customization
(defvar eglot-java-java-agent nil
"Java agent JVM arg for eglot JDTLS.")
(defclass eglot-java-server (eglot-lsp-server) ()
:documentation "Eglot integration with JDTLS.")
(defun eglot-java-contact (&optional interactive)
`(eglot-java-server .
("jdtls" ,(if (s-blank? eglot-java-java-agent)
""
(format "--jvm-arg=-javaagent:%s" eglot-java-java-agent))
:initializationOptions
(:extendedClientCapabilities (:classFileContentsSupport t)
:settings
(:java
(:home "/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home"
:configuration
(:runtimes [(:name "JavaSE-11"
:path "/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk")
(:name "JavaSE-17"
:path "/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk"
:default t)])))))))
(add-to-list 'eglot-server-programs '(java-mode . eglot-java-contact))
;; Fix JDTLS's weird handling of workspaceEdit
(cl-defmethod eglot-execute-command
((_server eglot-java-server) (_cmd (eql java.apply.workspaceEdit)) arguments)
"Eclipse JDT breaks spec and replies with edits as arguments."
(mapc #'eglot--apply-workspace-edit arguments))
;; Support jdtls' ability to jump into class files
(define-advice file-relative-name
(:before-until (filename &optional directory) jdt-uri)
(when (string-match-p "\\`jdt://" filename)
filename))
(define-hash-table-test 'jdt-file-to-uri-hash 'equal 'sxhash-equal)
(defvar eglot-java--jdt-file-to-uri (make-hash-table :test 'jdt-file-to-uri-hash)
"Internal variable to map temporary filepaths to JDT URIs.")
(define-advice eglot--path-to-uri
(:before-until (path) jdt-path)
(gethash (file-truename path) eglot-java--jdt-file-to-uri))
(defun jdt-class-file-name-handler (operation &rest args)
"File name handler for jdtls' `jdt://' URIs."
(cond
((eq operation 'temporary-file-directory) temporary-file-directory)
((member operation '(file-remote-p
file-name-case-insensitive-p
make-auto-save-file-name
find-file-backup-name)) nil)
(t (let* ((uri (car args))
(uri-hash (secure-hash 'md5 uri))
(filename (save-match-data
(string-match "jdt://contents/\\(.*?\\)/\\(.*?\\)\.class\\?" uri)
(format "%s.java"
(replace-regexp-in-string "/" "." (match-string 2 uri) t t))))
(temp-dir (expand-file-name "eglot-jdtls" temporary-file-directory))
(uri-temp-dir (expand-file-name uri-hash temp-dir))
(filepath (concat (file-name-as-directory uri-temp-dir) filename))
(metadata-path (format "%s.%s.metadata"
(file-name-directory filepath)
(file-name-base filepath))))
(unless (gethash (file-truename filepath) eglot-java--jdt-file-to-uri)
(puthash (file-truename filepath) uri eglot-java--jdt-file-to-uri))
(unless (or (file-readable-p filepath) (not (eglot-current-server)))
(let ((contents
(jsonrpc-request (eglot-current-server)
:java/classFileContents
(list :uri uri))))
(unless (file-directory-p uri-temp-dir) (make-directory uri-temp-dir t))
(with-temp-file filepath (insert contents))
(with-temp-file metadata-path (insert uri))))
(cond
((eq operation 'get-file-buffer) (get-buffer filename))
((member operation '(expand-file-name
directory-file-name
file-truename
file-name-directory
file-name-nondirectory)) (apply operation filepath (cdr args)))
((eq operation 'insert-file-contents)
(seq-let (uri visit beg end replace) args
(let ((content (with-temp-buffer
(insert-file-contents filepath nil beg end replace)
(buffer-substring (point-min) (point-max)))))
(insert content)
(when visit
(set-visited-file-name filepath t)
(set-buffer-modified-p nil)
(read-only-mode))
(list uri (length content)))))
(t (let ((inhibit-file-name-handlers
(cons 'jdt-class-file-name-handler
(and (eq inhibit-file-name-operation operation)
inhibit-file-name-handlers)))
(inhibit-file-name-operation operation))
(apply operation args))))))))
(setq eglot-extend-to-xref t)
(add-to-list 'file-name-handler-alist '("\\`jdt://" . jdt-class-file-name-handler))
(add-to-list 'auto-mode-alist '("\\`jdt://" . java-mode))
;; Java build command
(defun eglot-java-maven-build ()
(interactive)
(if-let ((pom-path (locate-dominating-file default-directory "pom.xml")))
(let ((pom-uri (eglot--path-to-uri pom-path)))
(jsonrpc-notify
(eglot--current-server-or-lose)
:java/projectConfigurationUpdate
(list :uri pom-uri))
(jsonrpc-notify
(eglot--current-server-or-lose)
:java/buildWorkspace
'((:json-false))))
(user-error "This doesn't appear to be a Maven project, could not find pom.xml.")))
:init
(defvar eglot-prefix-map (make-sparse-keymap)
"Prefix keymap for eglot commands.")
(general-def eglot-prefix-map
"a" #'eglot-code-actions
"R" #'eglot-reconnect
"S" #'eglot-shutdown
"r" #'eglot-rename
"f" #'eglot-format-buffer
"i" #'eglot-code-action-organize-imports
"h" #'eldoc
"e" #'flymake-show-buffer-diagnostics
"E" #'flymake-show-project-diagnostics)
:general
(eglot-mode-map "C-c l" `(,eglot-prefix-map :which-key "eglot"))
:hook
(java-mode . eglot-ensure)
(rust-mode . eglot-ensure)
(js-mode . eglot-ensure)
(typescript-mode . eglot-ensure)
(python-mode . eglot-ensure)
(js-web-mode . eglot-ensure)
(html-web-mode . eglot-ensure)
(scala-mode . eglot-ensure)
(c-mode . eglot-ensure)
(terraform-mode . eglot-ensure)
(ruby-mode . eglot-ensure)
(ruby-ts-mode . eglot-ensure)
(sh-mode . eglot-ensure)
(bash-ts-mode . eglot-ensure)
(astro-ts-mode . eglot-ensure)
:custom
(eglot-confirm-server-initiated-edits nil)
(eglot-connect-timeout nil))
;; Debug adapter protocol
(use-package dape
:config
(add-to-list 'dape-configs
'(rdbg
modes (ruby-mode ruby-ts-mode)
ensure dape-ensure-command
command "bundle"
command-args ("exec" "rdbg" "-O" "--host" "0.0.0.0" "--port" :autoport "-c" "--" :-c)
fn (lambda (config)
(plist-put config 'command-args
(mapcar (lambda (arg)
(if (eq arg :-c)
(plist-get config '-c)
arg))
(plist-get config 'command-args))))
port :autoport
command-cwd dape-command-cwd
:type "Ruby"
;; -- examples:
;; rails server
;; bundle exec ruby foo.rb
;; bundle exec rake test
-c (lambda ()
(format "ruby %s"
(or (dape-buffer-default) ""))))))
;; Some compilation-mode conveniences
(use-package compile
:straight (:type built-in)
:commands compile
:config
(defun postprocess-compilation-buffer ()
(goto-char compilation-filter-start)
(when (looking-at "\033c")
(delete-region (point-min) (match-end 0)))
(ansi-color-apply-on-region (point) (point-max)))
(add-hook 'compilation-filter-hook 'postprocess-compilation-buffer))
;; Code formatting library
(use-package apheleia
:straight (apheleia :host github :repo "raxod502/apheleia")
:commands apheleia-format-buffer
:autoload apheleia--get-formatters
:config
(add-to-list 'apheleia-mode-alist '(ruby-mode . (rubocop)))
(add-to-list 'apheleia-mode-alist '(ruby-ts-mode . (rubocop))))
(defun apheleia ()
"Format the region or current buffer using Apheleia."
(interactive)
(if (not (region-active-p))
(call-interactively #'apheleia-format-buffer)
(let* ((buf (current-buffer))
(name (buffer-file-name))
(temp-file (make-temp-file "apheleia" nil (format ".%s" (file-name-extension name))))
(temp-buffer (find-file-noselect temp-file))
(formatters (apheleia--get-formatters))
(line (line-number-at-pos))
(col (current-column))
(start (region-beginning))
(end (region-end)))
(with-current-buffer temp-buffer
(erase-buffer)
(insert-buffer-substring buf start end)
(write-file temp-file)
(apheleia-format-buffer formatters
(lambda ()
(with-current-buffer buf
(delete-region start end)
(insert (with-current-buffer temp-buffer
(buffer-substring-no-properties (point-min) (point-max))))
(goto-char (point-min))
(forward-line (1- line))
(move-to-column col)
(delete-file temp-file)
(when (get-buffer temp-buffer)
(with-current-buffer temp-buffer
(set-buffer-modified-p nil))
(kill-buffer temp-buffer))))
:callback (lambda (&rest args)
(when-let ((error (plist-get args :error)))
(delete-file temp-file)
(when (get-buffer temp-buffer)
(with-current-buffer temp-buffer
(set-buffer-modified-p nil))
(kill-buffer temp-buffer))
(error "Formatting failed: %s" error)))))))
)
(keymap-set prog-mode-map "C-c f" #'apheleia)
;; AI assistance
(use-package copilot
:straight (:host github :repo "copilot-emacs/copilot.el" :files ("dist" "*.el"))
:hook ((prog-mode . copilot-mode)
(yaml-mode . copilot-mode)
(yaml-ts-mode . copilot-mode)
(json-mode . copilot-mode)
(json-ts-mode . copilot-mode)
(forge-post-mode . copilot-mode)
(git-commit-mode . copilot-mode))
:config
(add-to-list 'warning-suppress-types '(copilot))
(add-to-list 'copilot-major-mode-alist '("forge-post" . "markdown"))
(add-to-list 'copilot-major-mode-alist '("git-commit" . "markdown"))
:general
(prog-mode-map "C-c <tab>" #'copilot-complete)
(copilot-completion-map "C-n" #'copilot-next-completion
"C-p" #'copilot-previous-completion
"C-<tab>" #'copilot-accept-completion
"C-M-<tab>" #'copilot-accept-completion-by-word
"C-S-<tab>" #'copilot-accept-completion-by-line
"C-g" #'copilot-clear-overlay))
;; Debugger interface
(use-package realgud
:defer t)
(use-package realgud-jdb
:commands realgud:jdb-maven
:config
(defun realgud:jdb-maven ()
"Runs realgud:jdb, setting the classpath from Maven."
(interactive)
(let ((default-directory (project-root (project-current))))
(when (not (locate-dominating-file default-directory "pom.xml"))
(user-error "Not a Maven project."))
(with-env `(("CLASSPATH" .
,(s-trim
(shell-command-to-string
"mvn -q exec:exec -Dexec.executable=echo -Dexec.args=\"%classpath\""))))
(call-interactively #'realgud--jdb)))))
(provide 'init-ide)