mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 12:15:10 +00:00
Add MATLAB language server support (#893)
Requires Matlab and node to be installed, using vscode Matlab extension as LS
This commit is contained in:
1 parent
823e7c6325
commit
f758a3d19c
9 files changed
+839
-1
No files matched your search
@@ -56,6 +56,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* **Add support for Erlang** experimental, may hang or be slow, uses the recently archived [erlang_ls](https://github.com/erlang-ls/erlang_ls)
|
||||
* **Ruby dual language server support**: Added ruby-lsp as the modern primary Ruby language server. Solargraph remains available as an experimental legacy option. ruby-lsp supports both .rb and .erb files, while Solargraph supports .rb files only.
|
||||
* **Add support for PowerShell** via PowerShell Editor Services (PSES). Requires `pwsh` (PowerShell Core) to be installed and available in PATH. Supports symbol navigation, go-to-definition, and within-file references for .ps1 files.
|
||||
* **Add support for MATLAB** via the official MathWorks MATLAB Language Server. Requires MATLAB R2021b or later and Node.js. Set `MATLAB_PATH` environment variable or configure `matlab_path` in `ls_specific_settings`. Supports .m, .mlx, and .mlapp files with code completion, diagnostics, go-to-definition, find references, document symbols, formatting, and rename.
|
||||
|
||||
|
||||
# 0.1.4
|
||||
|
||||
@@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
|
||||
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
|
||||
|
||||
With Serena's LSP library, we provide **support for over 30 programming languages**, including
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (partial support), Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig.
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (partial support), Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
|
||||
|
||||
@@ -306,6 +306,7 @@ markers = [
|
||||
"powershell: language server running for PowerShell",
|
||||
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
|
||||
"toml: language server running for TOML",
|
||||
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
MATLAB language server integration using the official MathWorks MATLAB Language Server.
|
||||
|
||||
Architecture:
|
||||
This module uses the MathWorks MATLAB VS Code extension (mathworks.language-matlab)
|
||||
which contains a Node.js-based language server. The extension is downloaded from the
|
||||
VS Code Marketplace and extracted locally. The language server spawns a real MATLAB
|
||||
process to provide code intelligence - it is NOT a standalone static analyzer.
|
||||
|
||||
Flow: Serena -> Node.js LSP Server -> MATLAB Process -> Code Analysis
|
||||
|
||||
Why MATLAB installation is required:
|
||||
The language server launches an actual MATLAB session (via MatlabSession.js) to perform
|
||||
code analysis, diagnostics, and other features. Without MATLAB, the LSP cannot function.
|
||||
This is different from purely static analyzers that parse code without execution.
|
||||
|
||||
Requirements:
|
||||
- MATLAB R2021b or later must be installed and licensed
|
||||
- Node.js must be installed (for running the language server)
|
||||
- MATLAB path can be specified via MATLAB_PATH environment variable or auto-detected
|
||||
|
||||
The MATLAB language server provides:
|
||||
- Code diagnostics (publishDiagnostics)
|
||||
- Code completions (completionProvider)
|
||||
- Go to definition (definitionProvider)
|
||||
- Find references (referencesProvider)
|
||||
- Document symbols (documentSymbol)
|
||||
- Document formatting (documentFormattingProvider)
|
||||
- Function signature help (signatureHelpProvider)
|
||||
- Symbol rename (renameProvider)
|
||||
"""
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import threading
|
||||
import zipfile
|
||||
from typing import Any, cast
|
||||
|
||||
import requests
|
||||
|
||||
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Environment variable for MATLAB installation path
|
||||
MATLAB_PATH_ENV_VAR = "MATLAB_PATH"
|
||||
|
||||
# VS Code Marketplace URL for MATLAB extension
|
||||
MATLAB_EXTENSION_URL = (
|
||||
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/vsextensions/language-matlab/latest/vspackage"
|
||||
)
|
||||
|
||||
|
||||
class MatlabLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides MATLAB specific instantiation of the LanguageServer class using the official
|
||||
MathWorks MATLAB Language Server.
|
||||
|
||||
The MATLAB language server requires:
|
||||
- MATLAB R2021b or later installed on the system
|
||||
- Node.js for running the language server
|
||||
|
||||
The language server is automatically downloaded from the VS Code marketplace
|
||||
(MathWorks.language-matlab extension) and extracted.
|
||||
|
||||
You can pass the following entries in ls_specific_settings["matlab"]:
|
||||
- matlab_path: Path to MATLAB installation (overrides MATLAB_PATH env var)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _find_matlab_installation() -> str:
|
||||
"""
|
||||
Find MATLAB installation path.
|
||||
|
||||
Search order:
|
||||
1. MATLAB_PATH environment variable
|
||||
2. Common installation locations based on platform
|
||||
|
||||
Returns:
|
||||
Path to MATLAB installation directory.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If MATLAB installation is not found.
|
||||
|
||||
"""
|
||||
# Check environment variable first
|
||||
matlab_path = os.environ.get(MATLAB_PATH_ENV_VAR)
|
||||
if matlab_path and os.path.isdir(matlab_path):
|
||||
log.info(f"Using MATLAB from environment variable {MATLAB_PATH_ENV_VAR}: {matlab_path}")
|
||||
return matlab_path
|
||||
|
||||
system = platform.system()
|
||||
|
||||
if system == "Darwin": # macOS
|
||||
# Check common macOS locations
|
||||
search_patterns = [
|
||||
"/Applications/MATLAB_*.app",
|
||||
"/Volumes/*/Applications/MATLAB_*.app",
|
||||
os.path.expanduser("~/Applications/MATLAB_*.app"),
|
||||
]
|
||||
for pattern in search_patterns:
|
||||
matches = sorted(glob.glob(pattern), reverse=True) # Newest version first
|
||||
for match in matches:
|
||||
if os.path.isdir(match):
|
||||
log.info(f"Found MATLAB installation: {match}")
|
||||
return match
|
||||
|
||||
elif system == "Windows":
|
||||
# Check common Windows locations
|
||||
search_patterns = [
|
||||
"C:\\Program Files\\MATLAB\\R*",
|
||||
"C:\\Program Files (x86)\\MATLAB\\R*",
|
||||
]
|
||||
for pattern in search_patterns:
|
||||
matches = sorted(glob.glob(pattern), reverse=True)
|
||||
for match in matches:
|
||||
if os.path.isdir(match):
|
||||
log.info(f"Found MATLAB installation: {match}")
|
||||
return match
|
||||
|
||||
elif system == "Linux":
|
||||
# Check common Linux locations
|
||||
search_patterns = [
|
||||
"/usr/local/MATLAB/R*",
|
||||
"/opt/MATLAB/R*",
|
||||
os.path.expanduser("~/MATLAB/R*"),
|
||||
]
|
||||
for pattern in search_patterns:
|
||||
matches = sorted(glob.glob(pattern), reverse=True)
|
||||
for match in matches:
|
||||
if os.path.isdir(match):
|
||||
log.info(f"Found MATLAB installation: {match}")
|
||||
return match
|
||||
|
||||
raise RuntimeError(
|
||||
f"MATLAB installation not found. Set the {MATLAB_PATH_ENV_VAR} environment variable "
|
||||
"to your MATLAB installation directory (e.g., /Applications/MATLAB_R2024b.app on macOS, "
|
||||
"C:\\Program Files\\MATLAB\\R2024b on Windows, or /usr/local/MATLAB/R2024b on Linux)."
|
||||
)
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a MatlabLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
matlab_lsp_command, matlab_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
self._matlab_path = matlab_path
|
||||
|
||||
# Set environment for MATLAB
|
||||
proc_env = {
|
||||
"MATLAB_INSTALL_PATH": matlab_path,
|
||||
}
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=matlab_lsp_command, cwd=repository_root_path, env=proc_env),
|
||||
"matlab",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
@classmethod
|
||||
def _download_matlab_extension(cls, url: str, target_dir: str) -> bool:
|
||||
"""
|
||||
Download and extract the MATLAB extension from VS Code marketplace.
|
||||
|
||||
The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
|
||||
This method downloads the VSIX file and extracts it to get the language server.
|
||||
|
||||
Args:
|
||||
url: VS Code marketplace URL for the MATLAB extension
|
||||
target_dir: Directory where the extension will be extracted
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
log.info(f"Downloading MATLAB extension from {url}")
|
||||
|
||||
# Create target directory for the extension
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
# Download with proper headers to mimic VS Code marketplace client
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept": "application/octet-stream, application/vsix, */*",
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, stream=True, timeout=300)
|
||||
response.raise_for_status()
|
||||
|
||||
# Save to temporary VSIX file
|
||||
temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix")
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
|
||||
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
|
||||
|
||||
with open(temp_file, "wb") as f:
|
||||
downloaded = 0
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0:
|
||||
progress = (downloaded / total_size) * 100
|
||||
log.info(f"Download progress: {progress:.1f}%")
|
||||
|
||||
log.info("Download complete, extracting...")
|
||||
|
||||
# Extract VSIX file (VSIX files are ZIP archives)
|
||||
with zipfile.ZipFile(temp_file, "r") as zip_ref:
|
||||
zip_ref.extractall(target_dir)
|
||||
|
||||
# Clean up temp file
|
||||
os.remove(temp_file)
|
||||
|
||||
log.info("MATLAB extension extracted successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error downloading/extracting MATLAB extension: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _find_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
|
||||
"""
|
||||
Find MATLAB extension in various locations.
|
||||
|
||||
Search order:
|
||||
1. Environment variable (MATLAB_EXTENSION_PATH)
|
||||
2. Default download location (~/.serena/ls_resources/matlab-extension)
|
||||
3. VS Code installed extensions
|
||||
|
||||
Returns:
|
||||
Path to MATLAB extension directory or None if not found
|
||||
|
||||
"""
|
||||
# Check environment variable
|
||||
env_path = os.environ.get("MATLAB_EXTENSION_PATH")
|
||||
if env_path and os.path.exists(env_path):
|
||||
log.debug(f"Found MATLAB extension via MATLAB_EXTENSION_PATH: {env_path}")
|
||||
return env_path
|
||||
elif env_path:
|
||||
log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}")
|
||||
|
||||
# Check default download location
|
||||
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension", "extension")
|
||||
if os.path.exists(default_path):
|
||||
log.debug(f"Found MATLAB extension in default location: {default_path}")
|
||||
return default_path
|
||||
|
||||
# Search VS Code extensions
|
||||
vscode_extensions_dir = os.path.expanduser("~/.vscode/extensions")
|
||||
if os.path.exists(vscode_extensions_dir):
|
||||
for entry in os.listdir(vscode_extensions_dir):
|
||||
if entry.startswith("mathworks.language-matlab"):
|
||||
ext_path = os.path.join(vscode_extensions_dir, entry)
|
||||
if os.path.isdir(ext_path):
|
||||
log.debug(f"Found MATLAB extension in VS Code: {ext_path}")
|
||||
return ext_path
|
||||
|
||||
log.debug("MATLAB extension not found in any known location")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _download_and_install_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
|
||||
"""
|
||||
Download and install MATLAB extension from VS Code marketplace.
|
||||
|
||||
Returns:
|
||||
Path to installed extension or None if download failed
|
||||
|
||||
"""
|
||||
matlab_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension")
|
||||
|
||||
log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}")
|
||||
|
||||
if cls._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir):
|
||||
extension_path = os.path.join(matlab_extension_dir, "extension")
|
||||
if os.path.exists(extension_path):
|
||||
log.info("MATLAB extension downloaded and installed successfully")
|
||||
return extension_path
|
||||
else:
|
||||
log.error(f"Download completed but extension not found at: {extension_path}")
|
||||
else:
|
||||
log.error("Failed to download MATLAB extension from marketplace")
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_executable_path(cls, extension_path: str, system: str) -> str:
|
||||
"""
|
||||
Get the path to the MATLAB language server executable based on platform.
|
||||
|
||||
The language server is a Node.js script located in the extension's server directory.
|
||||
"""
|
||||
# The MATLAB extension bundles the language server in the 'server' directory
|
||||
server_dir = os.path.join(extension_path, "server", "out")
|
||||
main_script = os.path.join(server_dir, "index.js")
|
||||
|
||||
if os.path.exists(main_script):
|
||||
return main_script
|
||||
|
||||
# Alternative location
|
||||
alt_script = os.path.join(extension_path, "out", "index.js")
|
||||
if os.path.exists(alt_script):
|
||||
return alt_script
|
||||
|
||||
raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}")
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str]:
|
||||
"""
|
||||
Setup runtime dependencies for MATLAB Language Server and return the command to start the server.
|
||||
|
||||
Returns:
|
||||
Tuple of (command to start the server, MATLAB installation path)
|
||||
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
# Verify node is installed
|
||||
node_path = shutil.which("node")
|
||||
if node_path is None:
|
||||
raise RuntimeError("Node.js is not installed or isn't in PATH. Please install Node.js and try again.")
|
||||
|
||||
# Get MATLAB path from settings or auto-detect
|
||||
language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
|
||||
matlab_path = language_specific_config.get("matlab_path")
|
||||
|
||||
if not matlab_path:
|
||||
matlab_path = cls._find_matlab_installation() # Raises RuntimeError if not found
|
||||
|
||||
# Verify MATLAB path exists
|
||||
if not os.path.isdir(matlab_path):
|
||||
raise RuntimeError(f"MATLAB installation directory does not exist: {matlab_path}")
|
||||
|
||||
log.info(f"Using MATLAB installation: {matlab_path}")
|
||||
|
||||
# Find existing extension or download if needed
|
||||
extension_path = cls._find_matlab_extension(solidlsp_settings)
|
||||
if extension_path is None:
|
||||
log.info("MATLAB extension not found on disk, attempting to download...")
|
||||
extension_path = cls._download_and_install_matlab_extension(solidlsp_settings)
|
||||
|
||||
if extension_path is None:
|
||||
raise RuntimeError(
|
||||
"Failed to locate or download MATLAB Language Server. Please either:\n"
|
||||
"1. Set MATLAB_EXTENSION_PATH environment variable to the MATLAB extension directory\n"
|
||||
"2. Install the MATLAB extension in VS Code (MathWorks.language-matlab)\n"
|
||||
"3. Ensure internet connection for automatic download"
|
||||
)
|
||||
|
||||
# Get the language server script path
|
||||
server_script = cls._get_executable_path(extension_path, system)
|
||||
|
||||
if not os.path.exists(server_script):
|
||||
raise RuntimeError(f"MATLAB Language Server script not found at: {server_script}")
|
||||
|
||||
# Build the command to run the language server
|
||||
# The MATLAB language server is run via Node.js with the --stdio flag
|
||||
cmd = [node_path, server_script, "--stdio"]
|
||||
|
||||
return cmd, matlab_path
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""Return the initialize params for the MATLAB Language Server."""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {"snippetSupport": True},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True},
|
||||
},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""Start the MATLAB Language Server and wait for it to be ready."""
|
||||
root_uri = pathlib.Path(self.repository_root_path).as_uri()
|
||||
|
||||
def register_capability_handler(params: dict) -> None:
|
||||
assert "registrations" in params
|
||||
for registration in params["registrations"]:
|
||||
if registration["method"] == "workspace/executeCommand":
|
||||
self.initialize_searcher_command_available.set()
|
||||
return
|
||||
|
||||
def execute_client_command_handler(params: dict) -> list:
|
||||
return []
|
||||
|
||||
def workspace_folders_handler(params: dict) -> list:
|
||||
"""Handle workspace/workspaceFolders request from the server."""
|
||||
return [{"uri": root_uri, "name": os.path.basename(self.repository_root_path)}]
|
||||
|
||||
def workspace_configuration_handler(params: dict) -> list:
|
||||
"""Handle workspace/configuration request from the server."""
|
||||
items = params.get("items", [])
|
||||
result = []
|
||||
for item in items:
|
||||
section = item.get("section", "")
|
||||
if section == "MATLAB":
|
||||
# Return MATLAB configuration
|
||||
result.append({"installPath": self._matlab_path, "matlabConnectionTiming": "onStart"})
|
||||
else:
|
||||
result.append({})
|
||||
return result
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
message_text = msg.get("message", "")
|
||||
# Check for MATLAB language server ready signals
|
||||
# Wait for "MVM attach success" or "Adding workspace folder" which indicates MATLAB is fully ready
|
||||
# Note: "connected to" comes earlier but the server isn't fully ready at that point
|
||||
if "mvm attach success" in message_text.lower() or "adding workspace folder" in message_text.lower():
|
||||
log.info("MATLAB language server ready signal detected (MVM attached)")
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_request("workspace/workspaceFolders", workspace_folders_handler)
|
||||
self.server.on_request("workspace/configuration", workspace_configuration_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
log.info("Starting MATLAB server process")
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
log.debug(f"Received initialize response from MATLAB server: {init_response}")
|
||||
|
||||
# Verify basic capabilities
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
assert capabilities.get("textDocumentSync") in [1, 2], "Expected Full or Incremental text sync"
|
||||
|
||||
# Log available capabilities
|
||||
if "completionProvider" in capabilities:
|
||||
log.info("MATLAB server supports completions")
|
||||
if "definitionProvider" in capabilities:
|
||||
log.info("MATLAB server supports go-to-definition")
|
||||
if "referencesProvider" in capabilities:
|
||||
log.info("MATLAB server supports find-references")
|
||||
if "documentSymbolProvider" in capabilities:
|
||||
log.info("MATLAB server supports document symbols")
|
||||
if "documentFormattingProvider" in capabilities:
|
||||
log.info("MATLAB server supports document formatting")
|
||||
if "renameProvider" in capabilities:
|
||||
log.info("MATLAB server supports rename")
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# Wait for server readiness with timeout
|
||||
# MATLAB takes longer to start than most language servers (typically 10-30 seconds)
|
||||
log.info("Waiting for MATLAB language server to be ready (this may take up to 60 seconds)...")
|
||||
if not self.server_ready.wait(timeout=60.0):
|
||||
# Fallback: assume server is ready after timeout
|
||||
log.info("Timeout waiting for MATLAB server ready signal, proceeding anyway")
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
else:
|
||||
log.info("MATLAB server initialization complete")
|
||||
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
"""Define MATLAB-specific directories to ignore."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"slprj", # Simulink project files
|
||||
"codegen", # Code generation output
|
||||
"sldemo_cache", # Simulink demo cache
|
||||
"helperFiles", # Common helper file directories
|
||||
]
|
||||
|
||||
def _request_document_symbols(
|
||||
self, relative_file_path: str, file_data: LSPFileBuffer | None
|
||||
) -> list[SymbolInformation] | list[DocumentSymbol] | None:
|
||||
"""
|
||||
Override to normalize MATLAB symbol names.
|
||||
|
||||
The MATLAB LSP sometimes returns symbol names as lists instead of strings,
|
||||
particularly for script sections (cell mode markers like %%). This method
|
||||
normalizes the names to strings for compatibility with the unified symbol format.
|
||||
"""
|
||||
symbols = super()._request_document_symbols(relative_file_path, file_data)
|
||||
|
||||
if symbols is None or len(symbols) == 0:
|
||||
return symbols
|
||||
|
||||
self._normalize_matlab_symbols(symbols)
|
||||
return symbols
|
||||
|
||||
def _normalize_matlab_symbols(self, symbols: list[SymbolInformation] | list[DocumentSymbol]) -> None:
|
||||
"""
|
||||
Normalize MATLAB symbol names in-place.
|
||||
|
||||
MATLAB LSP returns section names as lists like ["Section Name"] instead of
|
||||
strings. This converts them to plain strings.
|
||||
"""
|
||||
for symbol in symbols:
|
||||
# MATLAB LSP returns names as lists for script sections, violating LSP spec
|
||||
# Cast to Any to handle runtime type that differs from spec
|
||||
name: Any = symbol.get("name")
|
||||
if isinstance(name, list):
|
||||
symbol["name"] = name[0] if name else ""
|
||||
log.debug("Normalized MATLAB symbol name from list to string")
|
||||
|
||||
# Recursively normalize children if present
|
||||
children: Any = symbol.get("children")
|
||||
if children and isinstance(children, list):
|
||||
self._normalize_matlab_symbols(children)
|
||||
@@ -64,6 +64,11 @@ class Language(str, Enum):
|
||||
GROOVY = "groovy"
|
||||
VUE = "vue"
|
||||
POWERSHELL = "powershell"
|
||||
MATLAB = "matlab"
|
||||
"""MATLAB language server using the official MathWorks MATLAB Language Server.
|
||||
Requires MATLAB R2021b or later and Node.js.
|
||||
Set MATLAB_PATH environment variable or configure matlab_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"""
|
||||
@@ -225,6 +230,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.ps1", "*.psm1", "*.psd1")
|
||||
case self.GROOVY:
|
||||
return FilenameMatcher("*.groovy", "*.gvy")
|
||||
case self.MATLAB:
|
||||
return FilenameMatcher("*.m", "*.mlx", "*.mlapp")
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
@@ -390,6 +397,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.groovy_language_server import GroovyLanguageServer
|
||||
|
||||
return GroovyLanguageServer
|
||||
case self.MATLAB:
|
||||
from solidlsp.language_servers.matlab_language_server import MatlabLanguageServer
|
||||
|
||||
return MatlabLanguageServer
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
classdef Calculator < handle
|
||||
% Calculator A simple calculator class for testing MATLAB LSP
|
||||
%
|
||||
% This class provides basic arithmetic operations and demonstrates
|
||||
% MATLAB class structure for LSP testing purposes.
|
||||
|
||||
properties
|
||||
LastResult double = 0
|
||||
History cell = {}
|
||||
end
|
||||
|
||||
properties (Access = private)
|
||||
OperationCount uint32 = 0
|
||||
end
|
||||
|
||||
methods
|
||||
function obj = Calculator()
|
||||
% Constructor for Calculator class
|
||||
obj.LastResult = 0;
|
||||
obj.History = {};
|
||||
obj.OperationCount = 0;
|
||||
end
|
||||
|
||||
function result = add(obj, a, b)
|
||||
% ADD Add two numbers
|
||||
% result = add(obj, a, b) returns the sum of a and b
|
||||
result = a + b;
|
||||
obj.updateState(result, 'add');
|
||||
end
|
||||
|
||||
function result = subtract(obj, a, b)
|
||||
% SUBTRACT Subtract b from a
|
||||
% result = subtract(obj, a, b) returns a - b
|
||||
result = a - b;
|
||||
obj.updateState(result, 'subtract');
|
||||
end
|
||||
|
||||
function result = multiply(obj, a, b)
|
||||
% MULTIPLY Multiply two numbers
|
||||
% result = multiply(obj, a, b) returns a * b
|
||||
result = a * b;
|
||||
obj.updateState(result, 'multiply');
|
||||
end
|
||||
|
||||
function result = divide(obj, a, b)
|
||||
% DIVIDE Divide a by b
|
||||
% result = divide(obj, a, b) returns a / b
|
||||
% Throws error if b is zero
|
||||
if b == 0
|
||||
error('Calculator:DivisionByZero', 'Cannot divide by zero');
|
||||
end
|
||||
result = a / b;
|
||||
obj.updateState(result, 'divide');
|
||||
end
|
||||
|
||||
function displayHistory(obj)
|
||||
% DISPLAYHISTORY Display the calculation history
|
||||
fprintf('Calculation History:\n');
|
||||
for i = 1:length(obj.History)
|
||||
fprintf(' %d: %s = %.4f\n', i, obj.History{i}.operation, obj.History{i}.result);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
methods (Access = private)
|
||||
function updateState(obj, result, operation)
|
||||
% Update internal state after an operation
|
||||
obj.LastResult = result;
|
||||
obj.OperationCount = obj.OperationCount + 1;
|
||||
obj.History{end+1} = struct('operation', operation, 'result', result);
|
||||
end
|
||||
end
|
||||
|
||||
methods (Static)
|
||||
function result = power(base, exponent)
|
||||
% POWER Compute base raised to exponent
|
||||
% result = Calculator.power(base, exponent) returns base^exponent
|
||||
result = base ^ exponent;
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
% MAIN Main script demonstrating Calculator and mathUtils usage
|
||||
%
|
||||
% This script shows how to use the Calculator class and mathUtils
|
||||
% functions together for various mathematical operations.
|
||||
|
||||
% Add lib folder to path
|
||||
addpath('lib');
|
||||
|
||||
%% Section 1: Basic Calculator Operations
|
||||
% Create a calculator instance and perform basic operations
|
||||
|
||||
calc = Calculator();
|
||||
|
||||
% Perform some calculations
|
||||
sum_result = calc.add(10, 5);
|
||||
fprintf('10 + 5 = %d\n', sum_result);
|
||||
|
||||
diff_result = calc.subtract(10, 3);
|
||||
fprintf('10 - 3 = %d\n', diff_result);
|
||||
|
||||
prod_result = calc.multiply(4, 7);
|
||||
fprintf('4 * 7 = %d\n', prod_result);
|
||||
|
||||
quot_result = calc.divide(20, 4);
|
||||
fprintf('20 / 4 = %d\n', quot_result);
|
||||
|
||||
%% Section 2: Static Method Usage
|
||||
% Use the static power method
|
||||
|
||||
power_result = Calculator.power(2, 10);
|
||||
fprintf('2^10 = %d\n', power_result);
|
||||
|
||||
%% Section 3: Math Utilities
|
||||
% Test the mathUtils functions
|
||||
|
||||
% Factorial
|
||||
fact5 = mathUtils('factorial', 5);
|
||||
fprintf('5! = %d\n', fact5);
|
||||
|
||||
% Fibonacci
|
||||
fib10 = mathUtils('fibonacci', 10);
|
||||
fprintf('Fibonacci(10) = %d\n', fib10);
|
||||
|
||||
% Prime check
|
||||
is17prime = mathUtils('isPrime', 17);
|
||||
fprintf('Is 17 prime? %s\n', mat2str(is17prime));
|
||||
|
||||
% Statistics
|
||||
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
[dataMean, dataStd] = mathUtils('stats', data);
|
||||
fprintf('Mean: %.2f, Std: %.2f\n', dataMean, dataStd);
|
||||
|
||||
%% Section 4: Display History
|
||||
% Show all calculations performed by the calculator
|
||||
|
||||
calc.displayHistory();
|
||||
|
||||
%% Section 5: Error Handling
|
||||
% Demonstrate error handling with division by zero
|
||||
|
||||
try
|
||||
calc.divide(10, 0);
|
||||
catch ME
|
||||
fprintf('Caught expected error: %s\n', ME.message);
|
||||
end
|
||||
|
||||
fprintf('\nAll tests completed successfully!\n');
|
||||
@@ -0,0 +1 @@
|
||||
# MATLAB language server tests
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Basic integration tests for the MATLAB language server functionality.
|
||||
|
||||
These tests validate the functionality of the language server APIs
|
||||
like request_document_symbols using the MATLAB test repository.
|
||||
|
||||
Requirements:
|
||||
- MATLAB R2021b or later must be installed
|
||||
- MATLAB_PATH environment variable should be set to MATLAB installation directory
|
||||
- Node.js and npm must be installed
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
# Skip all tests if MATLAB is not available
|
||||
pytestmark = pytest.mark.matlab
|
||||
|
||||
# Check if MATLAB is available
|
||||
MATLAB_AVAILABLE = os.environ.get("MATLAB_PATH") is not None or any(
|
||||
os.path.exists(p)
|
||||
for p in [
|
||||
"/Applications/MATLAB_R2024b.app",
|
||||
"/Applications/MATLAB_R2025b.app",
|
||||
"/Volumes/S1/Applications/MATLAB_R2024b.app",
|
||||
"/Volumes/S1/Applications/MATLAB_R2025b.app",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not MATLAB_AVAILABLE, reason="MATLAB installation not found")
|
||||
class TestMatlabLanguageServerBasics:
|
||||
"""Test basic functionality of the MATLAB language server."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that MATLAB language server can be initialized successfully."""
|
||||
assert language_server is not None
|
||||
assert language_server.language == Language.MATLAB
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_request_document_symbols_class(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols for MATLAB class file."""
|
||||
# Test getting symbols from Calculator.m (class file)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("Calculator.m").get_all_symbols_and_roots()
|
||||
|
||||
# Extract class symbols (LSP Symbol Kind 5 for class)
|
||||
class_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 5]
|
||||
class_names = [symbol["name"] for symbol in class_symbols]
|
||||
|
||||
# Should find the Calculator class
|
||||
assert "Calculator" in class_names, "Should find Calculator class"
|
||||
|
||||
# Extract method symbols (LSP Symbol Kind 6 for method or 12 for function)
|
||||
method_symbols = [symbol for symbol in all_symbols if symbol.get("kind") in [6, 12]]
|
||||
method_names = [symbol["name"] for symbol in method_symbols]
|
||||
|
||||
# Should find key methods
|
||||
expected_methods = ["add", "subtract", "multiply", "divide"]
|
||||
for method in expected_methods:
|
||||
assert method in method_names, f"Should find {method} method in Calculator class"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_request_document_symbols_function(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols for MATLAB function file."""
|
||||
# Test getting symbols from lib/mathUtils.m (function file)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("lib/mathUtils.m").get_all_symbols_and_roots()
|
||||
|
||||
# Extract function symbols (LSP Symbol Kind 12 for function)
|
||||
function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
|
||||
function_names = [symbol["name"] for symbol in function_symbols]
|
||||
|
||||
# Should find the main mathUtils function
|
||||
assert "mathUtils" in function_names, "Should find mathUtils function"
|
||||
|
||||
# Should also find nested/local functions
|
||||
expected_local_functions = ["computeFactorial", "computeFibonacci", "checkPrime", "computeStats"]
|
||||
for func in expected_local_functions:
|
||||
assert func in function_names, f"Should find {func} local function"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_request_document_symbols_script(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_document_symbols for MATLAB script file."""
|
||||
# Test getting symbols from main.m (script file)
|
||||
all_symbols, _root_symbols = language_server.request_document_symbols("main.m").get_all_symbols_and_roots()
|
||||
|
||||
# Scripts may have variables and sections, but less structured symbols
|
||||
# Just verify we can get symbols without errors
|
||||
assert all_symbols is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(not MATLAB_AVAILABLE, reason="MATLAB installation not found")
|
||||
class TestMatlabLanguageServerReferences:
|
||||
"""Test find references functionality of the MATLAB language server."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references within a single MATLAB file."""
|
||||
# Find references to 'result' variable in Calculator.m
|
||||
# This is a basic test to verify references work
|
||||
references = language_server.request_references("Calculator.m", 25, 12) # 'result' in add method
|
||||
|
||||
# Should find at least the definition
|
||||
assert references is not None
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
|
||||
def test_matlab_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references across MATLAB files."""
|
||||
# Find references to Calculator class used in main.m
|
||||
references = language_server.request_references("main.m", 11, 8) # 'Calculator' reference
|
||||
|
||||
# Should find references in both main.m and Calculator.m
|
||||
assert references is not None
|
||||
Reference in new issue
Block a user