mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
feat: Add comprehensive OCaml language support with ocaml-lsp-server integration (#573)
This commit is contained in:
1 parent
2c901b0a0e
commit
13e20c1cbd
19 files changed
+899
-5
No files matched your search
@@ -199,6 +199,23 @@ jobs:
|
||||
- name: Install Ruby language server
|
||||
shell: bash
|
||||
run: gem install ruby-lsp
|
||||
- name: Install OCaml and opam
|
||||
uses: ocaml/setup-ocaml@v3
|
||||
with:
|
||||
ocaml-compiler: ${{ runner.os == 'Windows' && '4.14' || '5.3.x' }}
|
||||
dune-cache: true
|
||||
opam-repositories: |
|
||||
${{ runner.os == 'Windows' && 'opam-repository-mingw: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset' || '' }}
|
||||
default: https://github.com/ocaml/opam-repository.git
|
||||
- name: Install OCaml packages
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
opam install -y dune ocaml-lsp-server
|
||||
else
|
||||
# Require ocaml-lsp-server >= 1.23.0 for cross-file reference support
|
||||
opam install -y dune 'ocaml-lsp-server>=1.23.0'
|
||||
fi
|
||||
- name: Install R
|
||||
uses: r-lib/actions/setup-r@v2
|
||||
with:
|
||||
|
||||
+7
-1
@@ -226,6 +226,9 @@ tmp/
|
||||
# Exception: Don't ignore Nix test repository lib directory (contains source code)
|
||||
!/test/resources/repos/nix/test_repo/lib
|
||||
|
||||
# Exception: Don't ignore OCaml test repository lib directory (contains source code)
|
||||
!/test/resources/repos/ocaml/test_repo/lib
|
||||
|
||||
# Exception: Don't ignore Julia test repository lib directory (contains source code)
|
||||
!/test/resources/repos/julia/test_repo/lib
|
||||
|
||||
@@ -233,6 +236,9 @@ tmp/
|
||||
/test/resources/repos/swift/test_repo/.build
|
||||
/test/resources/repos/swift/test_repo/.swiftpm
|
||||
|
||||
# OCaml
|
||||
/test/resources/repos/ocaml/test_repo/_build
|
||||
|
||||
# Elm
|
||||
/test/resources/repos/elm/test_repo/.elm/
|
||||
/test/resources/repos/elm/test_repo/elm-stuff/
|
||||
@@ -254,4 +260,4 @@ cabal.project.local*
|
||||
.ghc.environment.*
|
||||
|
||||
zz-misc/
|
||||
vue-implementation/
|
||||
vue-implementation/
|
||||
@@ -39,6 +39,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
|
||||
* Language support:
|
||||
|
||||
* **Add support for OCaml** via ocaml-lsp-server with cross-file reference support on OCaml 5.2+ (requires opam; see [setup guide](docs/03-special-guides/ocaml_setup_guide_for_serena.md))
|
||||
* **Add Phpactor as alternative PHP language server** (specify `php_phpactor` as language; requires PHP 8.1+)
|
||||
* **Add support for Fortran** via fortls language server (requires `pip install fortls`)
|
||||
* **Add partial support for Groovy** requires user-provided Groovy language server JAR (see [setup guide](docs/03-special-guides/groovy_setup_guide_for_serena.md))
|
||||
|
||||
@@ -82,22 +82,23 @@ 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, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, 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.
|
||||
> OCaml requires opam and ocaml-lsp-server to be installed manually; see the [OCaml Setup Guide](docs/03-special-guides/ocaml_setup_guide_for_serena.md).
|
||||
|
||||
### The Serena JetBrains Plugin
|
||||
|
||||
As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/)
|
||||
leverages the powerful code analysis capabilities of your JetBrains IDE.
|
||||
The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs,
|
||||
leverages the powerful code analysis capabilities of your JetBrains IDE.
|
||||
The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs,
|
||||
including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, CLion, and others.
|
||||
Only Rider is not supported.
|
||||
|
||||
<a href="https://plugins.jetbrains.com/plugin/28946-serena/"><img src="docs/_static/images/jetbrains-marketplace-button.png"></a>
|
||||
|
||||
The plugin offers the most robust and most powerful Serena experience.
|
||||
The plugin offers the most robust and most powerful Serena experience.
|
||||
See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# OCaml Setup Guide for Serena
|
||||
|
||||
This guide explains how to set up an OCaml project so that Serena can provide code intelligence via ocaml-lsp-server (ocamllsp).
|
||||
|
||||
Unlike some other languages, Serena does not download the OCaml language server automatically. You must install it yourself via opam, as OCaml tooling is compiled from source against your specific environment.
|
||||
|
||||
---
|
||||
## Prerequisites
|
||||
|
||||
Install the following on your system and ensure they are available on `PATH`:
|
||||
|
||||
- **opam** (OCaml package manager)
|
||||
- macOS: `brew install opam`
|
||||
- Ubuntu/Debian: `sudo apt install opam`
|
||||
- Fedora: `sudo dnf install opam`
|
||||
- Other: https://opam.ocaml.org/doc/Install.html
|
||||
- **OCaml compiler** (via opam)
|
||||
- OCaml < 5.1 or >= 5.1.1 (OCaml 5.1.0 is **not supported** by ocaml-lsp-server)
|
||||
- Recommended: OCaml 4.14.x (stable) or 5.2+ (for cross-file references)
|
||||
- **ocaml-lsp-server** (via opam)
|
||||
- **dune** (build system, via opam)
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
1. Initialize opam if you haven't already:
|
||||
```bash
|
||||
opam init
|
||||
eval $(opam env)
|
||||
```
|
||||
|
||||
2. Create an opam switch with a compatible OCaml version:
|
||||
```bash
|
||||
# For cross-file reference support (recommended)
|
||||
opam switch create serena-ocaml ocaml-base-compiler.5.2.1
|
||||
eval $(opam env)
|
||||
|
||||
# Or for stable OCaml 4.14.x
|
||||
opam switch create serena-ocaml ocaml-base-compiler.4.14.2
|
||||
eval $(opam env)
|
||||
```
|
||||
|
||||
3. Install the language server and build tools:
|
||||
```bash
|
||||
opam install ocaml-lsp-server dune
|
||||
```
|
||||
|
||||
4. Verify the installation:
|
||||
```bash
|
||||
opam exec -- ocamllsp --version
|
||||
opam exec -- ocaml -version
|
||||
```
|
||||
|
||||
---
|
||||
## Cross-File References
|
||||
|
||||
Cross-file reference support (finding all usages of a symbol across your project) requires:
|
||||
|
||||
- OCaml >= 5.2
|
||||
- ocaml-lsp-server >= 1.23.0
|
||||
- dune >= 3.16.0
|
||||
|
||||
When these requirements are met, Serena automatically builds the cross-file index during startup via `dune build @ocaml-index`. Without these versions, references are limited to the current file.
|
||||
|
||||
---
|
||||
## Using Serena with OCaml
|
||||
|
||||
- Serena automatically detects OCaml files (`*.ml`, `*.mli`) and Reason files (`*.re`, `*.rei`).
|
||||
- The language server is started via `opam exec -- ocamllsp`, so your opam environment must be configured.
|
||||
- Ensure your project builds successfully with `dune build` before using Serena for best results.
|
||||
|
||||
---
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "opam not found" | Install opam and add it to PATH |
|
||||
| "OCaml 5.1.0 is incompatible" | Create a new switch: `opam switch create <name> ocaml-base-compiler.5.2.1` |
|
||||
| "ocaml-lsp-server not found" | `opam install ocaml-lsp-server` |
|
||||
| Cross-file refs not working | Ensure OCaml >= 5.2 and ocaml-lsp-server >= 1.23.0; run `dune build` first |
|
||||
| Stale index | Rebuild with `dune build @ocaml-index` |
|
||||
|
||||
---
|
||||
## Reference
|
||||
|
||||
- opam: https://opam.ocaml.org
|
||||
- ocaml-lsp-server: https://github.com/ocaml/ocaml-lsp
|
||||
- Project-wide occurrences: https://discuss.ocaml.org/t/ann-project-wide-occurrences-in-merlin-and-lsp/14847
|
||||
@@ -310,6 +310,7 @@ markers = [
|
||||
"nix: language server running for Nix",
|
||||
"dart: language server running for Dart",
|
||||
"erlang: language server running for Erlang",
|
||||
"ocaml: language server running for OCaml and Reason",
|
||||
"scala: language server running for Scala",
|
||||
"al: language server running for AL (Microsoft Dynamics 365 Business Central)",
|
||||
"fsharp: language server running for F#",
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
"""
|
||||
Provides OCaml and Reason specific instantiation of the SolidLanguageServer class.
|
||||
Contains various configurations and settings specific to OCaml and Reason.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OcamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides OCaml and Reason specific instantiation of the SolidLanguageServer class.
|
||||
Contains various configurations and settings specific to OCaml and Reason.
|
||||
"""
|
||||
|
||||
_ocaml_version: tuple[int, int, int]
|
||||
_lsp_version: tuple[int, int, int]
|
||||
_index_built: bool
|
||||
|
||||
# Minimum LSP version for reliable cross-file references
|
||||
MIN_LSP_VERSION_FOR_CROSS_FILE_REFS: tuple[int, int, int] = (1, 23, 0)
|
||||
|
||||
@staticmethod
|
||||
def _ensure_opam_installed() -> None:
|
||||
"""Ensure OPAM is installed and available."""
|
||||
opam_path = shutil.which("opam")
|
||||
if opam_path is None:
|
||||
raise RuntimeError(
|
||||
"OPAM is not installed or not in PATH.\n"
|
||||
"Please install OPAM from: https://opam.ocaml.org/doc/Install.html\n\n"
|
||||
"Installation instructions:\n"
|
||||
" - macOS: brew install opam\n"
|
||||
" - Ubuntu/Debian: sudo apt install opam\n"
|
||||
" - Fedora: sudo dnf install opam\n"
|
||||
" - Windows: https://fdopen.github.io/opam-repository-mingw/installation/\n\n"
|
||||
"After installation, initialize OPAM with: opam init"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _detect_ocaml_version(repository_root_path: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
Detect and return the OCaml version as a tuple (major, minor, patch).
|
||||
Also checks for version compatibility with ocaml-lsp-server.
|
||||
Raises RuntimeError if version cannot be determined.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opam", "exec", "--", "ocaml", "-version"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=repository_root_path,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
version_match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout)
|
||||
if version_match:
|
||||
major = int(version_match.group(1))
|
||||
minor = int(version_match.group(2))
|
||||
patch = int(version_match.group(3))
|
||||
version_tuple = (major, minor, patch)
|
||||
version_str = f"{major}.{minor}.{patch}"
|
||||
log.info(f"OCaml version: {version_str}")
|
||||
|
||||
if version_tuple == (5, 1, 0):
|
||||
raise RuntimeError(
|
||||
f"OCaml {version_str} is incompatible with ocaml-lsp-server.\n"
|
||||
"Please use OCaml < 5.1 or >= 5.1.1.\n"
|
||||
"Consider creating a new opam switch:\n"
|
||||
" opam switch create <name> ocaml-base-compiler.4.14.2"
|
||||
)
|
||||
return version_tuple
|
||||
raise RuntimeError(
|
||||
f"Could not parse OCaml version from output: {result.stdout.strip()}\n"
|
||||
"Please ensure OCaml is properly installed: opam exec -- ocaml -version"
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to detect OCaml version: {e.stderr}\n"
|
||||
"Please ensure OCaml is installed and opam is configured:\n"
|
||||
" opam switch show\n"
|
||||
" opam exec -- ocaml -version"
|
||||
) from e
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError(
|
||||
"OCaml not found. Please install OCaml via opam:\n"
|
||||
" opam switch create <name> ocaml-base-compiler.4.14.2\n"
|
||||
" eval $(opam env)"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def _detect_lsp_version(repository_root_path: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
Detect and return the ocaml-lsp-server version as a tuple (major, minor, patch).
|
||||
Raises RuntimeError if version cannot be determined.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opam", "list", "-i", "ocaml-lsp-server", "--columns=version", "--short"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=repository_root_path,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
version_str = result.stdout.strip()
|
||||
version_match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_str)
|
||||
if version_match:
|
||||
major = int(version_match.group(1))
|
||||
minor = int(version_match.group(2))
|
||||
patch = int(version_match.group(3))
|
||||
version_tuple = (major, minor, patch)
|
||||
log.info(f"ocaml-lsp-server version: {major}.{minor}.{patch}")
|
||||
return version_tuple
|
||||
raise RuntimeError(
|
||||
f"Could not parse ocaml-lsp-server version from output: {version_str}\n"
|
||||
"Please ensure ocaml-lsp-server is properly installed:\n"
|
||||
" opam list -i ocaml-lsp-server"
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to detect ocaml-lsp-server version: {e.stderr}\n"
|
||||
"Please install ocaml-lsp-server:\n"
|
||||
" opam install ocaml-lsp-server"
|
||||
) from e
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError("opam not found. Please install opam:\n https://opam.ocaml.org/doc/Install.html") from e
|
||||
|
||||
@staticmethod
|
||||
def _ensure_ocaml_lsp_installed(repository_root_path: str) -> str:
|
||||
"""
|
||||
Ensure ocaml-lsp-server is installed and return the executable path.
|
||||
Raises RuntimeError with helpful message if not installed.
|
||||
"""
|
||||
# Check if ocaml-lsp-server is installed
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opam", "list", "-i", "ocaml-lsp-server"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=repository_root_path,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
if "ocaml-lsp-server" not in result.stdout or "# No matches found" in result.stdout:
|
||||
raise RuntimeError(
|
||||
"ocaml-lsp-server is not installed.\n\n"
|
||||
"Please install it with:\n"
|
||||
" opam install ocaml-lsp-server\n\n"
|
||||
"Note: ocaml-lsp-server requires OCaml < 5.1 or >= 5.1.1 (OCaml 5.1.0 is not supported).\n"
|
||||
"If you have OCaml 5.1.0, create a new opam switch with a compatible version:\n"
|
||||
" opam switch create <name> ocaml-base-compiler.4.14.2\n"
|
||||
" opam switch <name>\n"
|
||||
" eval $(opam env)\n"
|
||||
" opam install ocaml-lsp-server\n\n"
|
||||
"For more information: https://github.com/ocaml/ocaml-lsp"
|
||||
)
|
||||
log.info("ocaml-lsp-server is installed")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Failed to check ocaml-lsp-server installation: {e.stderr}")
|
||||
|
||||
# Find the executable path
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(
|
||||
["opam", "exec", "--", "where", "ocamllsp"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=repository_root_path,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
executable_path = result.stdout.strip().split("\n")[0]
|
||||
else:
|
||||
result = subprocess.run(
|
||||
["opam", "exec", "--", "which", "ocamllsp"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=repository_root_path,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
executable_path = result.stdout.strip()
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
raise RuntimeError(f"ocaml-lsp-server executable not found at {executable_path}")
|
||||
|
||||
if platform.system() != "Windows":
|
||||
os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC)
|
||||
|
||||
return executable_path
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to find ocaml-lsp-server executable.\n"
|
||||
f"Command failed: {e.cmd}\n"
|
||||
f"Return code: {e.returncode}\n"
|
||||
f"Stderr: {e.stderr}\n\n"
|
||||
"This usually means ocaml-lsp-server is not installed or not in PATH.\n"
|
||||
"Try:\n"
|
||||
" 1. Check opam switch: opam switch show\n"
|
||||
" 2. Install ocaml-lsp-server: opam install ocaml-lsp-server\n"
|
||||
" 3. Ensure opam env is activated: eval $(opam env)"
|
||||
)
|
||||
|
||||
@property
|
||||
def supports_cross_file_references(self) -> bool:
|
||||
"""
|
||||
Check if this OCaml environment supports cross-file references.
|
||||
|
||||
Cross-file references require OCaml >= 5.2 with project-wide occurrences
|
||||
AND ocaml-lsp-server >= 1.23.0 for reliable cross-file reference support.
|
||||
Full requirements:
|
||||
- OCaml 5.2+
|
||||
- ocaml-lsp-server >= 1.23.0 (earlier versions have unreliable cross-file refs)
|
||||
- merlin >= 5.1-502 (provides ocaml-index tool)
|
||||
- dune >= 3.16.0
|
||||
- Index built via `dune build @ocaml-index`
|
||||
- For best results: `dune build -w` running (enables dune RPC)
|
||||
|
||||
Note: Even when this returns True, cross-file refs may not work in all
|
||||
cases. The LSP server needs dune's RPC server (via -w flag) to be fully
|
||||
aware of the index. Without watch mode, cross-file refs are best-effort.
|
||||
|
||||
See: https://discuss.ocaml.org/t/ann-project-wide-occurrences-in-merlin-and-lsp/14847
|
||||
"""
|
||||
ocaml_ok = self._ocaml_version >= (5, 2, 0)
|
||||
lsp_ok = self._lsp_version >= self.MIN_LSP_VERSION_FOR_CROSS_FILE_REFS
|
||||
return ocaml_ok and lsp_ok
|
||||
|
||||
@staticmethod
|
||||
def _build_ocaml_index_static(repository_root_path: str) -> bool:
|
||||
"""
|
||||
Build the OCaml index for project-wide occurrences.
|
||||
This enables cross-file reference finding on OCaml 5.2+.
|
||||
Must be called BEFORE starting the LSP server.
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
log.info("Building OCaml index for cross-file references (dune build @ocaml-index)...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opam", "exec", "--", "dune", "build", "@ocaml-index"],
|
||||
cwd=repository_root_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
**subprocess_kwargs(),
|
||||
)
|
||||
if result.returncode == 0:
|
||||
log.info("OCaml index built successfully")
|
||||
return True
|
||||
else:
|
||||
log.warning(f"Failed to build OCaml index: {result.stderr}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
log.warning("OCaml index build timed out after 120 seconds")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
log.warning("opam not found, cannot build OCaml index")
|
||||
return False
|
||||
except Exception as e:
|
||||
log.warning(f"Error building OCaml index: {e}")
|
||||
return False
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates an OcamlLanguageServer instance.
|
||||
This class is not meant to be instantiated directly. Use SolidLanguageServer.create() instead.
|
||||
"""
|
||||
# Ensure dependencies are available
|
||||
self._ensure_opam_installed()
|
||||
|
||||
# Detect OCaml version for feature gating
|
||||
self._ocaml_version = self._detect_ocaml_version(repository_root_path)
|
||||
self._index_built = False
|
||||
|
||||
# Verify ocaml-lsp-server is installed (we don't need the path, just validation)
|
||||
self._ensure_ocaml_lsp_installed(repository_root_path)
|
||||
|
||||
# Detect LSP version for cross-file reference support
|
||||
self._lsp_version = self._detect_lsp_version(repository_root_path)
|
||||
|
||||
# Build OCaml index BEFORE starting server (required for cross-file refs on OCaml 5.2+)
|
||||
if self._ocaml_version >= (5, 2, 0):
|
||||
self._index_built = self._build_ocaml_index_static(repository_root_path)
|
||||
|
||||
# Use opam exec to run ocamllsp - this ensures correct opam environment
|
||||
# which is required for project-wide occurrences (cross-file references) to work
|
||||
ocaml_lsp_cmd = ["opam", "exec", "--", "ocamllsp", "--fallback-read-dot-merlin"]
|
||||
log.info(f"Using ocaml-lsp-server via: {' '.join(ocaml_lsp_cmd)}")
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=ocaml_lsp_cmd, cwd=repository_root_path),
|
||||
"ocaml",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.completions_available = threading.Event()
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
"""Define language-specific directories to ignore for OCaml projects."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["_build", "_opam", ".opam"]
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the OCaml Language Server.
|
||||
Supports both OCaml and Reason.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"processId": os.getpid(),
|
||||
"clientInfo": {"name": "Serena", "version": "0.1.0"},
|
||||
"locale": "en",
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"configuration": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": True,
|
||||
"willSave": True,
|
||||
"willSaveWaitUntil": True,
|
||||
"didSave": True,
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {
|
||||
"snippetSupport": True,
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
},
|
||||
"hover": {
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
"definition": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
},
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params # type: ignore[return-value]
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""
|
||||
Starts the OCaml Language Server (supports both OCaml and Reason)
|
||||
"""
|
||||
|
||||
def register_capability_handler(params: Any) -> None:
|
||||
if "registrations" in params:
|
||||
for registration in params.get("registrations", []):
|
||||
method = registration.get("method", "")
|
||||
log.info(f"OCaml LSP registered capability: {method}")
|
||||
return
|
||||
|
||||
def lang_status_handler(params: dict[str, Any]) -> None:
|
||||
if params.get("type") == "ServiceReady" and params.get("message") == "ServiceReady":
|
||||
self.server_ready.set()
|
||||
|
||||
def do_nothing(params: Any) -> None:
|
||||
return
|
||||
|
||||
def window_log_message(msg: dict[str, Any]) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
if "initialization done" in msg.get("message", "").lower():
|
||||
self.server_ready.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("language/status", lang_status_handler)
|
||||
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 OCaml LSP 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)
|
||||
|
||||
# Verify expected capabilities
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
log.info(f"OCaml LSP capabilities: {list(capabilities.keys())}")
|
||||
|
||||
text_doc_sync = capabilities.get("textDocumentSync")
|
||||
if isinstance(text_doc_sync, dict):
|
||||
assert text_doc_sync.get("change") == 2, "Expected incremental sync"
|
||||
assert "completionProvider" in capabilities, "Expected completion support"
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
self.server_ready.set()
|
||||
|
||||
log.info("OCaml Language Server initialized successfully")
|
||||
@@ -55,6 +55,7 @@ class Language(str, Enum):
|
||||
LUA = "lua"
|
||||
NIX = "nix"
|
||||
ERLANG = "erlang"
|
||||
OCAML = "ocaml"
|
||||
AL = "al"
|
||||
FSHARP = "fsharp"
|
||||
REGO = "rego"
|
||||
@@ -223,6 +224,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.nix")
|
||||
case self.ERLANG:
|
||||
return FilenameMatcher("*.erl", "*.hrl", "*.escript", "*.config", "*.app", "*.app.src")
|
||||
case self.OCAML:
|
||||
return FilenameMatcher("*.ml", "*.mli", "*.re", "*.rei")
|
||||
case self.AL:
|
||||
return FilenameMatcher("*.al", "*.dal")
|
||||
case self.FSHARP:
|
||||
@@ -405,6 +408,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer
|
||||
|
||||
return ErlangLanguageServer
|
||||
case self.OCAML:
|
||||
from solidlsp.language_servers.ocaml_lsp_server import OcamlLanguageServer
|
||||
|
||||
return OcamlLanguageServer
|
||||
case self.AL:
|
||||
from solidlsp.language_servers.al_language_server import ALLanguageServer
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
(executable
|
||||
(public_name test_repo)
|
||||
(name main)
|
||||
(libraries test_repo))
|
||||
@@ -0,0 +1,9 @@
|
||||
open Test_repo
|
||||
|
||||
let n = 20
|
||||
|
||||
let () =
|
||||
let res = fib n in
|
||||
Printf.printf "fib(%d) = %d\n" n res;
|
||||
let greeting = DemoModule.someFunction "Hello" in
|
||||
Printf.printf "%s\n" greeting
|
||||
@@ -0,0 +1,26 @@
|
||||
(lang dune 3.18)
|
||||
|
||||
(name test_repo)
|
||||
|
||||
(generate_opam_files true)
|
||||
|
||||
(source
|
||||
(github username/reponame))
|
||||
|
||||
(authors "Author Name <author@example.com>")
|
||||
|
||||
(maintainers "Maintainer Name <maintainer@example.com>")
|
||||
|
||||
(license LICENSE)
|
||||
|
||||
(documentation https://url/to/documentation)
|
||||
|
||||
(package
|
||||
(name test_repo)
|
||||
(synopsis "A short synopsis")
|
||||
(description "A longer description")
|
||||
(depends ocaml)
|
||||
(tags
|
||||
("add topics" "to describe" your project)))
|
||||
|
||||
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
|
||||
@@ -0,0 +1,3 @@
|
||||
(library
|
||||
(public_name test_repo)
|
||||
(name test_repo))
|
||||
@@ -0,0 +1,12 @@
|
||||
module DemoModule = struct
|
||||
type value = string
|
||||
|
||||
let someFunction s =
|
||||
s ^ " More String"
|
||||
end
|
||||
|
||||
let rec fib n =
|
||||
if n < 2 then 1
|
||||
else fib (n-1) + fib (n-2)
|
||||
|
||||
let num_domains = 2
|
||||
@@ -0,0 +1,7 @@
|
||||
module DemoModule : sig
|
||||
type value = string
|
||||
val someFunction : string -> string
|
||||
end
|
||||
|
||||
val fib : int -> int
|
||||
val num_domains : int
|
||||
@@ -0,0 +1,3 @@
|
||||
(test
|
||||
(name test_test_repo)
|
||||
(libraries test_repo))
|
||||
@@ -0,0 +1,18 @@
|
||||
open Test_repo
|
||||
|
||||
let test_fib () =
|
||||
assert (fib 0 = 1);
|
||||
assert (fib 1 = 1);
|
||||
assert (fib 2 = 2);
|
||||
assert (fib 5 = 8);
|
||||
Printf.printf "fib tests passed\n"
|
||||
|
||||
let test_demo_module () =
|
||||
let result = DemoModule.someFunction "Test" in
|
||||
assert (result = "Test More String");
|
||||
Printf.printf "DemoModule tests passed\n"
|
||||
|
||||
let () =
|
||||
test_fib ();
|
||||
test_demo_module ();
|
||||
Printf.printf "All tests passed!\n"
|
||||
@@ -0,0 +1,32 @@
|
||||
# This file is generated by dune, edit dune-project instead
|
||||
opam-version: "2.0"
|
||||
synopsis: "A short synopsis"
|
||||
description: "A longer description"
|
||||
maintainer: ["Maintainer Name <maintainer@example.com>"]
|
||||
authors: ["Author Name <author@example.com>"]
|
||||
license: "LICENSE"
|
||||
tags: ["add topics" "to describe" "your" "project"]
|
||||
homepage: "https://github.com/username/reponame"
|
||||
doc: "https://url/to/documentation"
|
||||
bug-reports: "https://github.com/username/reponame/issues"
|
||||
depends: [
|
||||
"dune" {>= "3.18"}
|
||||
"ocaml"
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
build: [
|
||||
["dune" "subst"] {dev}
|
||||
[
|
||||
"dune"
|
||||
"build"
|
||||
"-p"
|
||||
name
|
||||
"-j"
|
||||
jobs
|
||||
"@install"
|
||||
"@runtest" {with-test}
|
||||
"@doc" {with-doc}
|
||||
]
|
||||
]
|
||||
dev-repo: "git+https://github.com/username/reponame.git"
|
||||
x-maintenance-intent: ["(latest)"]
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Test cross-file references for OCaml.
|
||||
|
||||
Cross-file references require OCaml >= 5.2 and ocaml-lsp-server >= 1.23.0.
|
||||
On environments without these (e.g. Windows CI with OCaml 4.14), only
|
||||
same-file references are asserted.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.language_servers.ocaml_lsp_server import OcamlLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.ocaml
|
||||
class TestCrossFileReferences:
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_fib_has_cross_file_references(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that fib function references are found across multiple files.
|
||||
|
||||
The `fib` function is defined in lib/test_repo.ml and used in:
|
||||
- lib/test_repo.ml (definition + 2 recursive calls)
|
||||
- bin/main.ml (1 call)
|
||||
- test/test_test_repo.ml (5 references)
|
||||
|
||||
Total: 9 references across 3 files.
|
||||
"""
|
||||
file_path = os.path.join("lib", "test_repo.ml")
|
||||
|
||||
fib_line = 7
|
||||
fib_char = 8
|
||||
|
||||
refs = language_server.request_references(file_path, fib_line, fib_char)
|
||||
|
||||
lib_refs = [ref for ref in refs if "lib/test_repo.ml" in ref.get("uri", "")]
|
||||
bin_refs = [ref for ref in refs if "bin/main.ml" in ref.get("uri", "")]
|
||||
test_refs = [ref for ref in refs if "test/test_test_repo.ml" in ref.get("uri", "")]
|
||||
|
||||
log.info("Cross-file references result:")
|
||||
log.info(f"Total references found: {len(refs)}")
|
||||
log.info(f" lib/test_repo.ml: {len(lib_refs)}")
|
||||
log.info(f" bin/main.ml: {len(bin_refs)}")
|
||||
log.info(f" test/test_test_repo.ml: {len(test_refs)}")
|
||||
|
||||
for ref in refs:
|
||||
uri = ref.get("uri", "")
|
||||
filename = uri.split("/")[-1]
|
||||
line = ref.get("range", {}).get("start", {}).get("line", -1)
|
||||
log.info(f" {filename}:{line}")
|
||||
|
||||
# Same-file references always work
|
||||
assert len(lib_refs) >= 3, f"Expected at least 3 references in lib/test_repo.ml (definition + 2 recursive), but got {len(lib_refs)}"
|
||||
|
||||
# Cross-file references require OCaml >= 5.2 and ocaml-lsp-server >= 1.23.0
|
||||
if isinstance(language_server, OcamlLanguageServer) and language_server.supports_cross_file_references:
|
||||
assert len(refs) >= 9, (
|
||||
f"Expected at least 9 total references (3 in lib + 1 in bin + 5 in test), "
|
||||
f"but got {len(refs)}. Cross-file references are NOT working!"
|
||||
)
|
||||
|
||||
assert len(bin_refs) >= 1, (
|
||||
f"Expected at least 1 reference in bin/main.ml, but got {len(bin_refs)}. "
|
||||
"Cross-file references are NOT working - bin/main.ml not found!"
|
||||
)
|
||||
|
||||
assert len(test_refs) >= 1, (
|
||||
f"Expected at least 1 reference in test/test_test_repo.ml, but got {len(test_refs)}. "
|
||||
"Cross-file references are NOT working - test file not found!"
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
@pytest.mark.ocaml
|
||||
class TestOCamlLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoModule"), "DemoModule not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "fib"), "fib not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "someFunction"), "someFunction function not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("lib", "test_repo.ml")
|
||||
|
||||
# Use the correct character position for 'fib' function name
|
||||
# Line 8: "let rec fib n =" - 'fib' starts at character 8 (0-indexed)
|
||||
fib_line = 7 # 0-indexed line number
|
||||
fib_char = 8 # 0-indexed character position
|
||||
|
||||
refs = language_server.request_references(file_path, fib_line, fib_char)
|
||||
|
||||
# Should find at least 3 references: definition + 2 recursive calls in same file
|
||||
assert len(refs) >= 3, f"Expected at least 3 references to fib (definition + 2 recursive), found {len(refs)}"
|
||||
|
||||
# All references should be in lib/test_repo.ml (same file as definition)
|
||||
# Use forward slashes for URI matching (URIs always use /)
|
||||
lib_refs = [ref for ref in refs if "lib/test_repo.ml" in ref.get("uri", "")]
|
||||
assert len(lib_refs) >= 3, f"Expected at least 3 references in lib/test_repo.ml, found {len(lib_refs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_mixed_ocaml_modules(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that the language server can find symbols from OCaml modules"""
|
||||
# Test that full symbol tree includes symbols from various file types
|
||||
all_symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
# Should find symbols from main OCaml files
|
||||
assert SymbolUtils.symbol_tree_contains_name(all_symbols, "fib"), "Should find fib from .ml file"
|
||||
assert SymbolUtils.symbol_tree_contains_name(all_symbols, "DemoModule"), "Should find DemoModule from .ml file"
|
||||
assert SymbolUtils.symbol_tree_contains_name(all_symbols, "someFunction"), "Should find someFunction from DemoModule"
|
||||
assert SymbolUtils.symbol_tree_contains_name(all_symbols, "num_domains"), "Should find num_domains constant"
|
||||
|
||||
def test_reason_file_patterns(self) -> None:
|
||||
"""Test that OCaml language configuration recognizes Reason file extensions"""
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
ocaml_lang = Language.OCAML
|
||||
file_matcher = ocaml_lang.get_source_fn_matcher()
|
||||
|
||||
# Test OCaml extensions
|
||||
assert file_matcher.is_relevant_filename("test.ml"), "Should match .ml files"
|
||||
assert file_matcher.is_relevant_filename("test.mli"), "Should match .mli files"
|
||||
|
||||
# Test Reason extensions
|
||||
assert file_matcher.is_relevant_filename("test.re"), "Should match .re files"
|
||||
assert file_matcher.is_relevant_filename("test.rei"), "Should match .rei files"
|
||||
|
||||
# Test non-matching extensions
|
||||
assert not file_matcher.is_relevant_filename("test.py"), "Should not match .py files"
|
||||
assert not file_matcher.is_relevant_filename("test.js"), "Should not match .js files"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_module_hierarchy_navigation(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test navigation within module hierarchy including DemoModule."""
|
||||
file_path = os.path.join("lib", "test_repo.ml")
|
||||
|
||||
# Use correct position for 'DemoModule' (line 1, char 7)
|
||||
# Line 1: "module DemoModule = struct" - 'DemoModule' starts around char 7
|
||||
module_line = 0 # 0-indexed
|
||||
module_char = 7 # 0-indexed
|
||||
|
||||
refs = language_server.request_references(file_path, module_line, module_char)
|
||||
|
||||
# Should find at least 1 reference (the definition)
|
||||
assert len(refs) >= 1, f"Expected at least 1 reference to DemoModule, found {len(refs)}"
|
||||
|
||||
# Check that references are found - use forward slashes for URI matching
|
||||
lib_refs = [ref for ref in refs if "lib/test_repo.ml" in ref.get("uri", "")]
|
||||
assert len(lib_refs) >= 1, f"Expected at least 1 reference in lib/test_repo.ml, found {len(lib_refs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_let_binding_references(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references to let-bound values across files."""
|
||||
file_path = os.path.join("lib", "test_repo.ml")
|
||||
|
||||
# Use correct position for 'num_domains' (line 12, char 4)
|
||||
# Line 12: "let num_domains = 2" - 'num_domains' starts around char 4
|
||||
num_domains_line = 11 # 0-indexed
|
||||
num_domains_char = 4 # 0-indexed
|
||||
|
||||
refs = language_server.request_references(file_path, num_domains_line, num_domains_char)
|
||||
|
||||
# Should find at least 1 reference (the definition)
|
||||
assert len(refs) >= 1, f"Expected at least 1 reference to num_domains, found {len(refs)}"
|
||||
|
||||
# Check that reference is found in the definition file - use forward slashes
|
||||
ml_refs = [ref for ref in refs if "lib/test_repo.ml" in ref.get("uri", "")]
|
||||
assert len(ml_refs) >= 1, f"Expected at least 1 reference in lib/test_repo.ml, found {len(ml_refs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_recursive_function_analysis(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that recursive function calls are properly identified within the definition file."""
|
||||
file_path = os.path.join("lib", "test_repo.ml")
|
||||
|
||||
# Use correct position for 'fib' function name (line 8, char 8)
|
||||
fib_line = 7 # 0-indexed
|
||||
fib_char = 8 # 0-indexed
|
||||
|
||||
refs = language_server.request_references(file_path, fib_line, fib_char)
|
||||
|
||||
# Filter to references within the definition file only - use forward slashes
|
||||
same_file_refs = [ref for ref in refs if "lib/test_repo.ml" in ref.get("uri", "")]
|
||||
|
||||
# Should find at least 3 references in test_repo.ml: definition + 2 recursive calls
|
||||
# On OCaml 5.2+ with cross-file refs, there may be more total refs but same-file count stays the same
|
||||
assert (
|
||||
len(same_file_refs) >= 3
|
||||
), f"Expected at least 3 references in test_repo.ml (definition + 2 recursive), found {len(same_file_refs)}"
|
||||
|
||||
# Verify references are on different lines (definition + recursive calls)
|
||||
ref_lines = [ref.get("range", {}).get("start", {}).get("line", -1) for ref in same_file_refs]
|
||||
unique_lines = len(set(ref_lines))
|
||||
assert unique_lines >= 2, f"Recursive calls should appear on multiple lines, found {unique_lines} unique lines"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True)
|
||||
def test_open_statement_resolution(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that open statements allow unqualified access to module contents."""
|
||||
# In bin/main.ml, fib is called without Test_repo prefix due to 'open Test_repo'
|
||||
all_symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
# Should be able to find fib through symbol tree
|
||||
fib_accessible = SymbolUtils.symbol_tree_contains_name(all_symbols, "fib")
|
||||
assert fib_accessible, "fib should be accessible through open statement"
|
||||
|
||||
# DemoModule should also be accessible
|
||||
demo_module_accessible = SymbolUtils.symbol_tree_contains_name(all_symbols, "DemoModule")
|
||||
assert demo_module_accessible, "DemoModule should be accessible"
|
||||
|
||||
# Verify we have access to both qualified and unqualified symbols
|
||||
assert len(all_symbols) > 0, "Should find symbols from OCaml files"
|
||||
|
||||
# Test that the language server recognizes the open statement context
|
||||
file_path = os.path.join("bin", "main.ml")
|
||||
symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
assert len(symbols) > 0, "Should find symbols in main.ml that use opened modules"
|
||||
Reference in new issue
Block a user