Features

This page provides an overview of the language server features that JETLS offers.

Screenshots on this page use VSCode (via the jetls-client extension), as its LSP UI is the de facto reference. The same features work in any supported editor, though the presentation may differ.[error_lens]

Note

JETLS is under active development. This page is not exhaustive and is updated as features mature. For the current status of planned features, see the roadmap.

Overview

Diagnostic

JETLS reports diagnostics from three analysis stages — syntax parsing, lowering, and type inference — covering everything from malformed input to deep type-level issues. Some diagnostics come with quick-fix code actions. See the Diagnostic reference for the full list of diagnostic codes and examples.

Syntax diagnostics

Parse errors detected by JuliaSyntax.jl.

Syntax diagnostic

Syntax diagnostic

Lowering diagnostics

Static analysis issues produced during lowering by JuliaLowering.jl — including undefined / unused bindings, unreachable code, scope ambiguities, and import-related issues.

Lowering diagnostic

Lowering diagnostic

Inference diagnostics

Type-level issues caught by JET.jl during type inference, such as non-existent field access, out-of-bounds indexing, method errors, and non-boolean conditions.

Inference diagnostic

Inference diagnostic

Go to definition

Jump to where a symbol is defined. JETLS resolves method and module definitions, as well as local bindings.

Go to definition

Go to definition

When the cursor is on (or right after) a call site, JETLS narrows the result to the method dispatch picked for the inferred argument types — sin│(42) jumps to sin(::Real) only, not to every method of sin. Bare cursors on the function name (sin│) still return every definition.

JETLS also implements go to declaration (textDocument/declaration), which jumps to declaration sites (e.g., import/using, local x, or empty function foo end) when distinct from the definition, and falls back to go to definition otherwise.

Go to type definition

Jump to the definition of an expression's type rather than the expression itself, e.g. with the cursor on a binding of type Foo, JETLS navigates to the struct Foo definition; for a Vector{Int}-typed value, it lands on Vector's definition.

Go to type definition

Go to type definition

The cursor can be on:

  • An identifier (local binding, parameter, type name, etc.). The inferred type at that position is resolved.
  • A dot-chain expression (Base.Pa│ir). The full chain is treated as the target.
  • A call expression. Placing the cursor right after ) (e.g. sin(1.0)│) resolves to the call's return type. do-block calls work the same way — func() do ... end│ resolves to the call's return type.

For Union types, one location is returned per constituent.

Path strings inside include("...") and include_dependency("...") calls become clickable links that open the referenced file. The path must be a single non-interpolated string and resolve to an existing file relative to the current document's directory.

Document link

Document link

Find references

Find all references to a symbol across files analyzed together (e.g., a package and its included files). Both local and global bindings are supported. When the client requests includeDeclaration=false, method definitions of the target are excluded.

Find references

Find references

Hover

Hover surfaces the inferred type and documentation at the cursor. It works on any identifier, dot expression (Base.Compi│ler.tmeet), call result (func(args)│), or indexing position (xs[i]│), and the type is queried at the cursor's byte range so flow-sensitive narrowing is reflected.

Documentation displayed alongside the header depends on the cursor position:

  • Default: the binding's own docstring is shown together with the docstring of whatever value the expression resolves to via type inference (so e.g. hovering on some.value surfaces sin's docstring when the field resolves to sin).
  • Dot expressions whose left-hand side is a struct instance (x.y│) surface the per-field docstring attached to y in its struct definition when the field is documented.
  • Callee identifiers in a call (sin│(rand(Int)), Base.Math.sin│(x)) promote the header to the full call expression (sin(rand(Int)) :: Float64) and narrow the docstring to the dispatched method (sin(::Real)) when dispatch resolves to a single method.
  • Cursors past a call-like surface's closing punctuation (f(args)│, xs[i]│, [a, b]│, …) show only the expr :: T header without any docstring body.
  • Non-call cursors (f│) still show every overload's doc.

The type header is rendered as expr :: T, with a few specialized shapes:

  • Bindings carry a kind tag — (argument), (local), (static parameter), or (global) — before the name.
  • Closures are displayed as (args::T...) -> rt, with argument names recovered from the body method when available.
  • Function singletons render as typeof(<name>) only when the source text doesn't already mention the name, avoiding noise like sin :: typeof(sin).
TargetExample
Global binding

Hover on a global binding

Hover on a global binding

Local binding

Hover on a local binding

Hover on a local binding

Call

Hover on a call expression

Hover on a call expression

Closure

Hover on a closure

Hover on a closure

Field access

Hover on a struct field access

Hover on a struct field access

