This section provides step-by-step instructions for contributing an LSP language server in your IntelliJ plugin.
The first step is to reference LSP4IJ. LSP4IJ uses com.redhat.devtools.lsp4ij
as plugin Id.
You need to declare dependency in your plugin.xml like this:
<idea-plugin>
...
<depends>com.redhat.devtools.lsp4ij</depends>
...
</idea-plugin>
LSP4IJ depends on Eclipse LSP4J (Java binding for the Language Server Protocol and the Debug Adapter Protocol.). It provides its own version of LSP4J and its classes are loaded in the LSP4IJ plugin class loader.
If your IntelliJ Plugin contributes a Java-based language server, it must not embed its own version of LSP4J, in order to avoid conflicts with the version provided by
LSP4IJ. Failing to do so will result in ClassCastException
errors to be thrown.
Make sure that the LSP4J dependency in your plugin is either declared with a runtimeOnly
scope or excluded entirely,
if it's referenced as a transitive dependency.
Here is an example from the build.gradle.kts of the Quarkus Tools project, excluding the LSP4J dependency from the Qute Language Server:
implementation("com.redhat.microprofile:com.redhat.qute.ls:0.17.0) {
exclude("org.eclipse.lsp4j")
}
Create an implementation
of LanguageServerFactory
to expose your my.language.server.MyLanguageServer
,
implementing StreamConnectionProvider
:
package my.language.server;
import com.intellij.openapi.project.Project;
import com.redhat.devtools.lsp4ij.LanguageServerFactory;
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl;
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider;
import org.jetbrains.annotations.NotNull;
public class MyLanguageServerFactory implements LanguageServerFactory {
@Override
public @NotNull StreamConnectionProvider createConnectionProvider(@NotNull Project project) {
return new MyLanguageServer(project);
}
@Override //If you need to provide client specific features
public @NotNull LanguageClientImpl createLanguageClient(@NotNull Project project) {
return new MyLanguageClient(project);
}
@Override //If you need to expose a custom server API
public @NotNull Class<? extends LanguageServer> getServerInterface() {
return MyCustomServerAPI.class;
}
}
If you need to provide client specific features (e.g. commands), you can override the createLanguageClient
method to
return your custom LSP client implementation.
If you need to expose a custom server API, i.e. custom commands supported by your language server, you can override
the createLanguageClient
method to return a custom interface extending LSP4J's LanguageServer API.
Your MyLanguageServer
needs to implement
the StreamConnectionProvider
API which :
- manages the language server lifecycle (start/stop)
- returns the input/error stream of LSP requests, responses, notifications.
Frequently, a language server process is started through a runtime like Java, Node.js, etc. In this case you need to extend ProcessStreamConnectionProvider
Here is a basic sample which starts the path/to/my/language/server/main.js
language server written in JavaScript, with
the Node.js runtime found in "path/to/nodejs/node.exe":
package my.language.server;
import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider;
import java.util.Arrays;
import java.util.List;
public class MyLanguageServer extends ProcessStreamConnectionProvider {
public MyLanguageServer() {
List<String> commands = Arrays.asList("path/to/nodejs/node.exe", "path/to/my/language/server/main.js");
super.setCommands(commands);
}
}
If your language server is written in Java, you can use JavaProcessCommandBuilder to build the launch command:
package my.language.server;
import com.intellij.openapi.project.Project;
import com.redhat.devtools.lsp4ij.server.JavaProcessCommandBuilder;
import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider;
import java.util.Arrays;
import java.util.List;
public class MyLanguageServer extends ProcessStreamConnectionProvider {
public MyLanguageServer(Project project) {
List<String> commands = new JavaProcessCommandBuilder(project, "myLanguageServerId")
.setJar("path/to/my/language/server/server.jar")
.create();
super.setCommands(commands);
}
}
This builder takes care of filling command with the current Java runtime, and adds additional debug flags if the settings of the
language server myLanguageServerId
defines a debug port.
You can see a complete example with the QuteServer implementation.
It is not required, but you can override the LanguageClientImpl to, for instance:
- add some IJ listeners when the language client is created.
- override some LSP methods.
package my.language.server;
import com.intellij.openapi.project.Project;
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl;
public class MyLanguageClient extends LanguageClientImpl {
public MyLanguageClient(Project project) {
super(project);
}
}
If your language server manages custom LSP requests, it is recommended to extend IndexAwareLanguageClient, to ensure it won't be adversely affected by indexing operations.
You can see a full example with QuteLanguageClient
The next step is to declare the server in your plugin.xml with the com.redhat.devtools.lsp4ij.server
extension point
referencing your my.language.server.MyLanguageServerFactory
:
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<server id="myLanguageServerId"
name="My Language Server"
factoryClass="my.language.server.MyLanguageServerFactory">
<description><![CDATA[
Some description written in HTML to display it in the LSP consoles view and Language Servers settings.
]]>
</description>
</server>
</extensions>
A that point, once the declaration is done, your server should appear in the LSP Consoles
view:
Once the server is defined in your plugin.xml
, you still need to associate an IntelliJ language with the server
defined by the id attribute.
You can use three kinds of mappings:
- Language mapping with the
com.redhat.devtools.lsp4ij.languageMapping
extension point used to associate a Language with a language server. - File type mapping with the
com.redhat.devtools.lsp4ij.fileTypeMapping
extension point used to associate a File type with a language server. - File name pattern mapping with the
com.redhat.devtools.lsp4ij.fileNamePatternMapping
extension point used to associate a simple pattern file name (ex:*.less
) with a language server. This mapping can be very helpful if you need to support syntax coloration with TextMate. Indeed, when you manually define a File type for some file name patterns, you loose the TextMate syntax coloration.
Here is a sample snippet to associate the XML
language with the myLanguageServerId
server:
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<languageMapping language="XML"
serverId="myLanguageServerId"/>
</extensions>
Some language servers use
the TextDocumentItem#languageId
field to identify the document on the server side.
For instance the vscode-css-languageservice (used by the
vscode CSS language server) expects the languageId
to be css
or less
.
To do that, you can declare it with the languageId
attribute:
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<languageMapping language="CSS"
serverId="myLanguageServerId"
languageId="css"/>
</extensions>
If the language check is not enough, you can implement a
custom DocumentMatcher.
For instance your language server could be mapped to the Java
language, and you could implement a DocumentMatcher
to check if the module containing the file contains certain Java classes in its classpath.
The DocumentMatcher is executed in a non blocking read action.
A document matcher looks like this:
package my.language.server;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.redhat.devtools.lsp4ij.AbstractDocumentMatcher;
import org.jetbrains.annotations.NotNull;
public class MyDocumentMatcher extends AbstractDocumentMatcher {
@Override
public boolean match(@NotNull VirtualFile virtualFile, @NotNull Project project) {
return true;
}
}
and it must be registered as language mapping, with the documentMatcher
attribute:
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<languageMapping language="XML"
serverId="myLanguageServerId"
documentMatcher="my.language.server.MyDocumentMatcher"/>
</extensions>
If your plugin does not define an
IntelliJ Language
but just a File type, you can
use fileTypeMapping
instead of using languageMapping
.
A good example is if you want to associate the existing CSS
file type to the
CSS language server
in IntelliJ Community
, which only defines the CSS file type, but not the CSS language.
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<fileTypeMapping fileType="CSS"
serverId="myLanguageServerId"
languageId="css"/>
</extensions>
If your plugin does not define
a File type,
you can use fileNamePatternMapping
instead of using fileTypeMapping
.
A good example is if you want to associate *.less
files to the CSS language server
where LESS file type
doesn't exist in IntelliJ Community
.
Using fileNamePatternMapping
is recommended if you want to keep the TextMate
-based syntax coloration.
(using File type will override and
disable the syntax coloration with TextMate).
A good example is if you want to associate
the TypeScript Language Server
without breaking existing syntax coloration managed with TextMate
in IntelliJ Community
.
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<fileNamePatternMapping patterns="*.less;*.scss"
serverId="myLanguageServerId"
languageId="css"/>
</extensions>
In general, LSP4IJ maps language server features to specific Intellij APIs. However, in some cases, the mapping needs to be declared explicitly in your plugin.xml.
In case the language server provides hover support for a language already supported by Intellij or another plugin, you'll need to add a special mapping between that language and LSP4IJ, in your plugin.xml:
<lang.documentationProvider
language="MyLanguage"
implementationClass="com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationProvider"
order="first"/>
See specific hover implementation details for more details.
If the language server support requires to implement a custom client command, you can extend
LSPCommandAction.java and register it
in plugin.xml
with a standard
action element.
LSP4IJ provides default LSP commands that you language servers leverage.
TriggerSuggestAction.java emulates Visual Studio Code's editor.action.triggerSuggest
command,
to trigger code completion after selecting a completion item.
This command is used for instance with the CSS Language Server
to reopen completion after applying color completion item
:
ShowReferencesAction.java emulates Visual Studio Code's editor.action.showReferences
command,
to show the LSP references in a popup.
This command is used for instance with the TypeScript Language Server
to open references/implementations
in a popup when clicking on a Codelens
: