Skip to content

Commit dbd7bf7

Browse files
TheSmallKiwiclaude
andauthored
Add HLSL and related languages support through shader-language-server (#1093)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dadf3f6 commit dbd7bf7

File tree

12 files changed

+603
-3
lines changed

12 files changed

+603
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
8282
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
8383

8484
With Serena's LSP library, we provide **support for over 30 programming languages**, including
85-
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.
85+
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.
8686

8787
> [!IMPORTANT]
8888
> 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.

docs/01-about/020_programming-languages.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ Some languages require additional installations or setup steps, as noted.
5252
(requires installation of `gopls`)
5353
* **Groovy**
5454
(requires local groovy-language-server.jar setup via `GROOVY_LS_JAR_PATH` or configuration)
55-
* **Haskell**
55+
* **Haskell**
5656
(automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
57+
* **HLSL / GLSL / WGSL**
58+
(uses [shader-language-server](https://github.com/antaalt/shader-sense) (language `hlsl`); automatically downloaded;
59+
on macOS, requires Rust toolchain for building from source;
60+
note: reference search is not supported by this language server)
5761
* **Java**
5862
* **JavaScript**
5963
* **Julia**

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,12 @@ markers = [
326326
"toml: language server running for TOML",
327327
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
328328
"systemverilog: language server running for SystemVerilog (uses verible-verilog-ls)",
329+
"hlsl: language server running for HLSL shaders (uses shader-language-server)",
329330
]
330331

331332
[tool.codespell]
332333
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
333334
skip = '.git*,*.svg,*.lock,*.min.*'
334335
check-hidden = true
335-
# ignore-regex = ''
336+
ignore-regex = '\.\w+'
336337
ignore-words-list = 'paket'
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Shader language server using shader-language-server (antaalt/shader-sense).
3+
Supports HLSL, GLSL, and WGSL shader file formats.
4+
"""
5+
6+
import logging
7+
import os
8+
import pathlib
9+
import shutil
10+
from typing import Any, cast
11+
12+
from overrides import override
13+
14+
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
15+
from solidlsp.ls_config import LanguageServerConfig
16+
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
17+
from solidlsp.settings import SolidLSPSettings
18+
19+
from .common import RuntimeDependency, RuntimeDependencyCollection
20+
21+
log = logging.getLogger(__name__)
22+
23+
# GitHub release version to download when not installed locally
24+
_DEFAULT_VERSION = "1.3.0"
25+
_GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download"
26+
27+
28+
class HlslLanguageServer(SolidLanguageServer):
29+
"""
30+
Shader language server using shader-language-server.
31+
Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files.
32+
"""
33+
34+
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
35+
super().__init__(config, repository_root_path, None, "hlsl", solidlsp_settings)
36+
37+
@override
38+
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
39+
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
40+
41+
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
42+
def _get_or_install_core_dependency(self) -> str:
43+
# 1. Check PATH for system-installed binary
44+
system_binary = shutil.which("shader-language-server")
45+
if system_binary:
46+
log.info(f"Using system-installed shader-language-server at {system_binary}")
47+
return system_binary
48+
49+
# 2. Try to download pre-built binary from GitHub releases
50+
version = self._custom_settings.get("version", _DEFAULT_VERSION)
51+
tag = f"v{version}"
52+
base_url = f"{_GITHUB_RELEASE_BASE}/{tag}"
53+
54+
# macOS has no pre-built binaries; build from source via cargo install
55+
cargo_install_cmd = f"cargo install shader_language_server --version {version} --root ."
56+
57+
deps = RuntimeDependencyCollection(
58+
[
59+
RuntimeDependency(
60+
id="shader-language-server",
61+
description="shader-language-server for Windows (x64)",
62+
url=f"{base_url}/shader-language-server-x86_64-pc-windows-msvc.zip",
63+
platform_id="win-x64",
64+
archive_type="zip",
65+
binary_name="shader-language-server.exe",
66+
),
67+
RuntimeDependency(
68+
id="shader-language-server",
69+
description="shader-language-server for Linux (x64)",
70+
url=f"{base_url}/shader-language-server-x86_64-unknown-linux-gnu.zip",
71+
platform_id="linux-x64",
72+
archive_type="zip",
73+
binary_name="shader-language-server",
74+
),
75+
RuntimeDependency(
76+
id="shader-language-server",
77+
description="shader-language-server for Windows (ARM64)",
78+
url=f"{base_url}/shader-language-server-aarch64-pc-windows-msvc.zip",
79+
platform_id="win-arm64",
80+
archive_type="zip",
81+
binary_name="shader-language-server.exe",
82+
),
83+
RuntimeDependency(
84+
id="shader-language-server",
85+
description="shader-language-server for macOS (x64) - built from source",
86+
command=cargo_install_cmd,
87+
platform_id="osx-x64",
88+
binary_name="bin/shader-language-server",
89+
),
90+
RuntimeDependency(
91+
id="shader-language-server",
92+
description="shader-language-server for macOS (ARM64) - built from source",
93+
command=cargo_install_cmd,
94+
platform_id="osx-arm64",
95+
binary_name="bin/shader-language-server",
96+
),
97+
]
98+
)
99+
100+
try:
101+
dep = deps.get_single_dep_for_current_platform()
102+
except RuntimeError:
103+
dep = None
104+
105+
if dep is None:
106+
raise FileNotFoundError(
107+
"shader-language-server is not installed and no auto-install is available for your platform.\n"
108+
"Please install it using one of the following methods:\n"
109+
" cargo: cargo install shader_language_server\n"
110+
" GitHub: Download from https://github.com/antaalt/shader-sense/releases\n"
111+
"On macOS, install the Rust toolchain (https://rustup.rs) and Serena will build from source automatically.\n"
112+
"See https://github.com/antaalt/shader-sense for more details."
113+
)
114+
115+
install_dir = os.path.join(self._ls_resources_dir, "shader-language-server")
116+
executable_path = deps.binary_path(install_dir)
117+
118+
if not os.path.exists(executable_path):
119+
log.info(f"shader-language-server not found. Downloading from {dep.url}")
120+
_ = deps.install(install_dir)
121+
122+
if not os.path.exists(executable_path):
123+
raise FileNotFoundError(f"shader-language-server not found at {executable_path}")
124+
125+
os.chmod(executable_path, 0o755)
126+
return executable_path
127+
128+
def _create_launch_command(self, core_path: str) -> list[str]:
129+
return [core_path, "--stdio"]
130+
131+
@staticmethod
132+
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
133+
root_uri = pathlib.Path(repository_absolute_path).as_uri()
134+
initialize_params = {
135+
"processId": os.getpid(),
136+
"rootPath": repository_absolute_path,
137+
"rootUri": root_uri,
138+
"locale": "en",
139+
"capabilities": {
140+
"textDocument": {
141+
"synchronization": {"didSave": True, "dynamicRegistration": True},
142+
"completion": {
143+
"dynamicRegistration": True,
144+
"completionItem": {"snippetSupport": True},
145+
},
146+
"definition": {"dynamicRegistration": True},
147+
"hover": {
148+
"dynamicRegistration": True,
149+
"contentFormat": ["markdown", "plaintext"],
150+
},
151+
"signatureHelp": {
152+
"dynamicRegistration": True,
153+
"signatureInformation": {
154+
"parameterInformation": {"labelOffsetSupport": True},
155+
},
156+
},
157+
"documentSymbol": {
158+
"dynamicRegistration": True,
159+
"hierarchicalDocumentSymbolSupport": True,
160+
"symbolKind": {"valueSet": list(range(1, 27))},
161+
},
162+
"formatting": {"dynamicRegistration": True},
163+
"publishDiagnostics": {"relatedInformation": True},
164+
},
165+
"workspace": {
166+
"workspaceFolders": True,
167+
"didChangeConfiguration": {"dynamicRegistration": True},
168+
"configuration": True,
169+
},
170+
},
171+
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
172+
}
173+
return cast(InitializeParams, initialize_params)
174+
175+
@override
176+
def _start_server(self) -> None:
177+
def do_nothing(params: Any) -> None:
178+
return
179+
180+
def on_log_message(params: Any) -> None:
181+
message = params.get("message", "") if isinstance(params, dict) else str(params)
182+
log.info(f"shader-language-server: {message}")
183+
184+
def on_configuration_request(params: Any) -> list[dict]:
185+
"""Respond to workspace/configuration requests.
186+
187+
shader-language-server requests config with section 'shader-validator'.
188+
Return empty config to use defaults.
189+
"""
190+
items = params.get("items", []) if isinstance(params, dict) else []
191+
return [{}] * len(items)
192+
193+
self.server.on_request("client/registerCapability", do_nothing)
194+
self.server.on_request("workspace/configuration", on_configuration_request)
195+
self.server.on_notification("$/progress", do_nothing)
196+
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
197+
self.server.on_notification("window/logMessage", on_log_message)
198+
199+
log.info("Starting shader-language-server process")
200+
self.server.start()
201+
initialize_params = self._get_initialize_params(self.repository_root_path)
202+
203+
log.info("Sending initialize request")
204+
init_response = self.server.send.initialize(initialize_params)
205+
206+
capabilities = init_response.get("capabilities", {})
207+
log.info(f"Initialize response capabilities: {list(capabilities.keys())}")
208+
assert "textDocumentSync" in capabilities, "shader-language-server must support textDocumentSync"
209+
if "documentSymbolProvider" not in capabilities:
210+
log.warning("shader-language-server does not advertise documentSymbolProvider")
211+
if "definitionProvider" not in capabilities:
212+
log.warning("shader-language-server does not advertise definitionProvider")
213+
214+
self.server.notify.initialized({})
215+
216+
@override
217+
def is_ignored_dirname(self, dirname: str) -> bool:
218+
"""Ignore Unity-specific directories that contain no user-authored shaders."""
219+
return super().is_ignored_dirname(dirname) or dirname in {"Library", "Temp", "Logs", "obj", "Packages"}

src/solidlsp/ls_config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ class Language(str, Enum):
105105
"""TOML language server using Taplo.
106106
Supports TOML validation, formatting, and schema support.
107107
"""
108+
HLSL = "hlsl"
109+
"""Shader language server using shader-language-server (antaalt/shader-sense).
110+
Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files.
111+
Automatically downloads shader-language-server binary.
112+
"""
108113
SYSTEMVERILOG = "systemverilog"
109114
"""SystemVerilog language server using verible-verilog-ls.
110115
Supports .sv, .svh, .v, .vh files.
@@ -251,6 +256,24 @@ def get_source_fn_matcher(self) -> FilenameMatcher:
251256
return FilenameMatcher("*.groovy", "*.gvy")
252257
case self.MATLAB:
253258
return FilenameMatcher("*.m", "*.mlx", "*.mlapp")
259+
case self.HLSL:
260+
return FilenameMatcher(
261+
"*.hlsl",
262+
"*.hlsli",
263+
"*.fx",
264+
"*.fxh",
265+
"*.cginc",
266+
"*.compute",
267+
"*.shader",
268+
"*.glsl",
269+
"*.vert",
270+
"*.frag",
271+
"*.geom",
272+
"*.tesc",
273+
"*.tese",
274+
"*.comp",
275+
"*.wgsl",
276+
)
254277
case self.SYSTEMVERILOG:
255278
return FilenameMatcher("*.sv", "*.svh", "*.v", "*.vh")
256279
case _:
@@ -434,6 +457,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]:
434457
from solidlsp.language_servers.matlab_language_server import MatlabLanguageServer
435458

436459
return MatlabLanguageServer
460+
case self.HLSL:
461+
from solidlsp.language_servers.hlsl_language_server import HlslLanguageServer
462+
463+
return HlslLanguageServer
437464
case self.SYSTEMVERILOG:
438465
from solidlsp.language_servers.systemverilog_server import SystemVerilogLanguageServer
439466

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#ifndef COMMON_HLSL
2+
#define COMMON_HLSL
3+
4+
struct VertexInput
5+
{
6+
float3 position : POSITION;
7+
float3 normal : NORMAL;
8+
float2 uv : TEXCOORD0;
9+
};
10+
11+
struct VertexOutput
12+
{
13+
float4 clipPos : SV_POSITION;
14+
float3 worldNormal : TEXCOORD0;
15+
float2 uv : TEXCOORD1;
16+
};
17+
18+
float3 SafeNormalize(float3 v)
19+
{
20+
float len = length(v);
21+
return len > 0.0001 ? v / len : float3(0, 0, 0);
22+
}
23+
24+
float Remap(float value, float fromMin, float fromMax, float toMin, float toMax)
25+
{
26+
return toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin);
27+
}
28+
29+
#endif // COMMON_HLSL
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include "common.hlsl"
2+
3+
RWTexture2D<float4> OutputTexture : register(u0);
4+
Texture2D<float4> InputTexture : register(t0);
5+
6+
cbuffer ComputeParams : register(b0)
7+
{
8+
uint2 TextureSize;
9+
float BlurRadius;
10+
float _Pad;
11+
};
12+
13+
[numthreads(8, 8, 1)]
14+
void CSMain(uint3 id : SV_DispatchThreadID)
15+
{
16+
if (id.x >= TextureSize.x || id.y >= TextureSize.y)
17+
return;
18+
19+
float4 color = InputTexture[id.xy];
20+
float3 remapped = float3(
21+
Remap(color.r, 0.0, 1.0, 0.2, 0.8),
22+
Remap(color.g, 0.0, 1.0, 0.2, 0.8),
23+
Remap(color.b, 0.0, 1.0, 0.2, 0.8)
24+
);
25+
OutputTexture[id.xy] = float4(remapped, color.a);
26+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#ifndef LIGHTING_HLSL
2+
#define LIGHTING_HLSL
3+
4+
#include "common.hlsl"
5+
6+
cbuffer LightingConstants : register(b0)
7+
{
8+
float4x4 ViewProjection;
9+
float3 LightDirection;
10+
float LightIntensity;
11+
float3 AmbientColor;
12+
float _Padding;
13+
};
14+
15+
float3 CalculateDiffuse(float3 normal, float3 lightDir, float3 albedo)
16+
{
17+
float ndotl = max(dot(normal, -lightDir), 0.0);
18+
return albedo * ndotl;
19+
}
20+
21+
float3 CalculateSpecular(float3 normal, float3 lightDir, float3 viewDir, float shininess)
22+
{
23+
float3 halfVec = SafeNormalize(-lightDir + viewDir);
24+
float ndoth = max(dot(normal, halfVec), 0.0);
25+
return pow(ndoth, shininess);
26+
}
27+
28+
VertexOutput TransformVertex(VertexInput input)
29+
{
30+
VertexOutput output;
31+
output.clipPos = mul(ViewProjection, float4(input.position, 1.0));
32+
output.worldNormal = input.normal;
33+
output.uv = input.uv;
34+
return output;
35+
}
36+
37+
#endif // LIGHTING_HLSL

0 commit comments

Comments
 (0)