mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 14:15:14 +00:00
Add HLSL and related languages support through shader-language-server (#1093)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
dadf3f6a18
commit
dbd7bf7596
12 files changed
+603
-3
No files matched your search
@@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
|
||||
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
|
||||
|
||||
With Serena's LSP library, we provide **support for over 30 programming languages**, including
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (partial support), Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig.
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, 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.
|
||||
|
||||
> [!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.
|
||||
|
||||
@@ -52,8 +52,12 @@ Some languages require additional installations or setup steps, as noted.
|
||||
(requires installation of `gopls`)
|
||||
* **Groovy**
|
||||
(requires local groovy-language-server.jar setup via `GROOVY_LS_JAR_PATH` or configuration)
|
||||
* **Haskell**
|
||||
* **Haskell**
|
||||
(automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
|
||||
* **HLSL / GLSL / WGSL**
|
||||
(uses [shader-language-server](https://github.com/antaalt/shader-sense) (language `hlsl`); automatically downloaded;
|
||||
on macOS, requires Rust toolchain for building from source;
|
||||
note: reference search is not supported by this language server)
|
||||
* **Java**
|
||||
* **JavaScript**
|
||||
* **Julia**
|
||||
|
||||
+2
-1
@@ -326,11 +326,12 @@ markers = [
|
||||
"toml: language server running for TOML",
|
||||
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
|
||||
"systemverilog: language server running for SystemVerilog (uses verible-verilog-ls)",
|
||||
"hlsl: language server running for HLSL shaders (uses shader-language-server)",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = '.git*,*.svg,*.lock,*.min.*'
|
||||
check-hidden = true
|
||||
# ignore-regex = ''
|
||||
ignore-regex = '\.\w+'
|
||||
ignore-words-list = 'paket'
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Shader language server using shader-language-server (antaalt/shader-sense).
|
||||
Supports HLSL, GLSL, and WGSL shader file formats.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# GitHub release version to download when not installed locally
|
||||
_DEFAULT_VERSION = "1.3.0"
|
||||
_GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download"
|
||||
|
||||
|
||||
class HlslLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Shader language server using shader-language-server.
|
||||
Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
|
||||
super().__init__(config, repository_root_path, None, "hlsl", solidlsp_settings)
|
||||
|
||||
@override
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
# 1. Check PATH for system-installed binary
|
||||
system_binary = shutil.which("shader-language-server")
|
||||
if system_binary:
|
||||
log.info(f"Using system-installed shader-language-server at {system_binary}")
|
||||
return system_binary
|
||||
|
||||
# 2. Try to download pre-built binary from GitHub releases
|
||||
version = self._custom_settings.get("version", _DEFAULT_VERSION)
|
||||
tag = f"v{version}"
|
||||
base_url = f"{_GITHUB_RELEASE_BASE}/{tag}"
|
||||
|
||||
# macOS has no pre-built binaries; build from source via cargo install
|
||||
cargo_install_cmd = f"cargo install shader_language_server --version {version} --root ."
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="shader-language-server",
|
||||
description="shader-language-server for Windows (x64)",
|
||||
url=f"{base_url}/shader-language-server-x86_64-pc-windows-msvc.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="shader-language-server.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="shader-language-server",
|
||||
description="shader-language-server for Linux (x64)",
|
||||
url=f"{base_url}/shader-language-server-x86_64-unknown-linux-gnu.zip",
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="shader-language-server",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="shader-language-server",
|
||||
description="shader-language-server for Windows (ARM64)",
|
||||
url=f"{base_url}/shader-language-server-aarch64-pc-windows-msvc.zip",
|
||||
platform_id="win-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="shader-language-server.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="shader-language-server",
|
||||
description="shader-language-server for macOS (x64) - built from source",
|
||||
command=cargo_install_cmd,
|
||||
platform_id="osx-x64",
|
||||
binary_name="bin/shader-language-server",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="shader-language-server",
|
||||
description="shader-language-server for macOS (ARM64) - built from source",
|
||||
command=cargo_install_cmd,
|
||||
platform_id="osx-arm64",
|
||||
binary_name="bin/shader-language-server",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
dep = deps.get_single_dep_for_current_platform()
|
||||
except RuntimeError:
|
||||
dep = None
|
||||
|
||||
if dep is None:
|
||||
raise FileNotFoundError(
|
||||
"shader-language-server is not installed and no auto-install is available for your platform.\n"
|
||||
"Please install it using one of the following methods:\n"
|
||||
" cargo: cargo install shader_language_server\n"
|
||||
" GitHub: Download from https://github.com/antaalt/shader-sense/releases\n"
|
||||
"On macOS, install the Rust toolchain (https://rustup.rs) and Serena will build from source automatically.\n"
|
||||
"See https://github.com/antaalt/shader-sense for more details."
|
||||
)
|
||||
|
||||
install_dir = os.path.join(self._ls_resources_dir, "shader-language-server")
|
||||
executable_path = deps.binary_path(install_dir)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
log.info(f"shader-language-server not found. Downloading from {dep.url}")
|
||||
_ = deps.install(install_dir)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
raise FileNotFoundError(f"shader-language-server not found at {executable_path}")
|
||||
|
||||
os.chmod(executable_path, 0o755)
|
||||
return executable_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {"snippetSupport": True},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"hover": {
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": True,
|
||||
"signatureInformation": {
|
||||
"parameterInformation": {"labelOffsetSupport": True},
|
||||
},
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"configuration": True,
|
||||
},
|
||||
},
|
||||
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
|
||||
}
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
@override
|
||||
def _start_server(self) -> None:
|
||||
def do_nothing(params: Any) -> None:
|
||||
return
|
||||
|
||||
def on_log_message(params: Any) -> None:
|
||||
message = params.get("message", "") if isinstance(params, dict) else str(params)
|
||||
log.info(f"shader-language-server: {message}")
|
||||
|
||||
def on_configuration_request(params: Any) -> list[dict]:
|
||||
"""Respond to workspace/configuration requests.
|
||||
|
||||
shader-language-server requests config with section 'shader-validator'.
|
||||
Return empty config to use defaults.
|
||||
"""
|
||||
items = params.get("items", []) if isinstance(params, dict) else []
|
||||
return [{}] * len(items)
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_request("workspace/configuration", on_configuration_request)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("window/logMessage", on_log_message)
|
||||
|
||||
log.info("Starting shader-language-server process")
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
log.info("Sending initialize request")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
capabilities = init_response.get("capabilities", {})
|
||||
log.info(f"Initialize response capabilities: {list(capabilities.keys())}")
|
||||
assert "textDocumentSync" in capabilities, "shader-language-server must support textDocumentSync"
|
||||
if "documentSymbolProvider" not in capabilities:
|
||||
log.warning("shader-language-server does not advertise documentSymbolProvider")
|
||||
if "definitionProvider" not in capabilities:
|
||||
log.warning("shader-language-server does not advertise definitionProvider")
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
"""Ignore Unity-specific directories that contain no user-authored shaders."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in {"Library", "Temp", "Logs", "obj", "Packages"}
|
||||
@@ -105,6 +105,11 @@ class Language(str, Enum):
|
||||
"""TOML language server using Taplo.
|
||||
Supports TOML validation, formatting, and schema support.
|
||||
"""
|
||||
HLSL = "hlsl"
|
||||
"""Shader language server using shader-language-server (antaalt/shader-sense).
|
||||
Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files.
|
||||
Automatically downloads shader-language-server binary.
|
||||
"""
|
||||
SYSTEMVERILOG = "systemverilog"
|
||||
"""SystemVerilog language server using verible-verilog-ls.
|
||||
Supports .sv, .svh, .v, .vh files.
|
||||
@@ -251,6 +256,24 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.groovy", "*.gvy")
|
||||
case self.MATLAB:
|
||||
return FilenameMatcher("*.m", "*.mlx", "*.mlapp")
|
||||
case self.HLSL:
|
||||
return FilenameMatcher(
|
||||
"*.hlsl",
|
||||
"*.hlsli",
|
||||
"*.fx",
|
||||
"*.fxh",
|
||||
"*.cginc",
|
||||
"*.compute",
|
||||
"*.shader",
|
||||
"*.glsl",
|
||||
"*.vert",
|
||||
"*.frag",
|
||||
"*.geom",
|
||||
"*.tesc",
|
||||
"*.tese",
|
||||
"*.comp",
|
||||
"*.wgsl",
|
||||
)
|
||||
case self.SYSTEMVERILOG:
|
||||
return FilenameMatcher("*.sv", "*.svh", "*.v", "*.vh")
|
||||
case _:
|
||||
@@ -434,6 +457,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.matlab_language_server import MatlabLanguageServer
|
||||
|
||||
return MatlabLanguageServer
|
||||
case self.HLSL:
|
||||
from solidlsp.language_servers.hlsl_language_server import HlslLanguageServer
|
||||
|
||||
return HlslLanguageServer
|
||||
case self.SYSTEMVERILOG:
|
||||
from solidlsp.language_servers.systemverilog_server import SystemVerilogLanguageServer
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef COMMON_HLSL
|
||||
#define COMMON_HLSL
|
||||
|
||||
struct VertexInput
|
||||
{
|
||||
float3 position : POSITION;
|
||||
float3 normal : NORMAL;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct VertexOutput
|
||||
{
|
||||
float4 clipPos : SV_POSITION;
|
||||
float3 worldNormal : TEXCOORD0;
|
||||
float2 uv : TEXCOORD1;
|
||||
};
|
||||
|
||||
float3 SafeNormalize(float3 v)
|
||||
{
|
||||
float len = length(v);
|
||||
return len > 0.0001 ? v / len : float3(0, 0, 0);
|
||||
}
|
||||
|
||||
float Remap(float value, float fromMin, float fromMax, float toMin, float toMax)
|
||||
{
|
||||
return toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin);
|
||||
}
|
||||
|
||||
#endif // COMMON_HLSL
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "common.hlsl"
|
||||
|
||||
RWTexture2D<float4> OutputTexture : register(u0);
|
||||
Texture2D<float4> InputTexture : register(t0);
|
||||
|
||||
cbuffer ComputeParams : register(b0)
|
||||
{
|
||||
uint2 TextureSize;
|
||||
float BlurRadius;
|
||||
float _Pad;
|
||||
};
|
||||
|
||||
[numthreads(8, 8, 1)]
|
||||
void CSMain(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if (id.x >= TextureSize.x || id.y >= TextureSize.y)
|
||||
return;
|
||||
|
||||
float4 color = InputTexture[id.xy];
|
||||
float3 remapped = float3(
|
||||
Remap(color.r, 0.0, 1.0, 0.2, 0.8),
|
||||
Remap(color.g, 0.0, 1.0, 0.2, 0.8),
|
||||
Remap(color.b, 0.0, 1.0, 0.2, 0.8)
|
||||
);
|
||||
OutputTexture[id.xy] = float4(remapped, color.a);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef LIGHTING_HLSL
|
||||
#define LIGHTING_HLSL
|
||||
|
||||
#include "common.hlsl"
|
||||
|
||||
cbuffer LightingConstants : register(b0)
|
||||
{
|
||||
float4x4 ViewProjection;
|
||||
float3 LightDirection;
|
||||
float LightIntensity;
|
||||
float3 AmbientColor;
|
||||
float _Padding;
|
||||
};
|
||||
|
||||
float3 CalculateDiffuse(float3 normal, float3 lightDir, float3 albedo)
|
||||
{
|
||||
float ndotl = max(dot(normal, -lightDir), 0.0);
|
||||
return albedo * ndotl;
|
||||
}
|
||||
|
||||
float3 CalculateSpecular(float3 normal, float3 lightDir, float3 viewDir, float shininess)
|
||||
{
|
||||
float3 halfVec = SafeNormalize(-lightDir + viewDir);
|
||||
float ndoth = max(dot(normal, halfVec), 0.0);
|
||||
return pow(ndoth, shininess);
|
||||
}
|
||||
|
||||
VertexOutput TransformVertex(VertexInput input)
|
||||
{
|
||||
VertexOutput output;
|
||||
output.clipPos = mul(ViewProjection, float4(input.position, 1.0));
|
||||
output.worldNormal = input.normal;
|
||||
output.uv = input.uv;
|
||||
return output;
|
||||
}
|
||||
|
||||
#endif // LIGHTING_HLSL
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef TERRAIN_SDF_HLSL
|
||||
#define TERRAIN_SDF_HLSL
|
||||
|
||||
#include "../common.hlsl"
|
||||
|
||||
struct SDFBrickData
|
||||
{
|
||||
float3 center;
|
||||
float halfExtent;
|
||||
int resolution;
|
||||
float maxDistance;
|
||||
};
|
||||
|
||||
float3 WorldOffset;
|
||||
|
||||
float SampleSDF(float3 worldPos, SDFBrickData brick)
|
||||
{
|
||||
float3 localPos = worldPos - brick.center;
|
||||
float dist = length(localPos) - brick.halfExtent;
|
||||
return dist;
|
||||
}
|
||||
|
||||
float3 CalculateGradient(float3 worldPos, SDFBrickData brick)
|
||||
{
|
||||
float eps = 0.01;
|
||||
float3 gradient;
|
||||
gradient.x = SampleSDF(worldPos + float3(eps, 0, 0), brick) - SampleSDF(worldPos - float3(eps, 0, 0), brick);
|
||||
gradient.y = SampleSDF(worldPos + float3(0, eps, 0), brick) - SampleSDF(worldPos - float3(0, eps, 0), brick);
|
||||
gradient.z = SampleSDF(worldPos + float3(0, 0, eps), brick) - SampleSDF(worldPos - float3(0, 0, eps), brick);
|
||||
return SafeNormalize(gradient);
|
||||
}
|
||||
|
||||
#endif // TERRAIN_SDF_HLSL
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Basic tests for HLSL language server integration (shader-language-server).
|
||||
|
||||
This module tests Language.HLSL using shader-language-server from antaalt/shader-sense.
|
||||
Tests are skipped if the language server is not available.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None:
|
||||
"""Find a top-level symbol by name in a file's document symbols."""
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
return next((s for s in symbols[0] if s.get("name") == name), None)
|
||||
|
||||
|
||||
# ── Symbol Discovery ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.hlsl
|
||||
class TestHlslSymbols:
|
||||
"""Tests for document symbol extraction."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_find_struct(self, language_server: SolidLanguageServer) -> None:
|
||||
"""VertexInput struct should appear in common.hlsl symbols."""
|
||||
symbol = _find_symbol_by_name(language_server, "common.hlsl", "VertexInput")
|
||||
assert symbol is not None, "Expected 'VertexInput' struct in document symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_find_function(self, language_server: SolidLanguageServer) -> None:
|
||||
"""SafeNormalize function should appear in common.hlsl."""
|
||||
symbol = _find_symbol_by_name(language_server, "common.hlsl", "SafeNormalize")
|
||||
assert symbol is not None, "Expected 'SafeNormalize' function in document symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_find_cbuffer_members(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Cbuffer members should appear as variables in compute_test.hlsl.
|
||||
|
||||
Note: shader-language-server reports cbuffer members as individual
|
||||
variables (kind 13), not the cbuffer name itself as a symbol.
|
||||
"""
|
||||
symbol = _find_symbol_by_name(language_server, "compute_test.hlsl", "TextureSize")
|
||||
assert symbol is not None, "Expected 'TextureSize' cbuffer member in document symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_find_compute_kernel(self, language_server: SolidLanguageServer) -> None:
|
||||
"""CSMain kernel should appear in compute_test.hlsl."""
|
||||
symbol = _find_symbol_by_name(language_server, "compute_test.hlsl", "CSMain")
|
||||
assert symbol is not None, "Expected 'CSMain' compute kernel in document symbols"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Full symbol tree should contain symbols from multiple files."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "VertexInput"), "VertexInput not in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculateDiffuse"), "CalculateDiffuse not in symbol tree"
|
||||
|
||||
|
||||
# ── Go-to-Definition ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.hlsl
|
||||
class TestHlslDefinition:
|
||||
"""Tests for go-to-definition capability."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_goto_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Navigating to SafeNormalize call in lighting.hlsl should resolve to common.hlsl.
|
||||
|
||||
lighting.hlsl line 22 (0-indexed): " float3 halfVec = SafeNormalize(-lightDir + viewDir);"
|
||||
SafeNormalize starts at column 21.
|
||||
"""
|
||||
definitions = language_server.request_definition("lighting.hlsl", 22, 21)
|
||||
assert len(definitions) >= 1, f"Expected at least 1 definition, got {len(definitions)}"
|
||||
def_paths = [d.get("relativePath", d.get("uri", "")) for d in definitions]
|
||||
assert any("common.hlsl" in p for p in def_paths), f"Expected definition in common.hlsl, got: {def_paths}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_goto_definition_cross_file_remap(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Navigating to Remap call in compute_test.hlsl should resolve to common.hlsl.
|
||||
|
||||
compute_test.hlsl line 20 (0-indexed): " Remap(color.r, 0.0, 1.0, 0.2, 0.8),"
|
||||
Remap starts at column 8.
|
||||
"""
|
||||
definitions = language_server.request_definition("compute_test.hlsl", 20, 8)
|
||||
assert len(definitions) >= 1, f"Expected at least 1 definition, got {len(definitions)}"
|
||||
def_paths = [d.get("relativePath", d.get("uri", "")) for d in definitions]
|
||||
assert any("common.hlsl" in p for p in def_paths), f"Expected definition in common.hlsl, got: {def_paths}"
|
||||
|
||||
|
||||
# ── References ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.hlsl
|
||||
class TestHlslReferences:
|
||||
"""Tests for find-references capability.
|
||||
|
||||
shader-language-server does not advertise referencesProvider, so
|
||||
request_references is expected to return an empty list.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_references_not_supported(self, language_server: SolidLanguageServer) -> None:
|
||||
"""References request should raise because shader-language-server does not support it.
|
||||
|
||||
common.hlsl line 17 (0-indexed): "float3 SafeNormalize(float3 v)"
|
||||
SafeNormalize starts at column 7.
|
||||
"""
|
||||
with pytest.raises(SolidLSPException, match="Method not found"):
|
||||
language_server.request_references("common.hlsl", 17, 7)
|
||||
|
||||
|
||||
# ── Hover ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _extract_hover_text(hover_info: dict[str, Any]) -> str:
|
||||
"""Extract the text content from an LSP hover response."""
|
||||
contents = hover_info["contents"]
|
||||
if isinstance(contents, dict):
|
||||
return contents.get("value", "")
|
||||
elif isinstance(contents, str):
|
||||
return contents
|
||||
return str(contents)
|
||||
|
||||
|
||||
@pytest.mark.hlsl
|
||||
class TestHlslHover:
|
||||
"""Tests for hover information."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_hover_on_function(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Hovering over SafeNormalize definition should return info.
|
||||
|
||||
common.hlsl line 17 (0-indexed): "float3 SafeNormalize(float3 v)"
|
||||
SafeNormalize starts at column 7.
|
||||
"""
|
||||
hover_info = language_server.request_hover("common.hlsl", 17, 7)
|
||||
assert hover_info is not None, "Hover should return information for SafeNormalize"
|
||||
assert "contents" in hover_info, "Hover should have contents"
|
||||
hover_text = _extract_hover_text(hover_info)
|
||||
assert len(hover_text) > 0, "Hover text should not be empty"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_hover_on_struct(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Hovering over VertexInput should return struct info.
|
||||
|
||||
common.hlsl line 3 (0-indexed): "struct VertexInput"
|
||||
VertexInput starts at column 7.
|
||||
"""
|
||||
hover_info = language_server.request_hover("common.hlsl", 3, 7)
|
||||
assert hover_info is not None, "Hover should return information for VertexInput"
|
||||
assert "contents" in hover_info, "Hover should have contents"
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Regression tests for HLSL full symbol tree indexing.
|
||||
|
||||
These tests verify that request_full_symbol_tree() correctly indexes all files,
|
||||
including .hlsl includes in subdirectories. This catches bugs where files are
|
||||
silently dropped during workspace-wide indexing.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
def _collect_file_names(symbols: list[dict[str, Any]]) -> set[str]:
|
||||
"""Recursively collect the names of all File-kind symbols in the tree."""
|
||||
names: set[str] = set()
|
||||
for sym in symbols:
|
||||
if sym.get("kind") == SymbolKind.File:
|
||||
names.add(sym["name"])
|
||||
if "children" in sym:
|
||||
names.update(_collect_file_names(sym["children"]))
|
||||
return names
|
||||
|
||||
|
||||
EXPECTED_FILES = {"common", "lighting", "compute_test", "terrain_sdf"}
|
||||
|
||||
TERRAIN_SDF_UNIQUE_SYMBOLS = {"SampleSDF", "CalculateGradient", "SDFBrickData"}
|
||||
|
||||
|
||||
@pytest.mark.hlsl
|
||||
class TestHlslFullIndex:
|
||||
"""Tests for full symbol tree indexing completeness."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_all_files_indexed_in_symbol_tree(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Every .hlsl file in the test repo must appear as a File symbol in the tree."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
file_names = _collect_file_names(symbols)
|
||||
missing = EXPECTED_FILES - file_names
|
||||
assert not missing, f"Files missing from full symbol tree: {missing}. Found: {file_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_subdirectory_file_symbols_present(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Symbols unique to terrain/terrain_sdf.hlsl must appear in the full tree."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
for name in TERRAIN_SDF_UNIQUE_SYMBOLS:
|
||||
assert SymbolUtils.symbol_tree_contains_name(
|
||||
symbols, name
|
||||
), f"Expected '{name}' from terrain/terrain_sdf.hlsl in full symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
|
||||
def test_include_file_document_symbols_directly(self, language_server: SolidLanguageServer) -> None:
|
||||
"""request_document_symbols on terrain/terrain_sdf.hlsl should return its symbols."""
|
||||
doc_symbols = language_server.request_document_symbols("terrain/terrain_sdf.hlsl")
|
||||
all_symbols = doc_symbols.get_all_symbols_and_roots()
|
||||
symbol_names = {s.get("name") for s in all_symbols[0]}
|
||||
for name in TERRAIN_SDF_UNIQUE_SYMBOLS:
|
||||
assert name in symbol_names, f"Expected '{name}' in document symbols for terrain/terrain_sdf.hlsl, got: {symbol_names}"
|
||||
Reference in new issue
Block a user