From 1b459899cae0ecdb5edded35f1f134669b933d33 Mon Sep 17 00:00:00 2001 From: Pavel Hajek Date: Sun, 9 Aug 2026 13:31:30 +0200 Subject: [PATCH] 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 Co-Authored-By: Claude Fable 5 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 + README.md | 2 +- docs/01-about/020_programming-languages.md | 4 +- pyproject.toml | 1 + src/serena/resources/project.template.yml | 22 +-- .../wolfram_language_server.py | 183 ++++++++++++++++++ src/solidlsp/ls_config.py | 11 ++ test/conftest.py | 9 + .../repos/wolfram/test_repo/lib/helper.wl | 3 + .../resources/repos/wolfram/test_repo/main.wl | 17 ++ test/solidlsp/wolfram/__init__.py | 0 test/solidlsp/wolfram/test_wolfram_basic.py | 66 +++++++ 12 files changed, 309 insertions(+), 13 deletions(-) create mode 100644 src/solidlsp/language_servers/wolfram_language_server.py create mode 100644 test/resources/repos/wolfram/test_repo/lib/helper.wl create mode 100644 test/resources/repos/wolfram/test_repo/main.wl create mode 100644 test/solidlsp/wolfram/__init__.py create mode 100644 test/solidlsp/wolfram/test_wolfram_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3525c2de..022448dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index bd58d919..6e16380f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 62d581bb..331c1581 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 9944d840..96ed8e12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 5b1d0293..5008aa08 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -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) diff --git a/src/solidlsp/language_servers/wolfram_language_server.py b/src/solidlsp/language_servers/wolfram_language_server.py new file mode 100644 index 00000000..a3c3790d --- /dev/null +++ b/src/solidlsp/language_servers/wolfram_language_server.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 diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index d4f05de0..67d77b02 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -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 diff --git a/test/conftest.py b/test/conftest.py index a44be041..c06076b6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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 diff --git a/test/resources/repos/wolfram/test_repo/lib/helper.wl b/test/resources/repos/wolfram/test_repo/lib/helper.wl new file mode 100644 index 00000000..8502c91e --- /dev/null +++ b/test/resources/repos/wolfram/test_repo/lib/helper.wl @@ -0,0 +1,3 @@ +sayHello[name_String] := StringJoin["Hello, ", name, "!"] + +formatResult[value_] := StringJoin["Result: ", ToString[value]] diff --git a/test/resources/repos/wolfram/test_repo/main.wl b/test/resources/repos/wolfram/test_repo/main.wl new file mode 100644 index 00000000..8b3f2826 --- /dev/null +++ b/test/resources/repos/wolfram/test_repo/main.wl @@ -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[] diff --git a/test/solidlsp/wolfram/__init__.py b/test/solidlsp/wolfram/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/wolfram/test_wolfram_basic.py b/test/solidlsp/wolfram/test_wolfram_basic.py new file mode 100644 index 00000000..44b20385 --- /dev/null +++ b/test/solidlsp/wolfram/test_wolfram_basic.py @@ -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}"