mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 00:15:05 +00:00
Added YAML language server (#763)
Also minor fixes in Elixir LS and a more customizable logging (inferring log level from the log message) for LS implementations
This commit is contained in:
1 parent
e26c8b5195
commit
81a0fd875c
19 files changed
+615
-27
No files matched your search
+2
-1
@@ -31,9 +31,10 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* **Add support for Zig** via ZLS (cross-file references may not fully work on Windows)
|
||||
* **Add support for Lua** via lua-language-server
|
||||
* **Add support for Nix** requires nixd installation (Windows not supported)
|
||||
* **Add experimental support for YAML** via yaml-language-server with LSP integration for .yaml and .yml files
|
||||
* **Dart now officially supported**: Dart was always working, but now tests were added, and it is promoted to "officially supported"
|
||||
* **Rust now uses already installed rustup**: The rust-analyzer is no longer bundled with Serena. Instead, it uses the rust-analyzer from your Rust toolchain managed by rustup. This ensures compatibility with your Rust version and eliminates outdated bundled binaries.
|
||||
* **Kotlin now officially supported**: We now use the official Kotlin LS, tests run through and performance is good, even though the LS is in an early development stage.
|
||||
* **Kotlin now officially supported**: We now use the official Kotlin LS, tests run through and performance is good, even though the LS is in an early development stage.
|
||||
* **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.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ than existing solutions that charge a premium.
|
||||
|
||||
Language servers provide support for a wide range of programming languages.
|
||||
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, Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, Python, R, Ruby, Rust, Scala, Swift, TypeScript and Zig.
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, Python, R, Ruby, Rust, Scala, Swift, TypeScript, YAML and Zig.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Some languages require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
|
||||
|
||||
@@ -297,6 +297,7 @@ markers = [
|
||||
"julia: Julia language server tests",
|
||||
"fortran: language server running for Fortran",
|
||||
"haskell: Haskell language server tests",
|
||||
"yaml: language server running for YAML",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# haskell java julia kotlin lua markdown
|
||||
# nix perl php python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala swift
|
||||
# terraform typescript typescript_vts zig
|
||||
# terraform typescript typescript_vts yaml zig
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
|
||||
@@ -187,7 +187,8 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
self.logger.log("Waiting for Bash language server to be ready...", logging.INFO)
|
||||
if not self.server_ready.wait(timeout=3.0):
|
||||
# Fallback: assume server is ready after timeout
|
||||
self.logger.log("Timeout waiting for bash server ready signal, proceeding anyway", logging.WARNING)
|
||||
# This is common. bash-language-server doesn't always send explicit ready signals. Log as info
|
||||
self.logger.log("Timeout waiting for bash server ready signal, proceeding anyway", logging.INFO)
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,6 @@ import pathlib
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from overrides import override
|
||||
|
||||
@@ -34,6 +33,15 @@ class ElixirTools(SolidLanguageServer):
|
||||
# - cover: coverage reports
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", "cover"]
|
||||
|
||||
@override
|
||||
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
|
||||
"""Check if a path should be ignored for symbol indexing."""
|
||||
if relative_path.endswith("mix.exs"):
|
||||
# These are project configuration files, not source code with symbols to index
|
||||
return True
|
||||
|
||||
return super().is_ignored_path(relative_path, ignore_unsupported_files)
|
||||
|
||||
def _is_next_ls_internal_file(self, abs_path: str) -> bool:
|
||||
"""Check if an absolute path is a Next LS internal file that should be ignored."""
|
||||
return any(
|
||||
@@ -309,7 +317,7 @@ class ElixirTools(SolidLanguageServer):
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", check_server_ready)
|
||||
self.server.on_notification("window/workDoneProgress/create", do_nothing)
|
||||
self.server.on_request("window/workDoneProgress/create", do_nothing)
|
||||
self.server.on_notification("$/workDoneProgress", work_done_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
@@ -329,11 +337,12 @@ class ElixirTools(SolidLanguageServer):
|
||||
# Next LS may not provide all capabilities immediately, so we check for basic ones
|
||||
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}"
|
||||
|
||||
# Some capabilities might be optional or provided later
|
||||
# Some capabilities might be optional or provided later. This is expected, so we log as info
|
||||
if "completionProvider" not in init_response["capabilities"]:
|
||||
self.logger.log("Warning: completionProvider not available in initial capabilities", logging.WARNING)
|
||||
self.logger.log("completionProvider not available in initial capabilities", logging.INFO)
|
||||
|
||||
if "definitionProvider" not in init_response["capabilities"]:
|
||||
self.logger.log("Warning: definitionProvider not available in initial capabilities", logging.WARNING)
|
||||
self.logger.log("definitionProvider not available in initial capabilities", logging.INFO)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
@@ -346,12 +355,6 @@ class ElixirTools(SolidLanguageServer):
|
||||
if self.server_ready.wait(timeout=ready_timeout):
|
||||
self.logger.log("Next LS is ready and available for requests", logging.INFO)
|
||||
|
||||
# Add a small settling period to ensure background indexing is complete
|
||||
# Next LS often continues compilation/indexing in background after ready signal
|
||||
settling_time = 120.0
|
||||
self.logger.log(f"Allowing {settling_time} seconds for Next LS background indexing to complete...", logging.INFO)
|
||||
time.sleep(settling_time)
|
||||
self.logger.log("Next LS settling period complete", logging.INFO)
|
||||
else:
|
||||
error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself."
|
||||
self.logger.log(error_msg, logging.ERROR)
|
||||
|
||||
@@ -27,6 +27,24 @@ class Gopls(SolidLanguageServer):
|
||||
# - dist/build: common output directories
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["vendor", "node_modules", "dist", "build"]
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify gopls stderr output to avoid false-positive errors."""
|
||||
line_lower = line.lower()
|
||||
|
||||
# File discovery messages that are not actual errors
|
||||
if any(
|
||||
[
|
||||
"discover.go:" in line_lower,
|
||||
"walker.go:" in line_lower,
|
||||
"walking of {file://" in line_lower,
|
||||
"bus: -> discover" in line_lower,
|
||||
]
|
||||
):
|
||||
return logging.DEBUG
|
||||
|
||||
return SolidLanguageServer._determine_log_level(line)
|
||||
|
||||
@staticmethod
|
||||
def _get_go_version():
|
||||
"""Get the installed Go version or None if not found."""
|
||||
|
||||
@@ -5,7 +5,6 @@ Provides Rust specific instantiation of the LanguageServer class. Contains vario
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
@@ -24,6 +23,22 @@ class RustAnalyzer(SolidLanguageServer):
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify rust-analyzer stderr output to avoid false-positive errors."""
|
||||
line_lower = line.lower()
|
||||
|
||||
# Known informational/warning messages from rust-analyzer that aren't critical errors
|
||||
if any(
|
||||
[
|
||||
"failed to find any projects in" in line_lower,
|
||||
"fetchworkspaceerror" in line_lower,
|
||||
]
|
||||
):
|
||||
return logging.DEBUG
|
||||
|
||||
return SolidLanguageServer._determine_log_level(line)
|
||||
|
||||
@staticmethod
|
||||
def _get_rustup_version():
|
||||
"""Get installed rustup version or None if not found."""
|
||||
@@ -37,17 +52,17 @@ class RustAnalyzer(SolidLanguageServer):
|
||||
|
||||
@staticmethod
|
||||
def _get_rust_analyzer_path():
|
||||
"""Get rust-analyzer path via rustup or system PATH."""
|
||||
# First try rustup
|
||||
"""Get rust-analyzer path via rustup. Returns None if not found."""
|
||||
try:
|
||||
# Note: we avoid using system PATH to avoid picking up incorrect aliases
|
||||
result = subprocess.run(["rustup", "which", "rust-analyzer"], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Fallback to system PATH
|
||||
return shutil.which("rust-analyzer")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _ensure_rust_analyzer_installed():
|
||||
|
||||
@@ -25,6 +25,34 @@ class TerraformLS(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"]
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify terraform-ls stderr output to avoid false-positive errors."""
|
||||
line_lower = line.lower()
|
||||
|
||||
# File discovery messages that are not actual errors
|
||||
if any(
|
||||
[
|
||||
"discover.go:" in line_lower,
|
||||
"walker.go:" in line_lower,
|
||||
"walking of {file://" in line_lower,
|
||||
"bus: -> discover" in line_lower,
|
||||
]
|
||||
):
|
||||
return logging.DEBUG
|
||||
|
||||
# Known informational messages from terraform-ls that contain "error" but aren't errors
|
||||
# Note: pattern match is flexible to handle file paths between keywords
|
||||
if any(
|
||||
[
|
||||
"loading module metadata returned error:" in line_lower and "state not changed" in line_lower,
|
||||
"incoming notification for" in line_lower,
|
||||
]
|
||||
):
|
||||
return logging.DEBUG
|
||||
|
||||
return SolidLanguageServer._determine_log_level(line)
|
||||
|
||||
@staticmethod
|
||||
def _ensure_tf_command_available(logger: LanguageServerLogger):
|
||||
logger.log("Starting terraform version detection...", logging.DEBUG)
|
||||
|
||||
@@ -69,6 +69,11 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
"coverage",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify typescript-language-server stderr output to avoid false-positive errors."""
|
||||
return SolidLanguageServer._determine_log_level(line)
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(
|
||||
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Provides YAML specific instantiation of the LanguageServer class using yaml-language-server.
|
||||
Contains various configurations and settings specific to YAML files.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
class YamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides YAML specific instantiation of the LanguageServer class using yaml-language-server.
|
||||
Contains various configurations and settings specific to YAML files.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify yaml-language-server stderr output to avoid false-positive errors."""
|
||||
line_lower = line.lower()
|
||||
|
||||
# Known informational messages from yaml-language-server that aren't critical errors
|
||||
if any(
|
||||
[
|
||||
"cannot find module" in line_lower and "package.json" in line_lower, # Schema resolution - not critical
|
||||
"no parser" in line_lower, # Parser messages - informational
|
||||
]
|
||||
):
|
||||
return logging.DEBUG
|
||||
|
||||
return SolidLanguageServer._determine_log_level(line)
|
||||
|
||||
def __init__(
|
||||
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
|
||||
):
|
||||
"""
|
||||
Creates a YamlLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
yaml_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=yaml_lsp_executable_path, cwd=repository_root_path),
|
||||
"yaml",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(
|
||||
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
|
||||
) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for YAML Language Server and return the command to start the server.
|
||||
"""
|
||||
# Verify both node and npm are installed
|
||||
is_node_installed = shutil.which("node") is not None
|
||||
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="yaml-language-server",
|
||||
description="yaml-language-server package (Red Hat)",
|
||||
command="npm install --prefix ./ yaml-language-server@1.19.2",
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install yaml-language-server if not already installed
|
||||
yaml_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "yaml-lsp")
|
||||
yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server")
|
||||
|
||||
# Handle Windows executable extension
|
||||
if os.name == "nt":
|
||||
yaml_executable_path += ".cmd"
|
||||
|
||||
if not os.path.exists(yaml_executable_path):
|
||||
logger.log(f"YAML Language Server executable not found at {yaml_executable_path}. Installing...", logging.INFO)
|
||||
deps.install(logger, yaml_ls_dir)
|
||||
logger.log("YAML language server dependencies installed successfully", logging.INFO)
|
||||
|
||||
if not os.path.exists(yaml_executable_path):
|
||||
raise FileNotFoundError(
|
||||
f"yaml-language-server executable not found at {yaml_executable_path}, something went wrong with the installation."
|
||||
)
|
||||
return f"{yaml_executable_path} --stdio"
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the YAML 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"]},
|
||||
"codeAction": {"dynamicRegistration": 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),
|
||||
}
|
||||
],
|
||||
"initializationOptions": {
|
||||
"yaml": {
|
||||
"schemaStore": {"enable": True, "url": "https://www.schemastore.org/api/json/catalog.json"},
|
||||
"format": {"enable": True},
|
||||
"validate": True,
|
||||
"hover": True,
|
||||
"completion": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the YAML Language Server, waits for the server to be ready and yields the LanguageServer instance.
|
||||
"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_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)
|
||||
|
||||
self.logger.log("Starting YAML server process", logging.INFO)
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to LSP server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
self.logger.log(f"Received initialize response from YAML server: {init_response}", logging.DEBUG)
|
||||
|
||||
# Verify document symbol support is available
|
||||
if "documentSymbolProvider" in init_response["capabilities"]:
|
||||
self.logger.log("YAML server supports document symbols", logging.INFO)
|
||||
else:
|
||||
self.logger.log("Warning: YAML server does not report document symbol support", logging.WARNING)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# YAML language server is ready immediately after initialization
|
||||
self.logger.log("YAML server initialization complete", logging.INFO)
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
@@ -151,6 +151,26 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
return dirname.startswith(".")
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""
|
||||
Classify a stderr line from the language server to determine appropriate logging level.
|
||||
|
||||
Language servers may emit informational messages to stderr that contain words like "error"
|
||||
but are not actual errors. Subclasses can override this method to filter out known
|
||||
false-positive patterns specific to their language server.
|
||||
|
||||
:param line: The stderr line to classify
|
||||
:return: A logging level (logging.DEBUG, logging.INFO, logging.WARNING, or logging.ERROR)
|
||||
"""
|
||||
line_lower = line.lower()
|
||||
|
||||
# Default classification: treat lines with "error" or "exception" as ERROR level
|
||||
if "error" in line_lower or "exception" in line_lower or line.startswith("E["):
|
||||
return logging.ERROR
|
||||
else:
|
||||
return logging.INFO
|
||||
|
||||
@classmethod
|
||||
def get_language_enum_instance(cls) -> Language:
|
||||
return Language.from_ls_class(cls)
|
||||
@@ -291,6 +311,7 @@ class SolidLanguageServer(ABC):
|
||||
self.server = SolidLanguageServerHandler(
|
||||
process_launch_info,
|
||||
language=self.language,
|
||||
determine_log_level=self._determine_log_level,
|
||||
logger=logging_fn,
|
||||
start_independent_lsp_process=config.start_independent_lsp_process,
|
||||
)
|
||||
|
||||
@@ -78,6 +78,10 @@ class Language(str, Enum):
|
||||
Must be explicitly specified as the main language, not auto-detected.
|
||||
This is an edge case primarily useful when working on documentation-heavy projects.
|
||||
"""
|
||||
YAML = "yaml"
|
||||
"""YAML language server (experimental).
|
||||
Must be explicitly specified as the main language, not auto-detected.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def iter_all(cls, include_experimental: bool = False) -> Iterable[Self]:
|
||||
@@ -89,7 +93,7 @@ class Language(str, Enum):
|
||||
"""
|
||||
Check if the language server is experimental or deprecated.
|
||||
"""
|
||||
return self in {self.TYPESCRIPT_VTS, self.PYTHON_JEDI, self.CSHARP_OMNISHARP, self.RUBY_SOLARGRAPH, self.MARKDOWN}
|
||||
return self in {self.TYPESCRIPT_VTS, self.PYTHON_JEDI, self.CSHARP_OMNISHARP, self.RUBY_SOLARGRAPH, self.MARKDOWN, self.YAML}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -142,6 +146,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.swift")
|
||||
case self.BASH:
|
||||
return FilenameMatcher("*.sh", "*.bash")
|
||||
case self.YAML:
|
||||
return FilenameMatcher("*.yaml", "*.yml")
|
||||
case self.ZIG:
|
||||
return FilenameMatcher("*.zig", "*.zon")
|
||||
case self.LUA:
|
||||
@@ -259,6 +265,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.bash_language_server import BashLanguageServer
|
||||
|
||||
return BashLanguageServer
|
||||
case self.YAML:
|
||||
from solidlsp.language_servers.yaml_language_server import YamlLanguageServer
|
||||
|
||||
return YamlLanguageServer
|
||||
case self.ZIG:
|
||||
from solidlsp.language_servers.zls import ZigLanguageServer
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ class LanguageServerTerminatedException(Exception):
|
||||
|
||||
|
||||
class Request(ToStringMixin):
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
payload: PayloadLike | None = None
|
||||
@@ -135,11 +134,13 @@ class SolidLanguageServerHandler:
|
||||
self,
|
||||
process_launch_info: ProcessLaunchInfo,
|
||||
language: Language,
|
||||
determine_log_level: Callable[[str], int],
|
||||
logger: Callable[[str, str, StringDict | str], None] | None = None,
|
||||
start_independent_lsp_process=True,
|
||||
request_timeout: float | None = None,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self._determine_log_level = determine_log_level
|
||||
self.send = LanguageServerRequest(self)
|
||||
self.notify = LspNotification(self.send_notification)
|
||||
|
||||
@@ -385,11 +386,7 @@ class SolidLanguageServerHandler:
|
||||
if not line:
|
||||
continue
|
||||
line = line.decode(ENCODING, errors="replace")
|
||||
line_lower = line.lower()
|
||||
if "error" in line_lower or "exception" in line_lower or line.startswith("E["):
|
||||
level = logging.ERROR
|
||||
else:
|
||||
level = logging.INFO
|
||||
level = self._determine_log_level(line)
|
||||
log.log(level, line)
|
||||
except Exception as e:
|
||||
log.error("Error while reading stderr from language server process: %s", e, exc_info=e)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Application configuration
|
||||
app:
|
||||
name: test-application
|
||||
version: 1.0.0
|
||||
port: 8080
|
||||
debug: true
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
name: testdb
|
||||
username: admin
|
||||
password: secret123
|
||||
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
outputs:
|
||||
- console
|
||||
- file
|
||||
|
||||
features:
|
||||
authentication: true
|
||||
caching: false
|
||||
monitoring: true
|
||||
@@ -0,0 +1,41 @@
|
||||
# Sample data structure
|
||||
users:
|
||||
- id: 1
|
||||
name: John Doe
|
||||
email: john@example.com
|
||||
roles:
|
||||
- admin
|
||||
- developer
|
||||
active: true
|
||||
|
||||
- id: 2
|
||||
name: Jane Smith
|
||||
email: jane@example.com
|
||||
roles:
|
||||
- developer
|
||||
active: true
|
||||
|
||||
- id: 3
|
||||
name: Bob Johnson
|
||||
email: bob@example.com
|
||||
roles:
|
||||
- viewer
|
||||
active: false
|
||||
|
||||
projects:
|
||||
- name: project-alpha
|
||||
status: active
|
||||
team:
|
||||
- John Doe
|
||||
- Jane Smith
|
||||
tags:
|
||||
- backend
|
||||
- api
|
||||
|
||||
- name: project-beta
|
||||
status: planning
|
||||
team:
|
||||
- Jane Smith
|
||||
tags:
|
||||
- frontend
|
||||
- ui
|
||||
@@ -0,0 +1,51 @@
|
||||
# Docker compose services definition
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html
|
||||
environment:
|
||||
- NGINX_HOST=localhost
|
||||
- NGINX_PORT=80
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
api:
|
||||
image: node:18
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- database
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=database
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
database:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=mydb
|
||||
- POSTGRES_USER=admin
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- backend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
backend:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
Whitespace-only changes.
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Basic integration tests for the YAML language server functionality.
|
||||
|
||||
These tests validate the functionality of the language server APIs
|
||||
like request_document_symbols using the YAML test repository.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
|
||||
@pytest.mark.yaml
|
||||
class TestYAMLLanguageServerBasics:
|
||||
"""Test basic functionality of the YAML language server."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_language_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test that YAML language server can be initialized successfully."""
|
||||
assert language_server is not None
|
||||
assert language_server.language == Language.YAML
|
||||
assert language_server.is_running()
|
||||
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_config_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test document symbols detection in config.yaml with specific symbol verification."""
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols is not None, "Should return symbols for config.yaml"
|
||||
assert len(all_symbols) > 0, f"Should find symbols in config.yaml, found {len(all_symbols)}"
|
||||
|
||||
# Verify specific top-level keys are detected
|
||||
symbol_names = [sym.get("name") for sym in all_symbols]
|
||||
assert "app" in symbol_names, "Should detect 'app' key in config.yaml"
|
||||
assert "database" in symbol_names, "Should detect 'database' key in config.yaml"
|
||||
assert "logging" in symbol_names, "Should detect 'logging' key in config.yaml"
|
||||
assert "features" in symbol_names, "Should detect 'features' key in config.yaml"
|
||||
|
||||
# Verify nested symbols exist (child keys under 'app')
|
||||
assert "name" in symbol_names, "Should detect nested 'name' key"
|
||||
assert "port" in symbol_names, "Should detect nested 'port' key"
|
||||
assert "debug" in symbol_names, "Should detect nested 'debug' key"
|
||||
|
||||
# Check symbol kinds are appropriate (LSP kinds: 2=module/namespace, 15=string, 16=number, 17=boolean)
|
||||
app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
|
||||
assert app_symbol is not None, "Should find 'app' symbol"
|
||||
assert app_symbol.get("kind") == 2, "Top-level object should have kind 2 (module/namespace)"
|
||||
|
||||
port_symbol = next((s for s in all_symbols if s.get("name") == "port"), None)
|
||||
assert port_symbol is not None, "Should find 'port' symbol"
|
||||
assert port_symbol.get("kind") == 16, "'port' should have kind 16 (number)"
|
||||
|
||||
debug_symbol = next((s for s in all_symbols if s.get("name") == "debug"), None)
|
||||
assert debug_symbol is not None, "Should find 'debug' symbol"
|
||||
assert debug_symbol.get("kind") == 17, "'debug' should have kind 17 (boolean)"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_services_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test symbol detection in services.yml Docker Compose file."""
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("services.yml").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols is not None, "Should return symbols for services.yml"
|
||||
assert len(all_symbols) > 0, f"Should find symbols in services.yml, found {len(all_symbols)}"
|
||||
|
||||
# Verify specific top-level keys from Docker Compose file
|
||||
symbol_names = [sym.get("name") for sym in all_symbols]
|
||||
assert "version" in symbol_names, "Should detect 'version' key"
|
||||
assert "services" in symbol_names, "Should detect 'services' key"
|
||||
assert "networks" in symbol_names, "Should detect 'networks' key"
|
||||
assert "volumes" in symbol_names, "Should detect 'volumes' key"
|
||||
|
||||
# Verify service names
|
||||
assert "web" in symbol_names, "Should detect 'web' service"
|
||||
assert "api" in symbol_names, "Should detect 'api' service"
|
||||
assert "database" in symbol_names, "Should detect 'database' service"
|
||||
|
||||
# Check that arrays are properly detected
|
||||
ports_symbols = [s for s in all_symbols if s.get("name") == "ports"]
|
||||
assert len(ports_symbols) > 0, "Should find 'ports' arrays in services"
|
||||
# Arrays should have kind 18
|
||||
for ports_sym in ports_symbols:
|
||||
assert ports_sym.get("kind") == 18, "'ports' should have kind 18 (array)"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_data_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test symbol detection in data.yaml file with array structures."""
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("data.yaml").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols is not None, "Should return symbols for data.yaml"
|
||||
assert len(all_symbols) > 0, f"Should find symbols in data.yaml, found {len(all_symbols)}"
|
||||
|
||||
# Verify top-level keys
|
||||
symbol_names = [sym.get("name") for sym in all_symbols]
|
||||
assert "users" in symbol_names, "Should detect 'users' array"
|
||||
assert "projects" in symbol_names, "Should detect 'projects' array"
|
||||
|
||||
# Verify array elements (indexed by position)
|
||||
# data.yaml has user entries and project entries
|
||||
assert "id" in symbol_names, "Should detect 'id' fields in array elements"
|
||||
assert "name" in symbol_names, "Should detect 'name' fields"
|
||||
assert "email" in symbol_names, "Should detect 'email' fields"
|
||||
assert "roles" in symbol_names, "Should detect 'roles' arrays"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_symbols_with_body(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test request_document_symbols with body extraction."""
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols is not None, "Should return symbols for config.yaml"
|
||||
assert len(all_symbols) > 0, "Should have symbols"
|
||||
|
||||
# Find the 'app' symbol and verify its body
|
||||
app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
|
||||
assert app_symbol is not None, "Should find 'app' symbol"
|
||||
|
||||
# Check that body exists and contains expected content
|
||||
assert "body" in app_symbol, "'app' symbol should have body"
|
||||
app_body = app_symbol["body"]
|
||||
assert "app:" in app_body, "Body should start with 'app:'"
|
||||
assert "name: test-application" in app_body, "Body should contain 'name' field"
|
||||
assert "version: 1.0.0" in app_body, "Body should contain 'version' field"
|
||||
assert "port: 8080" in app_body, "Body should contain 'port' field"
|
||||
assert "debug: true" in app_body, "Body should contain 'debug' field"
|
||||
|
||||
# Find a simple string value symbol and verify its body
|
||||
name_symbols = [s for s in all_symbols if s.get("name") == "name" and "body" in s]
|
||||
assert len(name_symbols) > 0, "Should find 'name' symbols with bodies"
|
||||
# At least one should contain "test-application"
|
||||
assert any("test-application" in s["body"] for s in name_symbols), "Should find name with test-application"
|
||||
|
||||
# Find the database symbol and check its body
|
||||
database_symbol = next((s for s in all_symbols if s.get("name") == "database"), None)
|
||||
assert database_symbol is not None, "Should find 'database' symbol"
|
||||
assert "body" in database_symbol, "'database' symbol should have body"
|
||||
db_body = database_symbol["body"]
|
||||
assert "database:" in db_body, "Body should start with 'database:'"
|
||||
assert "host: localhost" in db_body, "Body should contain host configuration"
|
||||
assert "port: 5432" in db_body, "Body should contain port configuration"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
|
||||
def test_yaml_symbol_ranges(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test that symbols have proper range information."""
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
|
||||
|
||||
assert all_symbols is not None
|
||||
assert len(all_symbols) > 0
|
||||
|
||||
# Check the 'app' symbol range
|
||||
app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
|
||||
assert app_symbol is not None, "Should find 'app' symbol"
|
||||
assert "range" in app_symbol, "'app' symbol should have range"
|
||||
|
||||
app_range = app_symbol["range"]
|
||||
assert "start" in app_range, "Range should have start"
|
||||
assert "end" in app_range, "Range should have end"
|
||||
assert app_range["start"]["line"] == 1, "'app' should start at line 1 (0-indexed, actual line 2)"
|
||||
# The app block spans from line 2 to line 7 in the file (1-indexed)
|
||||
# In 0-indexed LSP coordinates: line 1 (start) to line 6 (end)
|
||||
assert app_range["end"]["line"] == 6, "'app' should end at line 6 (0-indexed)"
|
||||
|
||||
# Check a nested symbol range
|
||||
port_symbols = [s for s in all_symbols if s.get("name") == "port"]
|
||||
assert len(port_symbols) > 0, "Should find 'port' symbols"
|
||||
# Find the one under 'app' (should be at line 4 in 0-indexed, actual line 5)
|
||||
app_port = next((s for s in port_symbols if s["range"]["start"]["line"] == 4), None)
|
||||
assert app_port is not None, "Should find 'port' under 'app'"
|
||||
assert app_port["range"]["start"]["character"] == 2, "'port' should be indented 2 spaces"
|
||||
Reference in new issue
Block a user