This article is the 24th day article of Emacs Advent Calandar 2018.
Hi, it ’s me. This time, I would like to write after the story of writing Java in Emacs that I wrote earlier. See below for the story of writing Java in previous Emacs. http://qiita.com/mopemope/items/d1658a4ac72d85db9ccf Also, I would like to write a little about the parts that I couldn't write before.
To write Java in Emacs, you have to cross the Acheron River with Karon and travel through several hellish hierarchies.
And before that, I would like to write a little about lsp-mode
.
Recently, there is a trend to use LSP in Emacs as well.
LSP, Language Server Protocol is editor independent
It's like standardizing the APIs that connect the back-end server and the front-end editor.
In other words, you will be able to convert your favorite editor into an IDE.
You can use lsp-mode or egolot in Emacs.
Among them, lsp-mode
is provided by a more customized package for each language.
Java provides a package called lsp-java.
Previously, back-end server installation was not automated and the installation cost was high, but now it is automatic installation.
Is also supported and installation has become easier.
And the backend of lsp-java is Eclipse. Since the part that speaks LSP is added to Eclipse, the functions and achievements of Eclipse can be used as it is.
It's very powerful.
Therefore, when writing Java in Emacs, I think that lsp-mode is often used.
However, it is not the case that we are developing including the back end.
And lsp-mode is very much influenced by VS Code, including the UI, and it doesn't seem to follow the Emacs Way.
The meghanada-mode I am developing is developed in a way that follows the Emacs Way as much as possible.
Therefore, it can be used with Emacs without any discomfort. Since the back end is also developed by ourselves, we can respond flexibly.
Let's talk about hell.
By the way, in the version introduced before, all type analysis was implemented by myself.
What class is the symbol type on the source? I was just analyzing the information, but it was difficult because of speed problems and the wall of Generisc.
Not only simple symbols, but also the return value of each method call must be parsed for completion to work.
Many of the causes of difficulty are the lambda
and Stream
API collect
being confusing.
The definition of collect
is as follows.
<R,A> R collect(Collector<? super T,A,R> collector)
A common pattern is to apply the method of the Collectors
class to this, but the left side cannot be known unless the type of the method inside is resolved first. It may be simple, but in some cases, there are also things such as groupingBy
that take lambda
as an argument, which easily opens the hell gate.
static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
When four Type Parameters appear, the spiciness goes up.
Also, the following pattern is a painful case with lambda
.
.map(m -> {
if (m.isActive()) {
return b;
}
return a;
})
.filter(s -> s.enable())
Here, if ʻaand
b are of different types, the return value of
map must derive a class that is a common term of ʻa
and b
. You also have to consider the presence or absence of block
and method refernce
.
To be honest, it's a difficult story to analyze this.
So what's the best way to solve this problem?
The answer is simple. The compiler should know all the types, so all you have to do is get the type analysis results from the compiler.
meghanada
lets the compiler do the type parsing.
Access the ʻASTspit out by the compiler, read the
type symbolhanging from it, and get the type of each symbol, the return type of the method, and so on. In many development environments, when saving a file, analysis and compilation are performed, but by doing this at the same time, efficiency and speed are increased. (Although the compilation speed is reduced by the amount of analysis, it is overwhelmingly faster than individual analysis) As a result, even with the
flycheck` check, type checking and compilation errors can be displayed smoothly.
The analysis time takes about 200ms, but since it is executed asynchronously, it doesn't bother me much.
By knowing the type, if the type on the left side can be read, the completion candidates that match the type on the left side are preferentially displayed.
In the case of gradle
project, I get the model of gradle
from gradle tooling API
and get the dependency, source path, etc. from it. To use gradle tooling API
, gradle daemon You need to launch
.
It may not bother you in a small project, but in a large project such as ʻelasticsearch, it takes a considerable amount of time just to load and evaluate the build file when launching this daemon. In addition, it is necessary to analyze and consider the build order in consideration of the dependency between subprojects. Since it is a daemon, it seems that it takes time only at the first startup, but if it takes time for each startup, it is quite severe.
meghanadaholds project dependencies etc. as independent intermediate data. The result of the build file once analyzed is converted to this data class and cached. (This is also because it supports multiple build tools) After analyzing at the first startup, the analyzed cache will be used at the next startup. The
gradle tooling API` is not used until it is needed, so it can be started quickly.
Thanks to the adoption of intermediate data, it is now possible to support various projects such as Maven, Gradle, Eclipse, and original projects.
Since it is not based on Eclipse, meghanada actually supports Android projects as well.
Formatting is also an annoying problem. Previously it only supported Google Java Format, but now it also supports Eclipse Format.
As with the formatter type, more importantly, how can the buffer you're editing be formatted comfortably when you save it? is.
Regarding this, go-mode
was very helpful.
I am trying to apply the patch so as not to shift the cursor from the patch generated by diff as much as possible.
I was writing the Google Assistant app on Android Things, which was also written on Emacs.
(defun meghanada--apply-rcs-patch (patch-buffer)
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer."
(let ((target-buffer (current-buffer))
;; Relative offset between buffer line numbers and line numbers
;; in patch.
;;
;; Line numbers in the patch are based on the source file, so
;; we have to keep an offset when making changes to the
;; buffer.
;;
;; Appending lines decrements the offset (possibly making it
;; negative), deleting lines increments it. This order
;; simplifies the forward-line invocations.
(line-offset 0))
(save-excursion
(with-current-buffer patch-buffer
(goto-char (point-min))
(while (not (eobp))
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
(error "Invalid rcs patch or internal error in apply-rcs-patch"))
(forward-line)
(let ((action (match-string 1))
(from (string-to-number (match-string 2)))
(len (string-to-number (match-string 3))))
(cond
((equal action "a")
(let ((start (point)))
(forward-line len)
(let ((text (buffer-substring start (point))))
(with-current-buffer target-buffer
(cl-decf line-offset len)
(goto-char (point-min))
(forward-line (- from len line-offset))
(insert text)))))
((equal action "d")
(with-current-buffer target-buffer
(meghanada--goto-line (- from line-offset))
(cl-incf line-offset len)
(meghanada--delete-whole-line len)))
(t
(error "Invalid rcs patch or internal error in apply-rcs-patch")))))))))
This is the biggest problem. When converting Emacs to an IDE, what supports everything is how to handle the interaction with the back end. Vim now supports asynchronous communication, but Emacs has long supported asynchronous communication. This is what makes me use Emacs. By utilizing this asynchronous communication, it is possible to smoothly interact with the back-end server and perform powerful editing processing without interfering with the editing process. This part is all inspired by irony. Without irony-mode I wouldn't have created this package either. The general flow is as follows.
2-9 run asynchronously on the backend. Numbering is done at the time of request. This is asynchronous processing, and it corresponds to the case when there is a request that overtakes the previous request and returns a response quickly. (For the time being, meghanada also synchronizes asynchronous processing so that it can be processed simply) The commands are executed asynchronously sequentially so that they do not interfere with editing in the editor. In some cases it is better to run synchronously, so we support both.
Well, we have been developing it since 2016, but the processing speed and accuracy have improved significantly compared to before. The ease of installation and configuration has long been evaluated. How about using it if you think that lsp-java does not fit?
Recommended Posts