Document highlight

Highlight all occurrences of the symbol at the cursor within the current file, distinguishing between writes (definitions, assignments) and reads (uses).

Document highlight

Document highlight

Document and workspace symbol

Symbol scopeDescriptionExample
Document symbol An outline view of the current file, listing modules, functions, methods, structs, constants, etc.

Document symbol

Document symbol

Workspace symbol Fuzzy-search across symbols in the whole workspace.

Workspace symbol

Workspace symbol

Code lens

JETLS surfaces actionable information inline above relevant code via code lenses.

Reference count

When code_lens.references is enabled, a reference count is displayed above each top-level symbol (functions, structs, constants, abstract/primitive types, modules). Clicking the lens dispatches the editor.action.showReferences command (a VSCode convention) carrying the pre-resolved reference locations. Clients that follow this convention open the references panel out of the box; clients that don't need a client-side handler — see the Neovim setup for an example.

Reference count code lens

Reference count code lens

TestRunner code lens

Run and re-run @testset blocks directly from the editor. See TestRunner code lens for details.

TestRunner code lens

TestRunner code lens

Inlay hint

Show inline annotations in the editor without modifying the source. JETLS provides two kinds of inlay hints: structural end-tag labels for long blocks, and inferred type annotations next to expressions.

All inlay hints can be disabled via your editor's global settings (e.g. editor.inlayHints.enabled in VSCode, on by default).

Block-end hints

Label the construct that a long end keyword closes — module Foo, function foo, @testset "foo", and so on — to make navigation in long blocks easier. See [inlay_hint.block_end] for enable/disable and threshold configuration.

Block-end inlay hint

Block-end inlay hint

Type hints

Show inferred types next to expressions — bindings, calls, function return types, branch results, etc. — so types are visible inline without hovering. Long types are clipped with to fit inline; hover the hint to see the unclipped type in the tooltip.

Method bodies: Hints currently come from inference starting at the declared signature — parameter types as written, defaulting to Any if absent. Because this drives Julia's standard inference machinery, features like isa / isnothing narrowing and return-type merging work as expected, and the inline hints let you read those results directly off the source.

Type hints — method body

Type hints — method body

Note that, in the example above, the isnothing(x) branch shows x::Union{…} — the unclipped Union{Int, String, Nothing} is in the hover tooltip.

Top-level chunks: Type hints aren't limited to method bodies, e.g. standalone let blocks, are each inferred independently and get the same per-statement annotations.

Type hints — top-level chunk inference

Type hints — top-level chunk inference

Non-obvious types and Union{}: Types you wouldn't write by hand surface inline — Vector{SubString{String}} from split, SubString{String} from first. When inference proves an expression always errors, the type collapses to Union{}; the tooltip explains the bottom type. Here, parse(Int, head, 16) has no matching method (the right form is parse(Int, head; base = 16)), so the call receives both a ::Union{} hint and JET's method-error diagnostic.

Type hints — non-obvious types and Union{}

Type hints — non-obvious types and Union{}

Can be disabled with [inlay_hint.types] enabled configuration.

Semantic tokens

JETLS implements textDocument/semanticTokens to augment the editor's built-in syntactic highlighter (e.g. tree-sitter or TextMate grammar) with information that requires semantic analysis. Tokens for keywords, operators, literals, comments, and macros are left to the syntactic highlighter, which typically handles them well without semantic information.

Emitted token types:

  • parameter — function arguments
  • typeParameterwhere clause type variables
  • variable — locally scoped names
  • jetls.unspecified — global bindings (function, type, module, variable etc.) whose concrete kind JETLS does not classify. Sending a custom (non-predefined) type leaves the syntactic highlighter's color in place while still allowing modifier styling (e.g. .declaration) to apply.[jetls_unspecified_styling]

Modifiers:

  • declaration — explicit local x declarations
  • definition — assignments, function arguments, where bindings

How these tokens are rendered depends on the editor theme. In the screenshot below (VSCode with the Catppuccin theme), xs and factor are colored as parameter and T as typeParameter, while the locally bound total and x use the variable color. Identifiers carrying the definition modifier are rendered in bold, so the definition sites of xs, factor, T, total, and x stand out from their references.[semantic_tokens_customization]

Semantic tokens

Semantic tokens

Both textDocument/semanticTokens/full and textDocument/semanticTokens/range requests are supported. Delta updates are not implemented.

Note

Because JETLS only emits identifier classifications and leaves keywords / operators / literals / comments / macros to the editor's syntactic highlighter, semantic tokens are only registered when the client advertises augmentsSyntaxTokens = true in its capabilities. Clients that do not declare this capability, or that explicitly set it to false, will not have JETLS's semantic tokens feature activated.

