A few months back I posted about using meghanada to provide support for working on my team’s Java projects. Meghanada has served me well so far, but I sorely missed some of the creature comforts that Eclim gave me, namely import management and some basic refactoring tooling.

So, when I saw an announcment in one of the internal mailing lists I’m subscribed to about a script that generates the necessary configuration for Eclipse so as to work with our build tooling, I jumped into the chance of trying out LSP again, particularly using the Eclipse JDT LSP server. I honestly could have moved back to Eclim, seeing as it’s more mature and I’ve been using it for quite a while now – however, having a more seamless editing experience seemed like a big enough that it made sense for me to learn to use lsp-mode: my biggest pain point was having to start eclimd or Eclipse before opening anything in Emacs, and I have had problems sometimes running eclimd on some of the codebases I’ve worked on.

The other bit that always annoyed me with Eclim though was having to install a host Eclipse installation somewhere. This would get fiddly sometimes: I’d upgrade emacs-eclim, and find that it’d be incompatible with the version of Eclim I had configured in my host Eclipse installation, necessitating having to reinstall/upgrade it. So, having one less moving part is a huge win for me.

I also previously had problems with using Eclipse (in general) on our internal projects, which I suspect are likely related to classpath configuration issues and some other quirks of how our build system works. That said, the script I mentioned does generate the proper entries, although some of the entries it generates are just slightly incorrect – I mean, they point to build-time artifacts and not to the actual project sources, which complicates things a bit, in that I’ll need to run our build outside of the Eclipse tooling before running the LSP server. I’ll probably work on that some time in the future…

Anyway, I did get lsp-mode and lsp-java to work with our projects with minimal fuss, admittedly – with the help of said script – and I’ve been using it for the past month now. Compared to Eclim, lsp-mode and lsp-java gives me the same niceties: I get symbol renaming and other refactoring tools, and I get formatting and import organization. The one thing LSP integration has over Eclim though is how fairly low-touch it is. I had never quite gotten Emacs to launch eclimd upon browsing to a Java source file, which usually meant I had to launch eclimd separately in a terminal somewhere. With lsp-java, I didn’t need to install the Eclipse JDT language server separately: it was installed automagically in my ~/.emacs.d/ by lsp-java, and it’s automatically started by lsp-java upon enabling lsp-mode in a Java source buffer. Doubleplus good.

If it were just the refactoring tools though, lsp-mode wouldn’t be much of a difference from meghanada, even if it meghanada didn’t have those tools: after all, I’ve lived without those niceties for a while now, and I did have some low-tech solutions I could lean on if push comes to shove (sed and a bunch of shell one-liners come to mind). What got me sold completely are cross references.

I’ve never really used xref and friends in Emacs: I do know about ETAGS and friends, and I once tried setting that up years ago. I gave up on it, as I had difficulty figuring out how to keep my source’s ETAGS consistent with changes to my source code, or to integrate it seamlessly with build tooling. I’ll probably revisit it someday, maybe in other projects where there isn’t an appropriate LSP server. But with lsp-mode, lsp-java, and the Eclipse JDT language server, I don’t really need to worry about all of that: cross-referencing symbols is straightforward and Just Works, particularly when used with lsp-ui, which provides a host of goodies as well:

  (define-key lsp-ui-mode-map
    [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
  (define-key lsp-ui-mode-map
    [remap xref-find-references] #'lsp-ui-peek-find-references)

Admittedly, lsp-mode does have integration with xref, so the above config isn’t entirely necessary. Replacing the bindings for xref-find-definitions and xref-find-references when lsp-ui is active however means I do get some interesting lsp-ui affordances, especially when trying to find, for instance, callers of a method.

Completion is also in my experience much quicker than in meghanada; there’s some improvement though with the quality of completions, compared to say IntelliJ’s completions. That said, it’s definitely better than meghanada, so I’m not complaining much.

The config in my .emacs is also pretty straightforward. I have lsp-mode set up so I use flycheck instead of flymake, and I load lsp-ui:

(use-package lsp-mode
  :init
  (setq lsp-prefer-flymake nil)
  :demand t
  :after jmi-init-platform-paths)

(use-package lsp-ui
  :config
  (setq lsp-ui-doc-enable nil
        lsp-ui-sideline-enable nil
        lsp-ui-flycheck-enable t)
  :after lsp-mode)

(use-package dap-mode
  :config
  (dap-mode t)
  (dap-ui-mode t))

I’ve tweaked my lsp-java settings though to better reflect how I prefer things, but YMMV:

(use-package lsp-java
  :init
  (defun jmi/java-mode-config ()
    (setq-local tab-width 4
                c-basic-offset 4)
    (toggle-truncate-lines 1)
    (setq-local tab-width 4)
    (setq-local c-basic-offset 4)
    (lsp))

  :config
  ;; Enable dap-java
  (require 'dap-java)

  ;; Support Lombok in our projects, among other things
  (setq lsp-java-vmargs
        (list "-noverify"
              "-Xmx2G"
              "-XX:+UseG1GC"
              "-XX:+UseStringDeduplication"
              (concat "-javaagent:" jmi/lombok-jar)
              (concat "-Xbootclasspath/a:" jmi/lombok-jar))
        lsp-file-watch-ignored
        '(".idea" ".ensime_cache" ".eunit" "node_modules"
          ".git" ".hg" ".fslckout" "_FOSSIL_"
          ".bzr" "_darcs" ".tox" ".svn" ".stack-work"
          "build")

        lsp-java-import-order '["" "java" "javax" "#"]
        ;; Don't organize imports on save
        lsp-java-save-action-organize-imports nil

        ;; Formatter profile
        lsp-java-format-settings-url
        (concat "file://" jmi/java-format-settings-file))

  :hook (java-mode   . jmi/java-mode-config)

  :demand t
  :after (lsp lsp-mode dap-mode jmi-init-platform-paths))

It’s not a panacea however. As much as I wish that there’s some magic I could introduce, every new project directory needs me generating the .classpath and .project entries needed by the language server for it to understand where everything goes: admittedly, there’s some support for Gradle and Maven projects in the language server, so I suspect that it might be easy to fork it and add more direct support for our internal build tooling instead. Might serve as an interesting internal side project for me.

There’s also the fact that a single server instance supports only a single “workspace”, which complicates matters a bit when it comes to source repositories in different contexts.

That all said, I’m not looking back. Considering how nice it’s been using it to edit my Java projects, I’ve started to explore other language servers and integrating those into my environment; eventually, I might even get rid of some of my flycheck configurations!

LSP is a pretty nifty idea, I think, as it does greatly simplify a lot. Anything that keeps me productive editing code in Emacs is always a good thing.

Previously: print('foo')