Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Status of the `main` branch. Changes prior to the next official version change w

* Language support:

* **Add support for OpenTofu** via the existing Terraform language server integration, with CLI discovery for both `tofu` and `terraform`
* **Add support for Lean 4** via built-in `lean --server` with cross-file reference support (requires `lean` and `lake` via [elan](https://github.com/leanprover/elan))
* **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+)
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ You can choose either of these backends depending on your preferences and requir

### Language Servers

Serena incorporates a powerful abstraction layer for the integration of language servers that implement the language server protocol (LSP).
The underlying language servers are typically open-source projects or at least freely available for use.
Serena incorporates a powerful abstraction layer for the integration of language servers
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.

When using Serena's language server backend, we provide **support for over 40 programming languages**, including
AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, OpenTofu Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.

### The Serena JetBrains Plugin

Expand Down
2 changes: 2 additions & 0 deletions docs/01-about/020_programming-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Some languages require additional installations or setup steps, as noted.
(requires nixd installation)
* **OCaml**
(requires opam and ocaml-lsp-server to be installed manually; see the [OCaml Setup Guide](../03-special-guides/ocaml_setup_guide_for_serena.md))
* **OpenTofu**
(language `opentofu`; uses the Terraform language server and supports repositories that have `tofu` or `terraform` installed)
* **Pascal**
(uses Pascal/Lazarus, which is automatically downloaded; set `PP` and `FPCDIR` environment variables for source navigation)
* **Perl**
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ markers = [
"elixir: language server running for Elixir",
"elm: language server running for Elm",
"terraform: language server running for Terraform",
"opentofu: language server running for OpenTofu",
"swift: language server running for Swift",
"bash: language server running for Bash",
"r: language server running for R",
Expand Down
3 changes: 2 additions & 1 deletion src/serena/resources/project.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ project_name: "project_name"
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# swift terraform opentofu toml typescript
# typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
Expand Down
54 changes: 31 additions & 23 deletions src/solidlsp/language_servers/terraform_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

class TerraformLS(SolidLanguageServer):
"""
Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
Provides Terraform/OpenTofu specific instantiation of the LanguageServer class using terraform-ls.

You can pass the following entries in ``ls_specific_settings["terraform"]``:
- terraform_ls_version: Override the pinned terraform-ls version downloaded
Expand Down Expand Up @@ -62,33 +62,41 @@ def _determine_log_level(line: str) -> int:

@staticmethod
def _ensure_tf_command_available() -> None:
log.debug("Starting terraform version detection...")
log.debug("Starting Terraform/OpenTofu CLI detection...")

# 1. Try standard CLI names from PATH
for binary_name in ["terraform", "tofu"]:
cli_cmd = shutil.which(binary_name)
if cli_cmd is not None:
log.debug(f"Found {binary_name} via shutil.which: {cli_cmd}")
return

terraform_cmd = None

# 2. Fallback to explicitly configured CLI paths
for env_var_name, binary_name in [("TOFU_CLI_PATH", "tofu"), ("TERRAFORM_CLI_PATH", "terraform")]:
cli_path = os.environ.get(env_var_name)
if not cli_path:
continue

log.debug(f"Trying {env_var_name}: {cli_path}")
if os.name == "nt":
cli_binary = os.path.join(cli_path, f"{binary_name}.exe")
else:
cli_binary = os.path.join(cli_path, binary_name)

if os.path.exists(cli_binary):
terraform_cmd = cli_binary
log.debug(f"Found {binary_name} via {env_var_name}: {terraform_cmd}")
return

# 1. Try to find terraform using shutil.which
terraform_cmd = shutil.which("terraform")
if terraform_cmd is not None:
log.debug(f"Found terraform via shutil.which: {terraform_cmd}")
return

# TODO: is this needed?
# 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
if not terraform_cmd:
terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH")
if terraform_cli_path:
log.debug(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}")
# TODO: use binary name from runtime dependencies if we keep this code
if os.name == "nt":
terraform_binary = os.path.join(terraform_cli_path, "terraform.exe")
else:
terraform_binary = os.path.join(terraform_cli_path, "terraform")
if os.path.exists(terraform_binary):
terraform_cmd = terraform_binary
log.debug(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}")
return

raise RuntimeError(
"Terraform executable not found, please ensure Terraform is installed."
"See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions."
"Terraform/OpenTofu executable not found, please ensure Terraform or OpenTofu is installed."
"See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli"
" or https://opentofu.org/docs/intro/install/ for instructions."
)

@classmethod
Expand Down
7 changes: 4 additions & 3 deletions src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Language(str, Enum):
ELIXIR = "elixir"
ELM = "elm"
TERRAFORM = "terraform"
OPENTOFU = "opentofu"
SWIFT = "swift"
BASH = "bash"
CRYSTAL = "crystal"
Expand Down Expand Up @@ -231,8 +232,8 @@ def get_source_fn_matcher(self) -> FilenameMatcher:
return FilenameMatcher("*.ex", "*.exs")
case self.ELM:
return FilenameMatcher("*.elm")
case self.TERRAFORM:
return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
case self.TERRAFORM | self.OPENTOFU:
return FilenameMatcher("*.tf")
case self.SWIFT:
return FilenameMatcher("*.swift")
case self.BASH:
Expand Down Expand Up @@ -411,7 +412,7 @@ def get_ls_class(self) -> type["SolidLanguageServer"]:
from solidlsp.language_servers.elm_language_server import ElmLanguageServer

return ElmLanguageServer
case self.TERRAFORM:
case self.TERRAFORM | self.OPENTOFU:
from solidlsp.language_servers.terraform_ls import TerraformLS

return TerraformLS
Expand Down
1 change: 1 addition & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class LanguageParamRequest:

_LANGUAGE_REPO_ALIASES: dict[Language, Language] = {
Language.CPP_CCLS: Language.CPP,
Language.OPENTOFU: Language.TERRAFORM,
Language.PHP_PHPACTOR: Language.PHP,
Language.PYTHON_JEDI: Language.PYTHON,
Language.RUBY_SOLARGRAPH: Language.RUBY,
Expand Down
40 changes: 40 additions & 0 deletions test/solidlsp/opentofu/test_opentofu_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest

from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language


@pytest.mark.opentofu
class TestLanguageServerBasics:
"""Test basic functionality of the OpenTofu language server configuration."""

@pytest.mark.parametrize("language_server", [Language.OPENTOFU], indirect=True)
def test_basic_definition(self, language_server: SolidLanguageServer) -> None:
"""Test basic definition lookup functionality."""
file_path = "main.tf"
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
assert len(symbols) > 0, "Should find at least some symbols in main.tf"

@pytest.mark.parametrize("language_server", [Language.OPENTOFU], indirect=True)
def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on an aws_instance resource."""
file_path = "main.tf"
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None)
if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol:
raise AssertionError("aws_instance symbol or its selectionRange not found")
sel_start = aws_instance_symbol["selectionRange"]["start"]
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert len(references) >= 1, "aws_instance should be referenced at least once"

@pytest.mark.parametrize("language_server", [Language.OPENTOFU], indirect=True)
def test_request_references_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on a variable."""
file_path = "variables.tf"
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None)
if not var_symbol or "selectionRange" not in var_symbol:
raise AssertionError("variable symbol or its selectionRange not found")
sel_start = var_symbol["selectionRange"]["start"]
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert len(references) >= 1, "variable should be referenced at least once"
13 changes: 13 additions & 0 deletions test/solidlsp/terraform/test_terraform_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@
class TestLanguageServerBasics:
"""Test basic functionality of the Terraform language server."""

def test_source_matcher_excludes_tfvars_and_tfstate(self) -> None:
"""Test that indexing targets only Terraform source files."""
terraform_matcher = Language.TERRAFORM.get_source_fn_matcher()
opentofu_matcher = Language.OPENTOFU.get_source_fn_matcher()

assert terraform_matcher.is_relevant_filename("main.tf")
assert opentofu_matcher.is_relevant_filename("main.tf")

assert not terraform_matcher.is_relevant_filename("terraform.tfvars")
assert not terraform_matcher.is_relevant_filename("terraform.tfstate")
assert not opentofu_matcher.is_relevant_filename("terraform.tfvars")
assert not opentofu_matcher.is_relevant_filename("terraform.tfstate")

@pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
def test_basic_definition(self, language_server: SolidLanguageServer) -> None:
"""Test basic definition lookup functionality."""
Expand Down
Loading