#env

Configurando Emacs como Java IDE

IntelliJ, Android Studio e outras IDEs são extremamente poderosas, isso é fato. Mas muitas vezes queremos leveza, um ambiente produtivo sem dezenas de features que levam minutos para carregar.

Um ambiente onde nos permite abrir o editor o mais rápido possível e apenas começar a programar!

No Emacs, podemos usar um servidor de linguagem (LSP) para ter o mínimo de IntelliSense disponível no editor. Assim como o IntelliJ, Eclipse e outras IDEs fazem.

Ele funciona graças ao frontend eglot que já vem junto com o Emacs.

Vou mostrar um exemplo de configuração no MacOS, contudo vai funcionar também em outros sistemas operacionais, desde que você tenha o jdtls em seu computador e apontando corretamente o caminho dele no script que irei mostrar no decorrer deste post.

Instalando o Java Language Server

Com o Homebrew, instale com o jdtls com o comando

brew install jdtls.

Encontre o caminho de instalação com o brew --prefix jdtls.

Se estiver em outro sistema operacional, baixe o language server diretamente do repositório oficial do jdtls.

Script jdtls

Dentro do dotfiles você encontra o script jdtls responsável por inicializar o servidor de linguagem Java a partir do workspace definido como argumento.

Os parâmetros neste script está configurado para o ambiente macOS. Logo, adapte o JDTLS_HOME para o seu sistema operacional e o -configuration se necessário.

Ainda não deu tempo de deixar o script cross-platform, uso macOS 90% do tempo. Sorry!

Coloque o script jdtls em algum lugar no PATH da sua máquina. Aqui eu copio para ~/bin e configuro ele como PATH no meu .zshrc.

# .zshrc
export PATH="~/bin:$PATH"

E os arquivos do emacs estão como links simbólicos para a HOME.

bin/jdtls # copied from dotfiles
.emacs -> /Users/tiagoaguiar/dotfiles/.emacs #symlink
.emacs.d -> /Users/tiagoaguiar/dotfiles/.emacs.d #symlink
.emacs-custom.el # (empty file for Emacs)

O workspace do projeto Java será encontrado automaticamente pelo Emacs a partir do diretório do projeto que contenha a estrutura base com um dos arquivos na raiz: .project, .classpath, pom.xml ou build.gradle.

Ou seja, se o projeto estiver na estrutura correta ele irá carregar automaticamente e iniciar o E-glot com LSP.

Dot emacs

Caso não utilize o meu .emacs, você pode configurar no seu próprio. Eu extrai somente as partes relevantes para o ambiente Java IDE no Emacs.

O gerenciador de pacotes que utilizo é o use-package.

Instale também o company para o autocomplete.

Garanta que o java esteja instalado corretamente e que o Emacs possa encontrá-lo em seu JAVA_HOME.

Por aqui eu utilizo o java 21.

;;
;; .emacs (coloque no ~/.emacs)
;;
(use-package eglot
  :config
  (setq eldoc-echo-area-use-multiline-p nil)
  (setq eglot-events-buffer-size 0)
  (add-hook 'eglot-managed-mode-hook (lambda () (eglot-inlay-hints-mode -1)))  ;; disable param hint
  :config
  (add-to-list 'eglot-server-programs
               `(java-mode . ,#'my-jdtls-command)))


;; auto complete com company
(add-hook 'java-mode-hook 'global-company-mode)

(defun my-java-project-root (&optional dir)
  (let ((start-dir (file-name-as-directory
                    (or dir
                        (when buffer-file-name
                          (file-name-directory buffer-file-name))
                        default-directory))))
    (or
     (locate-dominating-file start-dir ".project")
     (locate-dominating-file start-dir ".classpath")
     (locate-dominating-file start-dir "pom.xml")
     (locate-dominating-file start-dir "build.gradle")
     start-dir)))


(defun my-jdtls-command (&rest _)
  (let* ((root (my-java-project-root))
         (project-name (file-name-nondirectory
                        (directory-file-name root)))
         (workspace (expand-file-name
                     (concat "~/.jdtls-workspace/" project-name))))

    (message "------------------------------")
    (message "[JDTLS] Root: %s" root)
    (message "[JDTLS] Project: %s" project-name)
    (message "[JDTLS] Workspace: %s" workspace)
    (message "[JDTLS] Default dir (before): %s" default-directory)

    (make-directory workspace t)

    (message "[JDTLS] Starting server...")

    (list "jdtls" workspace)))

;; NOTE(tiago): the jdtls only works when default-directory is set to root project
;; however, if we want to keep find-file opening at current buffer dir
;; we need to restore this variable to "default" default-directory
(defun my-eglot-java-init ()
  (let ((buffer-dir default-directory)
        (project-root (my-java-project-root)))
    (setq-local my-eglot-java-buffer-directory buffer-dir)
    (setq-local default-directory project-root)
    (add-hook 'eglot-managed-mode-hook #'my-eglot-java-restore-directory nil t)
    (eglot-ensure)))

(defun my-eglot-java-restore-directory ()
  (when (bound-and-true-p my-eglot-java-buffer-directory)
    (setq-local default-directory my-eglot-java-buffer-directory)
    (kill-local-variable 'my-eglot-java-buffer-directory)
    (remove-hook 'eglot-managed-mode-hook #'my-eglot-java-restore-directory t)))

(add-hook 'java-mode-hook #'my-eglot-java-init)

(add-hook 'java-mode-hook
          (lambda ()
            (add-hook 'before-save-hook #'eglot-format-buffer nil t)))


Estrutura Java

Veja um exemplo de projeto Java com a estrutura básica (usando maven ou gradle o lsp encontrará automaticamente também).

.classpath:

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="src" path="src"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
    <classpathentry kind="output" path="bin"/>
</classpath>

.project:

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
  <name>tetris-java</name>
  <comment></comment>
  <projects>
  </projects>
  <buildSpec>
  </buildSpec>
  <natures>
      <nature>org.eclipse.jdt.core.javanature</nature>
  </natures>
  <filteredResources>
      <filter>
        <id>1775684946935</id>
        <name></name>
        <type>30</type>
        <matcher>
              <id>org.eclipse.core.resources.regexFilterMatcher</id>
              <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
        </matcher>
      </filter>
  </filteredResources>
</projectDescription>

A estrutura final do seu projeto deve ser algo parecido com o modelo abaixo:

root
├── .classpath
├── .project
└── src
    └── dev
        └── tiagoaguiar
            └── projeto
                └── App.java

Será preciso abrir o Emacs a partir do terminal para ter o JAVA_HOME disponível na GUI do Emacs.

Meu Acesso Rápido

  • Baixar o jdtls com homebrew
  • Clonar o dotfiles
  • Criar links simbólicos para ~/.emacs, ~/.emacs.d
  • Copiar o script jdtls para ~/bin
  • Garantir que ~/bin esteja no PATH
  • Estruturar projeto java com .project e .classpath na raiz