Add Wolfram Language support via official LSPServer paclet (#1108)

Adds support for Wolfram Language (.wl, .wls files) using the official
WolframResearch LSPServer paclet, which communicates via stdio.
Requires Wolfram Mathematica 13.0+ or Wolfram Engine 12.1+.
The WolframKernel is located via WOLFRAM_PATH, the system PATH, common
install locations, or ls_path in ls_specific_settings.

Includes language server implementation, test repo, test suite
(skipped gracefully when WolframKernel is unavailable), and
documentation updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-08-09 13:31:30 +02:00
1 parent c687e751af
commit 1b459899ca
12 files changed
+309 -13

No files matched your search

+4
View File
@@ -56,6 +56,10 @@ Status of the `main` branch. Changes prior to the next official version change w
`intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821
- Kotlin: update the managed Kotlin LSP from `261.13587.0` to `262.9593.0`, including support for the
new platform-specific archive layout and Windows ARM64 builds
- Add support for Wolfram Language via the official [WolframResearch LSPServer](https://github.com/WolframResearch/LSPServer) paclet.
Requires Wolfram Mathematica 13.0+ or Wolfram Engine 12.1+. Set `WOLFRAM_PATH` environment variable or configure
`ls_path` in `ls_specific_settings`. Supports .wl and .wls files with diagnostics, document symbols,
within-file references, hover documentation, and formatting.
- Nix/nixd: support custom `ls_path` launchers and external JSON settings through `config_path` #1737
- Fix: Nix/nixd diagnostics now use published diagnostics instead of the unsupported
`textDocument/diagnostic` request, which terminated nixd #1802
+1 -1
View File
@@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language
The underlying language servers are typically open-source projects or at least freely available for use.
When using Serena's language server backend, we provide **support for over 40 programming languages**, including
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, YAML, and Zig.
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, Wolfram Language, YAML, and Zig.
### The Serena JetBrains Plugin
+3 -1
View File
@@ -177,8 +177,10 @@ Some languages require additional installations or setup steps, as noted.
globals, which the plain TypeScript language server does not; overlaps `typescript` on file extensions,
so it is not auto-detected and must be set as the language explicitly — do not also enable `typescript`
for the same files)
* **Vue**
* **Vue**
(3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection)
* **Wolfram Language**
(requires Wolfram Mathematica 13.0+ or Wolfram Engine 12.1+; uses the official [WolframResearch LSPServer](https://github.com/WolframResearch/LSPServer) paclet; supports .wl and .wls files; references are within-file only)
* **YAML**
* **JSON**
(experimental; must be explicitly added to the languages list; requires Node.js and npm)
+1
View File
@@ -381,6 +381,7 @@ markers = [
"ada: language server running for Ada / SPARK (uses AdaCore Ada Language Server)",
"qml: language server running for QML (uses Qt's qmlls)",
"gleam: language server running for Gleam (uses the Gleam compiler's bundled `gleam lsp`)",
"wolfram: language server running for Wolfram Language (requires Mathematica 13.0+ or Wolfram Engine 12.1+)",
]
[tool.codespell]
+11 -11
View File
@@ -6,17 +6,17 @@ project_name: "project_name"
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart deno
# elixir elm erlang fortran fsharp
# gdscript go groovy haskell haxe
# hlsl html java json julia
# kotlin latex lean4 lua luau
# markdown matlab msl nextflow nix
# ocaml pascal perl php php_phpactor
# php_phpantom powershell python python_basedpyright python_jedi
# python_pyrefly python_ty qml r rego
# ruby ruby_solargraph rust scala scss
# solidity svelte swift systemverilog terraform
# toml typescript typescript_vts vue yaml
# zig
# gdscript gleam go groovy haskell
# haxe hlsl html java json
# julia kotlin latex lean4 lua
# luau markdown matlab msl nextflow
# nix ocaml pascal perl php
# php_phpactor php_phpantom powershell python python_basedpyright
# python_jedi python_pyrefly python_ty qml r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# wolfram yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
@@ -0,0 +1,183 @@
"""Wolfram Language server integration using the official WolframResearch LSPServer paclet."""
import glob
import logging
import os
import platform
import shutil
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
WOLFRAM_PATH_ENV_VAR = "WOLFRAM_PATH"
class WolframLanguageServer(SolidLanguageServer):
"""
Wolfram Language server using the official WolframResearch LSPServer paclet
(https://github.com/WolframResearch/LSPServer), which is bundled with Wolfram
installations and communicates via stdio.
Requires Wolfram Mathematica 13.0+ or Wolfram Engine 12.1+.
The WolframKernel executable is discovered via the ``ls_path`` entry in
``ls_specific_settings.wolfram``, the ``WOLFRAM_PATH`` environment variable
(pointing to the executable or the installation directory), the system PATH,
or common installation locations.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "wolfram", solidlsp_settings)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Discover the WolframKernel executable.
:return: path to the WolframKernel executable
:raises FileNotFoundError: if WolframKernel cannot be found
"""
return _find_wolfram_kernel()
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "-noprompt", "-noinit", "-run", 'Needs["LSPServer`"];LSPServer`StartServer[]']
def _create_base_initialize_params(self) -> dict:
return {
"capabilities": {
"workspace": {"workspaceFolders": True},
"textDocument": {
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"formatting": {"dynamicRegistration": True},
"publishDiagnostics": {"relatedInformation": True},
},
},
}
def _start_server(self) -> None:
def do_nothing(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"Wolfram LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting Wolfram LSPServer process")
self.server.start()
initialize_params = self._create_initialize_params()
init_response = self.server.send.initialize(initialize_params)
log.info(f"Wolfram LSP capabilities: {list(init_response.get('capabilities', {}).keys())}")
self.server.notify.initialized({})
log.info("Wolfram LSPServer initialized and ready.")
def _find_wolfram_kernel() -> str:
"""Find the WolframKernel executable via WOLFRAM_PATH, the system PATH, or common install locations."""
# 1. WOLFRAM_PATH environment variable (executable or installation directory)
env_path = os.environ.get(WOLFRAM_PATH_ENV_VAR)
if env_path:
if os.path.isfile(env_path) and os.access(env_path, os.X_OK):
log.info(f"Using WolframKernel from {WOLFRAM_PATH_ENV_VAR}: {env_path}")
return env_path
kernel_in_dir = _find_kernel_in_install_dir(env_path)
if kernel_in_dir:
log.info(f"Using WolframKernel from {WOLFRAM_PATH_ENV_VAR} directory: {kernel_in_dir}")
return kernel_in_dir
# 2. System PATH
kernel_path = shutil.which("WolframKernel")
if kernel_path:
log.info(f"Using WolframKernel from PATH: {kernel_path}")
return kernel_path
# 3. Common installation locations
system = platform.system()
search_locations: list[str] = []
if system == "Darwin":
search_locations = [
"/Applications/Mathematica.app/Contents/MacOS/WolframKernel",
"/Applications/Wolfram.app/Contents/MacOS/WolframKernel",
"/Applications/Wolfram Engine.app/Contents/MacOS/WolframKernel",
]
for pattern in [
"/Applications/Mathematica*.app/Contents/MacOS/WolframKernel",
"/Applications/Wolfram*.app/Contents/MacOS/WolframKernel",
]:
search_locations.extend(sorted(glob.glob(pattern), reverse=True))
elif system == "Linux":
search_locations = [
"/usr/local/bin/WolframKernel",
"/usr/bin/WolframKernel",
]
for pattern in [
"/usr/local/Wolfram/Mathematica/*/Executables/WolframKernel",
"/usr/local/Wolfram/WolframEngine/*/Executables/WolframKernel",
"/opt/Wolfram/Mathematica/*/Executables/WolframKernel",
]:
search_locations.extend(sorted(glob.glob(pattern), reverse=True))
elif system == "Windows":
for pattern in [
"C:\\Program Files\\Wolfram Research\\Mathematica\\*\\WolframKernel.exe",
"C:\\Program Files\\Wolfram Research\\Wolfram Engine\\*\\WolframKernel.exe",
]:
search_locations.extend(sorted(glob.glob(pattern), reverse=True))
for location in search_locations:
if os.path.isfile(location) and os.access(location, os.X_OK):
log.info(f"Found WolframKernel at: {location}")
return location
raise FileNotFoundError(
"WolframKernel not found. Please either:\n"
f"1. Set the {WOLFRAM_PATH_ENV_VAR} environment variable to your Wolfram installation\n"
"2. Add WolframKernel to your system PATH\n"
"3. Configure ls_path in ls_specific_settings.wolfram\n"
"4. Install Wolfram Mathematica (13.0+) or Wolfram Engine (12.1+) from https://www.wolfram.com/"
)
def _find_kernel_in_install_dir(install_dir: str) -> str | None:
"""Try to locate WolframKernel within a Wolfram installation directory."""
system = platform.system()
if system == "Darwin":
candidates = [
os.path.join(install_dir, "Contents", "MacOS", "WolframKernel"),
os.path.join(install_dir, "MacOS", "WolframKernel"),
]
elif system == "Windows":
candidates = [
os.path.join(install_dir, "WolframKernel.exe"),
]
else:
candidates = [
os.path.join(install_dir, "Executables", "WolframKernel"),
os.path.join(install_dir, "WolframKernel"),
]
for candidate in candidates:
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
+11
View File
@@ -196,6 +196,11 @@ class LanguageServerId(str, Enum):
it reports no symbols for them, so they are not treated as source files here).
Automatically downloads the language server JAR; requires Java 17+ (JAVA_HOME or 'java' on PATH).
"""
WOLFRAM = "wolfram"
"""Wolfram Language server using the official WolframResearch LSPServer paclet.
Requires Wolfram Mathematica 13.0+ or Wolfram Engine 12.1+.
Set WOLFRAM_PATH environment variable or configure ls_path in ls_specific_settings.
"""
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -598,6 +603,8 @@ class LanguageServerId(str, Enum):
# only scripts: the language server does have a service for .config files, but it provides
# no symbols for them, so treating them as source files would only pollute the symbol index
return FilenameMatcher(".nf")
case self.WOLFRAM:
return FilenameMatcher(".wl", ".wls")
case self.HTML:
return FilenameMatcher(".html", ".htm")
case self.SCSS:
@@ -901,6 +908,10 @@ class LanguageServerId(str, Enum):
from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer
return NextflowLanguageServer
case self.WOLFRAM:
from solidlsp.language_servers.wolfram_language_server import WolframLanguageServer
return WolframLanguageServer
case self.HTML:
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
+9
View File
@@ -492,6 +492,15 @@ def _determine_disabled_language_servers() -> list[LanguageServerId]:
if is_ci:
result.append(LanguageServerId.KOTLIN) # IntelliJ-based Kotlin LSP crashes on JVM restart under CI memory limits
# Disable Wolfram tests if WolframKernel is not available (checked with the same
# discovery logic used by the language server itself)
from solidlsp.language_servers.wolfram_language_server import _find_wolfram_kernel
try:
_find_wolfram_kernel()
except FileNotFoundError:
result.append(LanguageServerId.WOLFRAM)
return result
@@ -0,0 +1,3 @@
sayHello[name_String] := StringJoin["Hello, ", name, "!"]
formatResult[value_] := StringJoin["Result: ", ToString[value]]
@@ -0,0 +1,17 @@
Get["lib/helper.wl"]
calculateSum[a_, b_] := a + b
processData[data_List] := Module[{result},
result = Total[data];
formatResult[result]
]
main[] := Module[{result, greeting},
result = calculateSum[5, 3];
greeting = sayHello["World"];
Print[greeting];
Print[result]
]
main[]
View File
Whitespace-only changes.
@@ -0,0 +1,66 @@
import pytest
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
pytestmark = [
pytest.mark.wolfram,
pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.WOLFRAM), reason="Wolfram tests disabled (WolframKernel not available)"
),
]
# Note: the LSPServer paclet computes textDocument/references per document, i.e. only
# references within the file containing the queried position are returned; cross-file
# references are not supported by the language server.
class TestWolframLanguageServer:
@pytest.mark.parametrize("language_server", [LanguageServerId.WOLFRAM], indirect=True)
def test_wolfram_symbols(self, language_server: SolidLanguageServer):
"""
Test if we can find the top-level symbols in the main.wl file.
"""
all_symbols, _ = language_server.request_document_symbols("main.wl").get_all_symbols_and_roots()
symbol_names = {s["name"] for s in all_symbols}
assert "calculateSum" in symbol_names
assert "main" in symbol_names
@pytest.mark.parametrize("language_server", [LanguageServerId.WOLFRAM], indirect=True)
def test_wolfram_symbols_in_subdirectory(self, language_server: SolidLanguageServer):
"""
Test if we can find the top-level symbols in a file in a subdirectory.
"""
all_symbols, _ = language_server.request_document_symbols("lib/helper.wl").get_all_symbols_and_roots()
symbol_names = {s["name"] for s in all_symbols}
assert "sayHello" in symbol_names
assert "formatResult" in symbol_names
@pytest.mark.parametrize("language_server", [LanguageServerId.WOLFRAM], indirect=True)
def test_wolfram_within_file_references(self, language_server: SolidLanguageServer):
"""
Test finding references to a function within the same file.
"""
# 'calculateSum' is defined on line 2 of main.wl and called on line 10 (0-based)
references = language_server.request_references("main.wl", line=2, column=0)
reference_lines = {(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references}
assert ("main.wl", 2) in reference_lines, f"Expected the definition site among references, got {reference_lines}"
assert ("main.wl", 10) in reference_lines, f"Expected the call site among references, got {reference_lines}"
@pytest.mark.parametrize("language_server", [LanguageServerId.WOLFRAM], indirect=True)
def test_wolfram_no_cross_file_references(self, language_server: SolidLanguageServer):
"""
Test documenting that references are computed per document by the LSPServer paclet.
'sayHello' is defined in lib/helper.wl and called in main.wl, but querying references
from its definition must only return locations within lib/helper.wl. If this test
starts failing, the language server gained cross-file reference support and the
documentation (docs page, CHANGELOG, module comment above) should be updated.
"""
references = language_server.request_references("lib/helper.wl", line=0, column=0)
assert references, "Expected at least one within-file reference in lib/helper.wl fixture"
reference_paths = {ref["relativePath"] for ref in references}
assert reference_paths == {"lib/helper.wl"}, f"Expected references only within lib/helper.wl, got {reference_paths}"