378 lines
16 KiB
EmacsLisp
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)
|