;; -*- 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-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) (tsx-ts-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-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 " #'copilot-complete) (copilot-completion-map "C-n" #'copilot-next-completion "C-p" #'copilot-previous-completion "C-" #'copilot-accept-completion "C-M-" #'copilot-accept-completion-by-word "C-S-" #'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)