mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 14:15:14 +00:00
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:
12 files changed
+309
-13
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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[]
|
||||
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}"
|
||||
Reference in new issue
Block a user