LSP4IJ provides the capability to consume any language server without developing
an IntelliJ plugin via a User-defined language server.
The main idea is to:
- install the language server and its requirements(ex :
Node.jsto execute a language server written in JavaScript/TypeScript), - declare the command that starts the language server.
- associate the language server with the proper files (identified by IntelliJ Language, File Type or file name pattern)
In order to create a new User-defined language server, you need to open the New Language Server dialog, either:
- from the menu on the right of the LSP console:
- or with the
[+]on the top of the language server settings:
Once you clicked on either of them, the dialog will appear:
When you click the OK button, the language server will be created.
If the language server definition declares an installer
in the Installer tab, the installer will run once when the language server starts.
You can reinstall the server at any time (e.g. to fetch the latest version) using the Reinstall action:
The Server tab requires the server name and command fields to be set. A language server which runs already, and
to which LSP4IJ attaches over a socket, needs no command, but its port (see Transport).
Below the Working Directory field, a line shows the directory with its macros resolved, and says when no such directory
exists yet: the language server can only start in one that exists, which its installer can still create.
Here is a sample with the typescript-language-server:
The environment variables accessible by the process are populated with EnvironmentUtil.getEnvironmentMap() which retrieves system variables.
It is also possible to add custom environment variables via the Environment variables field:
LSP4IJ starts the command directly, without a shell, with these environment variables, and looks the command up in
their PATH (see below for a working directory). sh -c doesn't add any variables: a non-interactive shell reads no
profile, so it passes on the same environment. Use a shell only for shell syntax, such as &&, a pipe or ~, and give
it the whole command in double quotes, since sh -c runs only the argument right after -c:
- with
Linux,Mac OS:sh -c "cd server && ./start --stdio" - with
Windows OS:cmd /c "cd server && start.cmd --stdio"
Since IntelliJ 2025.1, a plugin can set up the environment of a process for its working directory (the
commandLineEnvCustomizer extension point), as the mise plugin does from a mise.toml, or a
direnv plugin from an .envrc. The language server's process gets that environment when it starts.
When a working directory is set, LSP4IJ also looks up the language server's command in the PATH of that environment, so
that the tool which the directory selects, such as its version of node, is the one that starts.
Note
- Set the working directory: the environment is the one of that directory. Without one, the directory's environment doesn't apply: the command is looked up in the
PATHof the environment variables configured here, or else in the IDE'sPATH. - Precedence: the directory's environment is applied when the process starts, after the environment variables configured here, so it wins over a variable that both set.
- Windows: with a working directory, a command without its extension, such as
nodeornpx, is looked up with the extensions inPATHEXT, as Windows does, so that the directory'snode.exeornpx.cmdstarts. - Tools not bundled: LSP4IJ doesn't bundle mise or direnv. Install the tool and its IntelliJ plugin. The mise plugin customizes the processes of other plugins only with its
useMiseInAllCommandLinessetting, which is off by default. - IntelliJ 2024.x has no such extension point, so the environment is as before. The command is looked up as described above, also with the extensions in
PATHEXTon Windows.
You can use built-in macros in your command. You could, for instance, store the language server in your project (to share it with your team) and write a command that references it in a portable way to start it.
That command might look like this:
$PROJECT_DIR$/path/to/your/start/command
Here are some useful standard built-in macros that you can use:
$WORKSPACE_DIR$: The path to the workspace where the current project belongs. The workspace is the root of the open file hierarchy and can include multiple projects.$PROJECT_DIR$: The root of the project where run.json is located. A project is typically a collection of files for developing and building an application such as a Maven or Node.js project.$USER_HOME$: User home directory.
LSP4IJ adds placeholders of its own:
${instanceRoot}, in the command and the working directory: the root of the language server. With several instances, it is the root of each instance, and otherwise the project's directory, as$PROJECT_DIR$. In the command, a root with spaces needs quotes, as in"${instanceRoot}/server.js".${port}, in the command of a language server that LSP4IJ starts and talks to over a socket: the port of that socket, see Transport.
Here is an example with Scala Language Server's metals.bat stored at the root of the project:
When commands contain macros, their resolved value is visible below the Command field.
By default, LSP4IJ talks to the language server over the standard input and output (stdio) of its process. A language
server which talks LSP over a TCP socket instead is configured with Transport: Socket, in one of three ways:
Attach to a running language server: the language server runs already and listens onHostandPort, as when it runs in a debugger or inside another application. LSP4IJ doesn't start it, so the environment variables, theCommandand the working directory are hidden, and stopping the language server only closes the connection. Without a host, LSP4IJ connects to the loopback address,127.0.0.1, and then to the IPv6 one,::1, which a language server that listens onlocalhostmay get, as Node.js may do. A host is a plain host name or IP address, without a scheme, and LSP4IJ tries each of its addresses in turn; the port has its own field. An attach that gets no answer within 10 seconds fails. The port is needed, since LSP4IJ can't choose it.Start the language server, LSP4IJ connects to it: LSP4IJ starts the language server with theCommand, and connects to the port once the language server accepts connections.Start the language server, it connects to LSP4IJ: LSP4IJ listens on the port and starts the language server with theCommand, which connects to it, as VS Code's socket transport does for a language server started with--socket=<port>.
When LSP4IJ starts the language server, the Command takes the port as ${port}, and a Port of Auto lets LSP4IJ
choose a free port each time: the Command must then have ${port}, or the language server can't know the port. A
start that gets no connection within 60 seconds fails. For instance, with a language server built on vscode-languageserver:
node path/to/server.js --socket=${port}
An installer which sets the Command writes the command of its template, which is for the standard
input and output unless it takes the port as ${port}. A language server which LSP4IJ starts over a socket is then
switched back to the standard input and output; set the socket again with a command that takes the port.
A template sets the transport in its
socket, and lists the transports which its language server supports
in transports.
LSP4IJ listens on the loopback addresses only, 127.0.0.1 and ::1. The output of a language server which LSP4IJ starts is shown in the Log
tab of the LSP console, since the socket carries the protocol: its standard output as
information, and its standard error as errors. As with any TCP transport
of LSP, which has no handshake to authenticate its peer, any local process can connect to a loopback port: on a machine
shared with other users, prefer the standard input and output.
A language server which LSP4IJ starts may listen on, or connect to, either loopback address, 127.0.0.1 or ::1, as
one which uses localhost may get either: LSP4IJ connects to both, and listens on both.
The Mappings tab provides the capability to associate the language server with the proper files identified by:
- IntelliJ Language
- IntelliJ File type
File name pattern
Here are mappings samples with the typescript-language-server:
- The existing
JavaScriptfile type is used to associate the file to the language server:
- Since IntelliJ (Community) doesn't provide file type by default
TypeScript,Reactfile name patterns are used:
NOTE: it is better to use file name pattern instead of creating custom file type for TypeScript, since by default
IntelliJ Community support TypeScript syntax coloration with TextMate. If you define a file type, you will
lose syntax coloration.
When you declare mapping, you can fill the Language ID column which is used to declare the LSP TextDocumentItem#languageId
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:
The Configuration tab allows to configure the language server with the expected (generally JSON format) configuration.
Here are configuration sample with the typescript-language-server:
A property set to null in Configuration or Initialization Options reaches the language server as null, so that
it can reset that setting to its default.
LSP4IJ must be able to read the JSON fields: Configuration, Initialization Options and Experimental in the
Server tab of the Configuration tab, the configuration in its Client tab, and the JSON of the Installer and
Workspace Folders tabs. It reads them leniently, so comments (//, /* */ and #), unquoted names and
single-quoted strings are accepted, but a missing brace or a trailing comma is not. Configuration,
Initialization Options and Experimental must stay readable once their macros, such as $PROJECT_DIR$, are
expanded, which a Windows path in a string may not. A language server with a field that can't be read is not saved:
the New Language Server dialog lists the errors and disables OK, and the settings page and the Apply action of
the LSP console show the error.
The Debug tab is available when you have created the language server definition. It allows to customize the
level Trace used in LSP console.
The Installer tab provides the capability to declare with JSON, the language server installation
with task to execute like:
execto execute a command (ex:npm install typescript-language-serverto install the typescript-language-server).downloadto download the language server from a given url according the OS.
This installer content will be used when you will create the language server. See here to understand how to write this JSON installer descriptor.
Workspace folders define the scope of files that the language server will analyze. They can be sent to the language server:
- At startup via the initialize request
- Dynamically via workspace/didChangeWorkspaceFolders notifications
This tab allows you to configure and preview how workspace folders will be discovered and sent to your language server.
The rootType defines which IntelliJ project structure to use as workspace folders:
PROJECT_BASE(default): Uses content rootsSOURCE_ROOTS: Uses source rootsNONE: No workspace folders
{
"rootType": "SOURCE_ROOTS"
}This configuration will populate InitializeParams.workspaceFolders when the language server starts:
{
"workspaceFolders": [
{
"name": "my-module",
"uri": "file:///path/to/my-module"
}
]
}If rootType strategies don't match your project structure, use markers to define workspace folders based on specific files. This is useful for:
- Mono-repos with multiple independent modules
- Python projects with
pyproject.tomlorsetup.py - Projects where workspace boundaries are defined by configuration files
{
"markers": [
"pyproject.toml",
"pom.xml",
".git"
]
}When a file is opened, LSP4IJ walks up the directory tree to find the closest marker file and uses its directory as the workspace folder.
Note: Marker-based discovery always uses lazy mode.
By default (eager mode), all workspace folders are sent at startup via the initialize request. For large projects, this can cause the language server to load and analyze many files unnecessarily.
Lazy mode defers workspace folder discovery: folders are sent progressively via workspace/didChangeWorkspaceFolders notifications as you open files.
{
"rootType": "PROJECT_BASE",
"lazy": true
}Example: Consider a project with 2 content roots: client and common.
Eager mode (lazy: false):
- All folders sent at startup in the
initializerequest:{ "workspaceFolders": [ { "uri": "file:///.../client", "name": "client" }, { "uri": "file:///.../common", "name": "common" } ] } - Language server loads all files from both
clientandcommon, even if you only open a file incommon
You can see this behavior with the following configuration:
Lazy mode (lazy: true):
- Empty workspace folders at startup:
{ "workspaceFolders": [] } - When you open a file from
common, only that folder is sent viaworkspace/didChangeWorkspaceFolders:{ "event": { "added": [{ "uri": "file:///.../common", "name": "common" }], "removed": [] } }
By default, one language server instance serves all workspace folders of a project. If parts of a project need a server process of their own, for example because each one has its own environment, serverInstances says which instance serves a file. It takes rootType or markers, as described above:
{
"serverInstances": {
"markers": ["pyproject.toml"]
}
}Files with the same root share an instance:
- An instance starts when the first of its files is opened. The Language Servers view shows it as a process of its own, with the path of its root in the project. Its node stays when the instance stops or is replaced, with its traces and logs, for the next instance of that root.
- It's initialized with its root as
rootUri. - A file without a root gets no instance, so this language server provides no features for it. LSP4IJ says so once, when such a file is open in an editor.
- With markers, the search stops at the project: a file without a marker up to its topmost content root has that content root as its root, so no instance runs above the project. In a project with content roots, a file outside all of them has no root then.
- All instances use the same command. If no working directory is configured, an instance runs in its root. A
configured working directory applies to every instance, so all of them get the environment of that one directory.
With
${instanceRoot}in it, as in${instanceRoot}/server, each instance runs in a directory of its own root, with the environment of that directory. - Stop on an instance in the Language Servers view, or too many failed starts of it, switch off that instance only, and the others keep working. Restart switches it on again, and so does a changed configuration.
- Each instance needs a connection of its own. Over a socket, use an automatic port, and let LSP4IJ start the language server: the settings don't save
serverInstanceswith a language server that runs already, or with a fixed port.
Without rootType and markers next to serverInstances, the root is the only workspace folder of an instance. With them, they describe the workspace folders inside an instance:
{
"serverInstances": {
"markers": ["pyproject.toml"]
},
"rootType": "SOURCE_ROOTS"
}Here, each folder with a pyproject.toml has an instance, and that instance gets the source roots inside it. With "lazy": true, it gets them through workspace/didChangeWorkspaceFolders as their files are opened. A workspace folder reaches the instance that its own directory has as root: a folder of another instance doesn't, nor does a folder above the root, such as one that contains several instances. An instance that would start without a folder gets its root, unless rootType next to serverInstances is NONE. In lazy mode, an instance starts without folders and gets those of its files as they are opened, and a file outside all of them adds none.
In this example, backend and frontend each contain a pyproject.toml and source roots, so each has its own instance. scripts/tool.py has no marker up to the project's folder, so that folder is its root, with an instance of its own:
Changing this configuration, the command, the working directory, the environment variables or the mappings replaces the instances. Those with an open file start again, the others when one of their files is opened.
The preview panel on the right shows how workspace folders will be sent based on your configuration.
Checkbox unchecked - Shows all discovered workspace folders (discovery mode):
Checkbox checked - Shows only folders sent at initialization (empty in lazy mode):
Drag & drop or click "Open file..." to simulate opening a file and see which workspace folder it belongs to:
With serverInstances, the preview shows each language server instance with its workspace folders below it, so a file you open shows which instance serves it.
If a file is not included in any workspace folder, it will appear under a "(No root)" node:
Template can be used to quickly create user defined language server pre-filled with server name, command, mappings and potential configurations.
The Template combo-box provides some default language servers templates (located in templates directory classpath),
pre-filled with server name, command, mappings and potential configuration.
- Ada Language Server
- Apache Camel Language Server
- Astro Language Server
- CSS Language Server
- Clangd
- Clojure LSP
- Cpp Language Server
- Dart LSP
- Docker Language Server
- EO LSP Server
- Erlang Language Server
- ESLint Language Server
- Go Language Server
- Harper Language Server
- JQ Language Server
- Julia Language Server
- Perl Language Server
- Ruby LSP
- Rust Language Server
- Scala Language Server (Metals)
- SourceKit-LSP
- Stylelint-LSP
- Svelte Language Server
- SysML v2 Language Server
- Terraform Language Server
- TypeScript Language Server
- Vue Language Server
- WGSL Analyzer
If the template directory contains a README.md file, you can open the instructions by pressing the help icon.
The Import from custom template... item from the Template combo-box can be used to select a directory from
the file system to load a custom language server template,
these templates can be pre-filled with server name, command, mappings and potential configuration.
The selected directory contents should match the custom template structure.
If the template directory contains a README.md file, you can open the instructions by pressing the help icon.
Custom templates can be created by exporting templates.
Users can export their own language servers to a zip file, where each language server is a separate directory. This can be done from the LSP console, by selecting one or more language servers and selecting the export option from the context menu.
These directories can then be used as a template for a new language server by importing a custom template.
The template.json of a language server which talks over a socket has its
socket: the mode, the port (0 for Auto), and the host when
one is set. Export doesn't write transports, since a language
server keeps only its transport, not the list of its template.
By default, each directory contains the following files, but only template.json is required.
template.jsonsettings.jsoninitializationOptions.json
A README.md file can be added manually to each of the language server directories to provide instructions
for the corresponding language server.
See here for more information about those JSON structures.






















