The goal of this project is to develop a new language server for Julia, currently called "JETLS.jl". This language server aims to enhance developer productivity by providing advanced static analysis and seamless integration with the Julia runtime. By leveraging tooling technologies like JET.jl, JuliaSyntax.jl and JuliaLowering.jl, JETLS aims to offer enhanced language features such as type-sensitive diagnostics, macro-aware go-to definition and such.
This repository manages JETLS.jl, a Julia package that implements a language server, and jetls-client, a sample VSCode extension that serves as a language client for testing JETLS.jl. For information on how to use JETLS.jl with other frontends, please refer to the Other editors section.
- VSCode v1.93.0 or higher
- npm v11.0.0 or higher
- Julia
v"1.12.0-beta2"
or higher
- Run
julia --project=. -e 'using Pkg; Pkg.instantiate()'
in this folder to install all necessary Julia packages. - Run
npm install
in this folder to install all necessary node modules for the client. - Open this folder in VSCode.
- Press Ctrl+Shift+B to start compiling the client and server in watch mode.
- Switch to the Run and Debug View in the Sidebar (Ctrl+Shift+D).
- Select
Launch Client
from the drop-down menu (if it is not already selected). - Press
▷
to run the launch configuration (F5). - In the Extension Development Host instance of VSCode, open a Julia file.
This section contains meta-documentation related to development. For more detailed coding guidelines, please refer to CLAUDE.md, which has been organized to be easily recognized by AI agents (Claude models in particular).
When working with AI agents for development, consider the following tips:
- AI agents generally produce highly random code without test code to guide them, yet they often struggle with writing quality test code themselves. Thus the recommended approach is to prepare solid test code yourself first, then ask the agent to implement the functionality based on these tests.
- AI agents will run the entire JETLS test suite using
Pkg.test()
if not specified otherwise, but as mentioned above, for best results, it's better to include which test code/files to run in your prompt. - You can have the
./julia
script in the root directory of this repository to specify which Julia binary should be used by agents. If the script doesn't exist, the agent will default to using the system'sjulia
command. For example, you can specify a local Julia build by creating a./julia
script like this:./julia
The#!/usr/bin/env bash exec /path/to/julia/usr/bin/julia "$@"
./julia
script is gitignored, so it won't be checked into the git tree.
In JETLS, since we need to use packages that aren’t yet registered
(e.g., JuliaLowering.jl) or
specific branches of JET.jl and
JuliaSyntax.jl,
the Project.toml includes [sources]
section.
The [sources]
section allows simply running Pkg.instantiate()
to install all
the required versions of these packages on any environment, including the CI
setup especially.
On the other hand, it can sometimes be convenient to Pkg.develop
some of the
packages listed in the [sources]
section and edit their source code while
developing JETLS. In particular, to have Revise immediately pick up changes made
to those packages, we may need to keep them in locally editable directories.
However, we cannot run Pkg.develop
directly on packages listed in the
[sources]
section, e.g.:
julia> Pkg.develop("JET")
ERROR: `path` and `url` are conflicting specifications
...
To work around this, you can temporarily comment out the [sources]
section and
run Pkg.develop("JET")
.
This lets you use any local JET implementation. After running Pkg.develop("JET")
,
you can restore the [sources]
section, and perform any most of Pkg
operations without any issues onward.
The same applies to the other packages listed in [sources]
.
JETLS has a development mode that can be enabled through the JETLS_DEV_MODE
preference.
When this mode is enabled, the language server enables several features to aid
in development:
- Automatic loading of Revise when starting the server, allowing changes to be applied without restarting
try
/catch
block is added for the top-level handler of non-lifecycle-related messages, allowing the server to continue running even if an error occurs in each message handler, showing error messages and stack traces in the output panel
You can configure JETLS_DEV_MODE
using Preferences.jl:
julia> using Preferences
julia> Preferences.set_preferences!("JETLS", "JETLS_DEV_MODE" => true; force=true) # enable the dev mode
Alternatively, you can directly edit the LocalPreferences.toml file.
While JETLS_DEV_MODE
is disabled by default, we strongly recommend enabling
it during JETLS development. For development work, we suggest creating the
following LocalPreferences.toml file in the root directory of this repository:
LocalPreferences.toml
[JETLS] # enable the dev mode of JETLS
JETLS_DEV_MODE = true
[JET] # additionally, allow JET to be loaded on nightly
JET_DEV_MODE = true
Note that in tests, this mode is always disabled to ensure that internal errors
are properly raised rather than being suppressed by the additional try
/catch
block (see test/LocalPreferences.toml).
This language server supports dynamic registration of LSP features.
With dynamic registration, for example, the server can switch the formatting engine when users change their preferred formatter, or disable specific LSP features upon configuration change, without restarting the server process (although neither of these features has been implemented yet).
Dynamic registration is also convenient for language server development. When enabling LSP features, the server needs to send various capabilities and options to the client during initialization. With dynamic registration, we can rewrite these activation options and re-enable LSP features dynamically, i.e. without restarting the server process.
For example, you can dynamically add ,
as a triggerCharacter
for
"completion" as follows. First, launch jetls-client
in VSCode1,
then add the following diff to unregister the already enabled completion feature.
Make a small edit to the file the language server is currently analyzing to send
some request from the client to the server. This will allow Revise to apply this
diff to the server process via the dev mode callback (see runserver.jl),
which should disable the completion feature:
diff --git a/src/completions.jl b/src/completions.jl
index 29d0db5..728da8f 100644
--- a/src/completions.jl
+++ b/src/completions.jl
@@ -21,6 +21,11 @@ completion_options() = CompletionOptions(;
const COMPLETION_REGISTRATION_ID = "jetls-completion"
const COMPLETION_REGISTRATION_METHOD = "textDocument/completion"
+let unreg = Unregistration(COMPLETION_REGISTRATION_ID, COMPLETION_REGISTRATION_METHOD)
+ unregister(currently_running, unreg)
+end
+
function completion_registration()
(; triggerCharacters, resolveProvider, completionItem) = completion_options()
documentSelector = DocumentFilter[
Tip
You can add the diff above anywhere Revise can track and apply changes, i.e.
any top-level scope in the JETLS
module namespace or any subroutine
of _handle_message
that is reachable upon the request handling.
Warning
Note that currently_running::Server
is a global variable that is only
defined in JETLS_DEV_MODE
. The use of this global variable should be limited
to such development purposes and should not be included in normal routines.
After that, delete that diff and add the following diff:
diff --git a/src/completions.jl b/src/completions.jl
index 29d0db5..7609a6a 100644
--- a/src/completions.jl
+++ b/src/completions.jl
@@ -9,6 +9,7 @@ const COMPLETION_TRIGGER_CHARACTERS = [
"@", # macro completion
"\\", # LaTeX completion
":", # emoji completion
+ ",", # new trigger character
NUMERIC_CHARACTERS..., # allow these characters to be recognized by `CompletionContext.triggerCharacter`
]
@@ -36,6 +37,8 @@ function completion_registration()
completionItem))
end
+register(currently_running, completion_registration())
+
# completion utils
# ================
This should re-enable completion, and now completion will also be triggered when
you type ,
.
For these reasons, when adding new LSP features, check whether the feature
supports dynamic/static registration, and if it does, actively opt-in to use it.
That is, register it via the client/registerCapability
request in response to
notifications sent from the client, most likely InitializedNotification
.
The JETLS.register
utility is especially useful for this purpose.
-
Minimal Emacs (eglot client) setup:
(add-to-list 'eglot-server-programs '(((julia-mode :language-id "julia") (julia-ts-mode :language-id "julia")) "julia" "--startup-file=no" "--project=/path/to/JETLS.jl" "/path/to/JETLS.jl/runserver.jl"))
-
Minimal Neovim setup (requires Neovim v0.11):
vim.lsp.config("jetls", { cmd = { "julia", "--startup-file=no", "--project=/path/to/JETLS.jl", "/path/to/JETLS.jl/runserver.jl", }, filetypes = {"julia"}, }) vim.lsp.enable("jetls")
-
Zed extension for Julia/JETLS is available: See aviatesk/zed-julia#avi/JETLS
Footnotes
-
Of course, the hack explained here is only possible with clients that support dynamic registration. VSCode is currently one of the frontends that best supports dynamic registration. ↩