Completion

JETLS provides type-aware code completion with multiple modes.

Global and local completion

Completion for global symbols (functions, types, modules, constants) and local bindings. Global completion items include detailed kind information resolved lazily when a candidate is selected.

Global and local completion

Global and local completion

Property completion

Triggered automatically by typing . after a typed expression (x.│, r.pat│). Candidate names are derived from the dot prefix's inferred type via propertynames(::T), so general struct property names and types that define a custom propertynames overload are both handled uniformly. The inferred type of each property (x.field :: T) is resolved lazily, only when the client requests details for a focused item. The resolved documentation panel additionally includes the per-field docstring attached to that field in its struct definition when one is present.

For union-typed prefixes (x::Union{Foo, Bar}), the offered names are the union of each component's properties, and the resolved type detail merges each component's per-property type. The common Union{T, Nothing} pattern thus still surfaces T's properties.

When the dot prefix is a module (Base.│), the request falls through to module-member completion (see Global and local completion).

Property completion

Property completion

Method signature completion

Triggered inside a function call (after (, ,, or space). Compatible method signatures are suggested based on the inferred type of each argument at the call site (mirroring Signature help's filtering — arbitrary local-scope expressions are included). Selecting a candidate inserts remaining positional arguments as snippet placeholders with type annotations. Inferred return type and documentation are resolved lazily.

Method signature completion

Method signature completion

Keyword argument completion

Triggered inside a function call at the keyword argument position (e.g. func(; |) or func(k|)). Available keyword arguments are suggested with = appended. Already-specified keywords are excluded.

Keyword argument completion

Keyword argument completion

LaTeX and emoji completion

Type \ to trigger LaTeX symbol completion (e.g. \alphaα) or \: to trigger emoji completion (e.g. \:smile:😄), mirroring the Julia REPL.

TriggerExample
LaTeX symbol (\)

LaTeX completion

LaTeX completion

Emoji (\:)

Emoji completion

Emoji completion

Signature help

Method signatures are displayed as you type function arguments. Methods are filtered based on the inferred type of each argument at the call site, including arbitrary local-scope expressions. For example, both sin(1,│) and let x = rand(Int); sin(x,│); end show only methods compatible with an Int first argument.

Signature help

Signature help

Rename

Rename local or global bindings across files analyzed together (e.g., a package and its included files).

Rename

Rename

When renaming a string literal that refers to a file path (e.g. in include("foo.jl")), JETLS also renames the file on disk.

File rename

File rename

Code actions

JETLS provides code actions for quick fixes and refactoring, including:

  • Prefix unused variables with _ (or delete the assignment entirely)
  • Remove unused imports
  • Sort import names
  • Delete unreachable code
  • Insert global / local declarations for ambiguous soft scope variables
  • Run a nearby @testset or @test case via TestRunner code actions
  • Expand macro calls via macro expansion views
  • View inferred types as type annotations

A few representative examples:

Code actionTriggered byExample
Prefix unused variable with _

lowering/unused-argument

Prefix unused argument with underscore

Prefix unused argument with underscore

Insert global / local declaration

lowering/ambiguous-soft-scope

Insert global/local declaration

Insert global/local declaration

Sort import names

lowering/unsorted-import-names

Sort import names

Sort import names

Code views

JETLS can open read-only code views: server-computed Julia documents derived from your source, such as the result of macro expansion. They are served as virtual documents through the LSP 3.18 workspace/textDocumentContent request when the client supports it, and otherwise fall back to a temporary file opened via window/showDocument, so they work across editors regardless of textDocumentContent support.

Macro expansion

JETLS offers two code actions on macro calls, each opening the expanded code in a read-only .jl view:

  • Show macro expansion for @macro expands the macro call under the cursor a single level, to see what one macro produces.
  • Expand all macros in this top-level form recursively expands every macro in the enclosing top-level form (the @macroexpand view).

You can trigger the first with the cursor on the macro call, and the second from anywhere inside a top-level form that contains a macro.

For example, Show macro expansion for @assert on @assert @isdefined(x) "x should be defined" opens a view like:

# Macro call:
# @assert @isdefined(x) "x should be defined"
# └─────────────────────────────────────────┘ ── the macro call being expanded
# # @ demo.jl:1

# Expanded code view:
:(if @isdefined(x)
      nothing
  else
      throw(AssertionError("x should be defined"))
  end)

The nested @isdefined call stays in the result because this action expands only the selected macro call one level.

In contrast, Expand all macros in this top-level form on

function selected_value(primary, fallback)
    if primary !== nothing
        value = primary
    elseif fallback !== nothing
        value = fallback
    else
        error("no value available")
    end
    @assert @isdefined(value) "value should be assigned"
    return value
end

expands every macro the form contains, including the nested @isdefined:

# All macros expanded in the top-level form at demo.jl:1

:(function selected_value(primary, fallback)
      if primary !== nothing
          value = primary
      elseif fallback !== nothing
          value = fallback
      else
          error("no value available")
      end
      if $(Expr(:isdefined, :value))
          nothing
      else
          throw(AssertionError("value should be assigned"))
      end
      return value
  end)

To read like hand-written code, the view trims hygiene noise: it strips LineNumberNodes and shows GlobalRefs that resolve in the expansion context (or to an exported Base/Core name) as bare symbols (so the expansions above show throw/AssertionError, not Base.throw). If expansion throws, the error trace is shown instead.

Type annotations

Show inferred type annotations opens a read-only view of the enclosing top-level form with JETLS's type inlay hints spliced into the source as explicit ::T annotations — every expression (parameter uses, intermediate results, the return value) carries its inferred type. Unlike the inline hints, which are transient editor decorations, the result is an annotated copy of the code as ordinary buffer text: you can select, scroll, search, and copy it like any other document.

For example, consider this function:

function summarize(xs::Vector{Int}, label)
    total = sum(xs)
    first_pos_even = findfirst(xs) do x
        x > 0 && iseven(x)
    end
    sample = if first_pos_even === nothing
        missing
    else
        xs[first_pos_even]
    end
    return (
        label,
        total,
        sample,
    )
end

The resulting annotated copy surfaces several related inference results at once: the do-block argument x is inferred as Int64, the predicate expression as Bool, findfirst returns Union{Nothing, Int64}, the else branch narrows the index to Int64, and the untyped label propagates to the final Tuple{Any, Int64, Union{Missing, Int64}} return type:

# Inferred type annotations at summary.jl:1

function summarize(xs::Vector{Int}, label)::Tuple{Any, Int64, Union{Missing, Int64}}
    total = sum(xs::Vector{Int64})::Int64
    first_pos_even = findfirst(xs::Vector{Int64}) do x::Int64
        ((x::Int64 > 0)::Bool && iseven(x::Int64)::Bool)::Bool
    end::Union{Nothing, Int64}
    sample = if (first_pos_even::Union{Nothing, Int64} === nothing)::Bool
        missing
    else
        (xs::Vector{Int64})[first_pos_even::Int64]::Int64
    end::Union{Missing, Int64}
    return (
        label::Any,
        total::Int64,
        sample::Union{Missing, Int64},
    )::Tuple{Any, Int64, Union{Missing, Int64}}
end

Formatting

JETLS integrates with external formatters (Runic.jl by default, or JuliaFormatter.jl) for both document and range formatting. See Formatter integration for setup instructions.

TestRunner integration

Run individual @testset blocks and @test cases directly from the editor via code lenses and code actions, with results surfaced as inline diagnostics and logs. See TestRunner integration for setup and supported patterns.

TestRunner integration

TestRunner integration

Notebook support

JETLS provides LSP features for Julia code cells in Jupyter notebooks. Cells are analyzed together, so all LSP features including diagnostics, go-to definition, find-references, etc., work across cells. See Notebook support for details.

Notebook support

Notebook support

  • error_lensSome screenshots show diagnostic messages rendered inline next to the affected line. This presentation is provided by the Error Lens VSCode extension, not by JETLS itself — JETLS reports the diagnostic information through standard LSP channels, and the inline rendering is up to the client.
  • jetls_unspecified_stylingDo not assign a foreground color or fontStyle to jetls.unspecified itself (e.g. "jetls.unspecified": "#abcdef") — doing so would override the syntactic highlighter's color, defeating the very reason we use a custom token type. Modifier-targeted rules (*.declaration, jetls.unspecified.declaration etc.) are the intended way to style these tokens.
  • semantic_tokens_customization

    The semantic tokens screenshots use editor.semanticTokenColorCustomizations on top of Catppuccin to color typeParameter distinctly and to render tokens carrying the definition modifier in bold:

    "editor.semanticTokenColorCustomizations": {
      "[Catppuccin Latte]": {
        "rules": {
          "typeParameter": "#fe640b",
          "*.definition": { "bold": true }
        }
      },
      "[Catppuccin Mocha]": {
        "rules": {
          "typeParameter": "#fab387",
          "*.definition": { "bold": true }
        }
      }
    }