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')