Feat: Add Support for Zig, Nix, Lua (#498)

Extensive tests for each of the three languages were added
This commit is contained in:
Dominic Rubas authored and GitHub committed 2025-08-22 23:34:46 +02:00
1 parent 47ccc916a4
commit 7e6dbdc9ba
30 files changed
+3094 -4

No files matched your search

+89
View File
@@ -111,6 +111,95 @@ jobs:
- name: Install Ruby language server
shell: bash
run: gem install solargraph
- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.14.1
- name: Install ZLS (Zig Language Server)
shell: bash
run: |
if [[ "${{ runner.os }}" == "Linux" ]]; then
wget https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-linux.tar.xz
tar -xf zls-x86_64-linux.tar.xz
sudo mv zls /usr/local/bin/
rm zls-x86_64-linux.tar.xz
elif [[ "${{ runner.os }}" == "macOS" ]]; then
wget https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-macos.tar.xz
tar -xf zls-x86_64-macos.tar.xz
sudo mv zls /usr/local/bin/
rm zls-x86_64-macos.tar.xz
elif [[ "${{ runner.os }}" == "Windows" ]]; then
curl -L -o zls.zip https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-windows.zip
unzip -o zls.zip
mkdir -p "$HOME/bin"
mv zls.exe "$HOME/bin/"
echo "$HOME/bin" >> $GITHUB_PATH
rm zls.zip
fi
- name: Install Lua Language Server
shell: bash
run: |
LUA_LS_VERSION="3.15.0"
LUA_LS_DIR="$HOME/.serena/language_servers/lua"
mkdir -p "$LUA_LS_DIR"
if [[ "${{ runner.os }}" == "Linux" ]]; then
if [[ "$(uname -m)" == "x86_64" ]]; then
wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-linux-x64.tar.gz
tar -xzf lua-language-server-${LUA_LS_VERSION}-linux-x64.tar.gz -C "$LUA_LS_DIR"
else
wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-linux-arm64.tar.gz
tar -xzf lua-language-server-${LUA_LS_VERSION}-linux-arm64.tar.gz -C "$LUA_LS_DIR"
fi
chmod +x "$LUA_LS_DIR/bin/lua-language-server"
# Create wrapper script instead of symlink to ensure supporting files are found
echo '#!/bin/bash' | sudo tee /usr/local/bin/lua-language-server > /dev/null
echo 'cd "${HOME}/.serena/language_servers/lua/bin"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
echo 'exec ./lua-language-server "$@"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
sudo chmod +x /usr/local/bin/lua-language-server
rm lua-language-server-*.tar.gz
elif [[ "${{ runner.os }}" == "macOS" ]]; then
if [[ "$(uname -m)" == "x86_64" ]]; then
wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-darwin-x64.tar.gz
tar -xzf lua-language-server-${LUA_LS_VERSION}-darwin-x64.tar.gz -C "$LUA_LS_DIR"
else
wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-darwin-arm64.tar.gz
tar -xzf lua-language-server-${LUA_LS_VERSION}-darwin-arm64.tar.gz -C "$LUA_LS_DIR"
fi
chmod +x "$LUA_LS_DIR/bin/lua-language-server"
# Create wrapper script instead of symlink to ensure supporting files are found
echo '#!/bin/bash' | sudo tee /usr/local/bin/lua-language-server > /dev/null
echo 'cd "${HOME}/.serena/language_servers/lua/bin"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
echo 'exec ./lua-language-server "$@"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
sudo chmod +x /usr/local/bin/lua-language-server
rm lua-language-server-*.tar.gz
elif [[ "${{ runner.os }}" == "Windows" ]]; then
curl -L -o lua-ls.zip https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-win32-x64.zip
unzip -o lua-ls.zip -d "$LUA_LS_DIR"
# For Windows, we'll add the bin directory directly to PATH
# The lua-language-server.exe can find its supporting files relative to its location
echo "$LUA_LS_DIR/bin" >> $GITHUB_PATH
rm lua-ls.zip
fi
- name: Install Nix
if: runner.os != 'Windows' # Nix doesn't support Windows natively
uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Install nixd (Nix Language Server)
if: runner.os != 'Windows' # Skip on Windows since Nix isn't available
shell: bash
run: |
# Install nixd using nix
nix profile install github:nix-community/nixd
# Verify nixd is installed and working
if ! command -v nixd &> /dev/null; then
echo "nixd installation failed or not in PATH"
exit 1
fi
echo "$HOME/.nix-profile/bin" >> $GITHUB_PATH
- name: Install uv
shell: bash
run: curl -LsSf https://astral.sh/uv/install.sh | sh
+3
View File
@@ -228,6 +228,9 @@ tmp/
# Exception: Don't ignore Elixir test repository lib directory (contains source code)
!/test/resources/repos/elixir/test_repo/lib
# Exception: Don't ignore Nix test repository lib directory (contains source code)
!/test/resources/repos/nix/test_repo/lib
# Swift
/test/resources/repos/swift/test_repo/.build
/test/resources/repos/swift/test_repo/.swiftpm
+6 -4
View File
@@ -1,10 +1,12 @@
# latest
Status of the `main` branch. Changes prior to the next official version change will appear here.
## New Features
* **Dart language support**: Added full support for Dart programming language with comprehensive test coverage. Dart projects are now fully supported with symbol navigation, definition finding, and reference detection capabilities.
* New languages supported:
* **Add support for Zig** via ZLS (Zig Language Server)
* **Add support for Lua** via lua-language-server (automatically downloaded if not installed)
* **Add support for Nix** via nixd (Nix Language server) - requires nixd installation (Windows not supported)
* **Dart language support**: Dart was always working, but now tests were added and it is promoted to "officially supported"
## Breaking Changes
* **Rust support now requires 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. If you don't have rustup installed, you'll need to install it from https://rustup.rs/
+3
View File
@@ -111,6 +111,9 @@ With Serena, we provide
* Clojure
* Bash
* C/C++ (You may experience issues with finding references, we are working on it)
* Zig (requires installation of ZLS - Zig Language Server)
* Lua (automatically downloads lua-language-server if not installed)
* Nix (requires nixd installation)
* Dart
* indirect support (may require some code changes/manual installation) for:
* Kotlin (untested)
+3
View File
@@ -264,6 +264,9 @@ markers = [
"bash: language server running for Bash",
"snapshot: snapshot tests for symbolic editing operations",
"ruby: language server running for Ruby",
"zig: language server running for Zig",
"lua: language server running for Lua",
"nix: language server running for Nix",
"dart: language server running for Dart",
]
+297
View File
@@ -0,0 +1,297 @@
"""
Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
"""
import logging
import os
import pathlib
import platform
import shutil
import tarfile
import threading
import zipfile
from pathlib import Path
import requests
from overrides import override
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 LuaLanguageServer(SolidLanguageServer):
"""
Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For Lua projects, we should ignore:
# - .luarocks: package manager cache
# - lua_modules: local dependencies
# - node_modules: if the project has JavaScript components
return super().is_ignored_dirname(dirname) or dirname in [".luarocks", "lua_modules", "node_modules", "build", "dist", ".cache"]
@staticmethod
def _get_lua_ls_path():
"""Get the path to lua-language-server executable."""
# First check if it's in PATH
lua_ls = shutil.which("lua-language-server")
if lua_ls:
return lua_ls
# Check common installation locations
home = Path.home()
possible_paths = [
home / ".local" / "bin" / "lua-language-server",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server",
Path("/usr/local/bin/lua-language-server"),
Path("/opt/lua-language-server/bin/lua-language-server"),
]
# Add Windows-specific paths
if platform.system() == "Windows":
possible_paths.extend(
[
home / "AppData" / "Local" / "lua-language-server" / "bin" / "lua-language-server.exe",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server.exe",
]
)
for path in possible_paths:
if path.exists():
return str(path)
return None
@staticmethod
def _download_lua_ls():
"""Download and install lua-language-server if not present."""
system = platform.system()
machine = platform.machine().lower()
lua_ls_version = "3.15.0"
# Map platform and architecture to download URL
if system == "Linux":
if machine in ["x86_64", "amd64"]:
download_name = f"lua-language-server-{lua_ls_version}-linux-x64.tar.gz"
elif machine in ["aarch64", "arm64"]:
download_name = f"lua-language-server-{lua_ls_version}-linux-arm64.tar.gz"
else:
raise RuntimeError(f"Unsupported Linux architecture: {machine}")
elif system == "Darwin":
if machine in ["x86_64", "amd64"]:
download_name = f"lua-language-server-{lua_ls_version}-darwin-x64.tar.gz"
elif machine in ["arm64", "aarch64"]:
download_name = f"lua-language-server-{lua_ls_version}-darwin-arm64.tar.gz"
else:
raise RuntimeError(f"Unsupported macOS architecture: {machine}")
elif system == "Windows":
if machine in ["amd64", "x86_64"]:
download_name = f"lua-language-server-{lua_ls_version}-win32-x64.zip"
else:
raise RuntimeError(f"Unsupported Windows architecture: {machine}")
else:
raise RuntimeError(f"Unsupported operating system: {system}")
download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}"
# Create installation directory
install_dir = Path.home() / ".serena" / "language_servers" / "lua"
install_dir.mkdir(parents=True, exist_ok=True)
# Download the file
print(f"Downloading lua-language-server from {download_url}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
# Save and extract
download_path = install_dir / download_name
with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Extracting lua-language-server to {install_dir}...")
if download_name.endswith(".tar.gz"):
with tarfile.open(download_path, "r:gz") as tar:
tar.extractall(install_dir)
elif download_name.endswith(".zip"):
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# Clean up download file
download_path.unlink()
# Make executable on Unix systems
if system != "Windows":
lua_ls_path = install_dir / "bin" / "lua-language-server"
if lua_ls_path.exists():
lua_ls_path.chmod(0o755)
return str(lua_ls_path)
else:
lua_ls_path = install_dir / "bin" / "lua-language-server.exe"
if lua_ls_path.exists():
return str(lua_ls_path)
raise RuntimeError("Failed to find lua-language-server executable after extraction")
@staticmethod
def _setup_runtime_dependency():
"""
Check if required Lua runtime dependencies are available.
Downloads lua-language-server if not present.
"""
lua_ls_path = LuaLanguageServer._get_lua_ls_path()
if not lua_ls_path:
print("lua-language-server not found. Downloading...")
lua_ls_path = LuaLanguageServer._download_lua_ls()
print(f"lua-language-server installed at: {lua_ls_path}")
return lua_ls_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
lua_ls_path = self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path),
"lua",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Lua Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# Lua Language Server specific options
"runtime": {
"version": "Lua 5.4",
"path": ["?.lua", "?/init.lua"],
},
"diagnostics": {
"enable": True,
"globals": ["vim", "describe", "it", "before_each", "after_each"], # Common globals
},
"workspace": {
"library": [], # Can be extended with project-specific libraries
"checkThirdParty": False,
"userThirdParty": [],
},
"telemetry": {
"enable": False,
},
"completion": {
"enable": True,
"callSnippet": "Both",
"keywordSnippet": "Both",
},
},
}
return initialize_params
def _start_server(self):
"""Start Lua Language Server process"""
def register_capability_handler(params):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
def do_nothing(params):
return
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 Lua Language 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)
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]
self.server.notify.initialized({})
self.completions_available.set()
# Lua Language Server is typically ready immediately after initialization
self.server_ready.set()
self.server_ready.wait()
+322
View File
@@ -0,0 +1,322 @@
"""
Provides Nix specific instantiation of the LanguageServer class using nixd (Nix Language Server).
Note: Windows is not supported as Nix itself doesn't support Windows natively.
"""
import logging
import os
import pathlib
import platform
import shutil
import subprocess
import threading
from pathlib import Path
from overrides import override
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 NixLanguageServer(SolidLanguageServer):
"""
Provides Nix specific instantiation of the LanguageServer class using nixd.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For Nix projects, we should ignore:
# - result: nix build output symlinks
# - result-*: multiple build outputs
# - .direnv: direnv cache
return super().is_ignored_dirname(dirname) or dirname in ["result", ".direnv"] or dirname.startswith("result-")
@staticmethod
def _get_nixd_version():
"""Get the installed nixd version or None if not found."""
try:
result = subprocess.run(["nixd", "--version"], capture_output=True, text=True, check=False)
if result.returncode == 0:
# nixd outputs version like: nixd 2.0.0
return result.stdout.strip()
except FileNotFoundError:
return None
return None
@staticmethod
def _check_nixd_installed():
"""Check if nixd is installed in the system."""
return shutil.which("nixd") is not None
@staticmethod
def _get_nixd_path():
"""Get the path to nixd executable."""
# First check if it's in PATH
nixd_path = shutil.which("nixd")
if nixd_path:
return nixd_path
# Check common installation locations
home = Path.home()
possible_paths = [
home / ".local" / "bin" / "nixd",
home / ".serena" / "language_servers" / "nixd" / "nixd",
home / ".nix-profile" / "bin" / "nixd",
Path("/usr/local/bin/nixd"),
Path("/run/current-system/sw/bin/nixd"), # NixOS system profile
Path("/opt/homebrew/bin/nixd"), # Homebrew on Apple Silicon
Path("/usr/local/opt/nixd/bin/nixd"), # Homebrew on Intel Mac
]
# Add Windows-specific paths
if platform.system() == "Windows":
possible_paths.extend(
[
home / "AppData" / "Local" / "nixd" / "nixd.exe",
home / ".serena" / "language_servers" / "nixd" / "nixd.exe",
]
)
for path in possible_paths:
if path.exists():
return str(path)
return None
@staticmethod
def _install_nixd_with_nix():
"""Install nixd using nix if available."""
# Check if nix is available
if not shutil.which("nix"):
return None
print("Installing nixd using nix... This may take a few minutes.")
try:
# Try to install nixd using nix profile
result = subprocess.run(
["nix", "profile", "install", "github:nix-community/nixd"],
capture_output=True,
text=True,
check=False,
timeout=600, # 10 minute timeout for building
)
if result.returncode == 0:
# Check if nixd is now in PATH
nixd_path = shutil.which("nixd")
if nixd_path:
print(f"Successfully installed nixd at: {nixd_path}")
return nixd_path
else:
# Try nix-env as fallback
result = subprocess.run(
["nix-env", "-iA", "nixpkgs.nixd"],
capture_output=True,
text=True,
check=False,
timeout=600,
)
if result.returncode == 0:
nixd_path = shutil.which("nixd")
if nixd_path:
print(f"Successfully installed nixd at: {nixd_path}")
return nixd_path
print(f"Failed to install nixd: {result.stderr}")
except subprocess.TimeoutExpired:
print("Nix install timed out after 10 minutes")
except Exception as e:
print(f"Error installing nixd with nix: {e}")
return None
@staticmethod
def _setup_runtime_dependency():
"""
Check if required Nix runtime dependencies are available.
Attempts to install nixd if not present.
"""
# First check if Nix is available (nixd needs it at runtime)
if not shutil.which("nix"):
print("WARNING: Nix is not installed. nixd requires Nix to function properly.")
raise RuntimeError("Nix is required for nixd. Please install Nix from https://nixos.org/download.html")
nixd_path = NixLanguageServer._get_nixd_path()
if not nixd_path:
print("nixd not found. Attempting to install...")
# Try to install with nix if available
nixd_path = NixLanguageServer._install_nixd_with_nix()
if not nixd_path:
raise RuntimeError(
"nixd (Nix Language Server) is not installed.\n"
"Please install nixd using one of the following methods:\n"
" - Using Nix flakes: nix profile install github:nix-community/nixd\n"
" - From nixpkgs: nix-env -iA nixpkgs.nixd\n"
" - On macOS with Homebrew: brew install nixd\n\n"
"After installation, make sure 'nixd' is in your PATH."
)
# Verify nixd works
try:
result = subprocess.run([nixd_path, "--version"], capture_output=True, text=True, check=False, timeout=5)
if result.returncode != 0:
raise RuntimeError(f"nixd failed to run: {result.stderr}")
except Exception as e:
raise RuntimeError(f"Failed to verify nixd installation: {e}")
return nixd_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
nixd_path = self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path),
"nix",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for nixd.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
"codeAction": {
"dynamicRegistration": True,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"",
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# nixd specific options
"nixpkgs": {"expr": "import <nixpkgs> { }"},
"formatting": {"command": ["nixpkgs-fmt"]}, # or ["alejandra"] or ["nixfmt"]
"options": {
"enable": True,
"target": {
"installable": "", # Will be auto-detected from flake.nix if present
},
},
},
}
return initialize_params
def _start_server(self):
"""Start nixd server process"""
def register_capability_handler(params):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
def do_nothing(params):
return
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 nixd 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)
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]
self.server.notify.initialized({})
self.completions_available.set()
# nixd server is typically ready immediately after initialization
self.server_ready.set()
self.server_ready.wait()
+252
View File
@@ -0,0 +1,252 @@
"""
Provides Zig specific instantiation of the LanguageServer class using ZLS (Zig Language Server).
"""
import logging
import os
import pathlib
import platform
import shutil
import subprocess
import threading
from overrides import override
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 ZigLanguageServer(SolidLanguageServer):
"""
Provides Zig specific instantiation of the LanguageServer class using ZLS.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For Zig projects, we should ignore:
# - zig-cache: build cache directory
# - zig-out: default build output directory
# - .zig-cache: alternative cache location
# - node_modules: if the project has JavaScript components
return super().is_ignored_dirname(dirname) or dirname in ["zig-cache", "zig-out", ".zig-cache", "node_modules", "build", "dist"]
@staticmethod
def _get_zig_version():
"""Get the installed Zig version or None if not found."""
try:
result = subprocess.run(["zig", "version"], capture_output=True, text=True, check=False)
if result.returncode == 0:
return result.stdout.strip()
except FileNotFoundError:
return None
return None
@staticmethod
def _get_zls_version():
"""Get the installed ZLS version or None if not found."""
try:
result = subprocess.run(["zls", "--version"], capture_output=True, text=True, check=False)
if result.returncode == 0:
return result.stdout.strip()
except FileNotFoundError:
return None
return None
@staticmethod
def _check_zls_installed():
"""Check if ZLS is installed in the system."""
return shutil.which("zls") is not None
@staticmethod
def _setup_runtime_dependency():
"""
Check if required Zig runtime dependencies are available.
Raises RuntimeError with helpful message if dependencies are missing.
"""
# Check for Windows and provide error message
if platform.system() == "Windows":
raise RuntimeError(
"Windows is not supported by ZLS in this integration. "
"Cross-file references don't work reliably on Windows. Reason unknown."
)
zig_version = ZigLanguageServer._get_zig_version()
if not zig_version:
raise RuntimeError(
"Zig is not installed. Please install Zig from https://ziglang.org/download/ and make sure it is added to your PATH."
)
if not ZigLanguageServer._check_zls_installed():
zls_version = ZigLanguageServer._get_zls_version()
if not zls_version:
raise RuntimeError(
"Found Zig but ZLS (Zig Language Server) is not installed.\n"
"Please install ZLS from https://github.com/zigtools/zls\n"
"You can install it via:\n"
" - Package managers (brew install zls, scoop install zls, etc.)\n"
" - Download pre-built binaries from GitHub releases\n"
" - Build from source with: zig build -Doptimize=ReleaseSafe\n\n"
"After installation, make sure 'zls' is added to your PATH."
)
return True
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd="zls", cwd=repository_root_path),
"zig",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Zig Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# ZLS specific options based on schema.json
# Critical paths for ZLS to understand the project
"zig_exe_path": shutil.which("zig"), # Path to zig executable
"zig_lib_path": None, # Let ZLS auto-detect
"build_runner_path": None, # Let ZLS use its built-in runner
"global_cache_path": None, # Let ZLS use default cache
# Build configuration
"enable_build_on_save": True, # Enable to analyze project structure
"build_on_save_args": ["build"],
# Features
"enable_snippets": True,
"enable_argument_placeholders": True,
"semantic_tokens": "full",
"warn_style": False,
"highlight_global_var_declarations": False,
"skip_std_references": False,
"prefer_ast_check_as_child_process": True,
"completion_label_details": True,
# Inlay hints configuration
"inlay_hints_show_variable_type_hints": True,
"inlay_hints_show_struct_literal_field_type": True,
"inlay_hints_show_parameter_name": True,
"inlay_hints_show_builtin": True,
"inlay_hints_exclude_single_argument": True,
"inlay_hints_hide_redundant_param_names": False,
"inlay_hints_hide_redundant_param_names_last_token": False,
},
}
return initialize_params
def _start_server(self):
"""Start ZLS server process"""
def register_capability_handler(params):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
def do_nothing(params):
return
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 ZLS 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)
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]
self.server.notify.initialized({})
self.completions_available.set()
# ZLS server is ready after initialization
self.server_ready.set()
self.server_ready.wait()
# Open build.zig if it exists to help ZLS understand project structure
build_zig_path = os.path.join(self.repository_root_path, "build.zig")
if os.path.exists(build_zig_path):
try:
with open(build_zig_path, encoding="utf-8") as f:
content = f.read()
uri = pathlib.Path(build_zig_path).as_uri()
self.server.notify.did_open_text_document(
{
"textDocument": {
"uri": uri,
"languageId": "zig",
"version": 1,
"text": content,
}
}
)
self.logger.log("Opened build.zig to provide project context to ZLS", logging.INFO)
except Exception as e:
self.logger.log(f"Failed to open build.zig: {e}", logging.WARNING)
+15
View File
@@ -238,6 +238,21 @@ class SolidLanguageServer(ABC):
ls = BashLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.ZIG:
from solidlsp.language_servers.zls import ZigLanguageServer
ls = ZigLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.NIX:
from solidlsp.language_servers.nixd_ls import NixLanguageServer
ls = NixLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.LUA:
from solidlsp.language_servers.lua_ls import LuaLanguageServer
ls = LuaLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise SolidLSPException(f"Language {config.code_language} is not supported")
+9
View File
@@ -44,6 +44,9 @@ class Language(str, Enum):
TERRAFORM = "terraform"
SWIFT = "swift"
BASH = "bash"
ZIG = "zig"
LUA = "lua"
NIX = "nix"
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -109,6 +112,12 @@ class Language(str, Enum):
return FilenameMatcher("*.swift")
case self.BASH:
return FilenameMatcher("*.sh", "*.bash")
case self.ZIG:
return FilenameMatcher("*.zig", "*.zon")
case self.LUA:
return FilenameMatcher("*.lua")
case self.NIX:
return FilenameMatcher("*.nix")
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -0,0 +1,13 @@
# Lua specific
*.luac
.luarocks/
lua_modules/
luarocks/
# Build artifacts
build/
dist/
# IDE
.vscode/
.idea/
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env lua
-- main.lua: Entry point for the test application
local calculator = require("src.calculator")
local utils = require("src.utils")
local function print_banner()
print("=" .. string.rep("=", 40))
print(" Lua Test Repository")
print("=" .. string.rep("=", 40))
end
local function test_calculator()
print("\nTesting Calculator Module:")
print("5 + 3 =", calculator.add(5, 3))
print("10 - 4 =", calculator.subtract(10, 4))
print("6 * 7 =", calculator.multiply(6, 7))
print("15 / 3 =", calculator.divide(15, 3))
print("2^8 =", calculator.power(2, 8))
print("5! =", calculator.factorial(5))
local numbers = {5, 2, 8, 3, 9, 1, 7}
print("Mean of", table.concat(numbers, ", "), "=", calculator.mean(numbers))
print("Median of", table.concat(numbers, ", "), "=", calculator.median(numbers))
end
local function test_utils()
print("\nTesting Utils Module:")
-- String utilities
print("Trimmed ' hello ' =", "'" .. utils.trim(" hello ") .. "'")
local parts = utils.split("apple,banana,orange", ",")
print("Split 'apple,banana,orange' by ',' =", table.concat(parts, " | "))
print("'hello' starts with 'he' =", utils.starts_with("hello", "he"))
print("'world' ends with 'ld' =", utils.ends_with("world", "ld"))
-- Table utilities
local t1 = {a = 1, b = 2}
local t2 = {b = 3, c = 4}
local merged = utils.table_merge(t1, t2)
print("Merged tables: a=" .. (merged.a or "nil") ..
", b=" .. (merged.b or "nil") ..
", c=" .. (merged.c or "nil"))
-- Logger
local logger = utils.Logger:new("TestApp")
logger:info("Application started")
logger:debug("Debug information")
logger:warn("This is a warning")
end
local function interactive_calculator()
print("\nInteractive Calculator (type 'quit' to exit):")
while true do
io.write("Enter operation (e.g., '5 + 3'): ")
local input = io.read()
if input == "quit" then
break
end
-- Simple parser for basic operations
local a, op, b = input:match("(%d+)%s*([%+%-%*/])%s*(%d+)")
if a and op and b then
a = tonumber(a)
b = tonumber(b)
local result
if op == "+" then
result = calculator.add(a, b)
elseif op == "-" then
result = calculator.subtract(a, b)
elseif op == "*" then
result = calculator.multiply(a, b)
elseif op == "/" then
local success, res = pcall(calculator.divide, a, b)
if success then
result = res
else
print("Error: " .. res)
goto continue
end
end
print("Result: " .. result)
else
print("Invalid input. Please use format: number operator number")
end
::continue::
end
end
-- Main execution
local function main(args)
print_banner()
if #args == 0 then
test_calculator()
test_utils()
elseif args[1] == "interactive" then
interactive_calculator()
elseif args[1] == "test" then
test_calculator()
test_utils()
print("\nAll tests completed!")
else
print("Usage: lua main.lua [interactive|test]")
end
end
-- Run main function
main(arg or {})
@@ -0,0 +1,76 @@
-- calculator.lua: A simple calculator module for testing LSP features
local calculator = {}
-- Basic arithmetic operations
function calculator.add(a, b)
return a + b
end
function calculator.subtract(a, b)
return a - b
end
function calculator.multiply(a, b)
return a * b
end
function calculator.divide(a, b)
if b == 0 then
error("Division by zero")
end
return a / b
end
-- Advanced operations
function calculator.power(base, exponent)
return base ^ exponent
end
function calculator.factorial(n)
if n < 0 then
error("Factorial is not defined for negative numbers")
elseif n == 0 or n == 1 then
return 1
else
local result = 1
for i = 2, n do
result = result * i
end
return result
end
end
-- Statistics functions
function calculator.mean(numbers)
if #numbers == 0 then
return nil
end
local sum = 0
for _, num in ipairs(numbers) do
sum = sum + num
end
return sum / #numbers
end
function calculator.median(numbers)
if #numbers == 0 then
return nil
end
local sorted = {}
for i, v in ipairs(numbers) do
sorted[i] = v
end
table.sort(sorted)
local mid = math.floor(#sorted / 2)
if #sorted % 2 == 0 then
return (sorted[mid] + sorted[mid + 1]) / 2
else
return sorted[mid + 1]
end
end
return calculator
@@ -0,0 +1,120 @@
-- utils.lua: Utility functions for the test repository
local utils = {}
-- String utilities
function utils.trim(s)
return s:match("^%s*(.-)%s*$")
end
function utils.split(str, delimiter)
local result = {}
local pattern = string.format("([^%s]+)", delimiter)
for match in string.gmatch(str, pattern) do
table.insert(result, match)
end
return result
end
function utils.starts_with(str, prefix)
return str:sub(1, #prefix) == prefix
end
function utils.ends_with(str, suffix)
return str:sub(-#suffix) == suffix
end
-- Table utilities
function utils.deep_copy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[utils.deep_copy(orig_key)] = utils.deep_copy(orig_value)
end
setmetatable(copy, utils.deep_copy(getmetatable(orig)))
else
copy = orig
end
return copy
end
function utils.table_contains(tbl, value)
for _, v in ipairs(tbl) do
if v == value then
return true
end
end
return false
end
function utils.table_merge(t1, t2)
local result = {}
for k, v in pairs(t1) do
result[k] = v
end
for k, v in pairs(t2) do
result[k] = v
end
return result
end
-- File utilities
function utils.read_file(path)
local file = io.open(path, "r")
if not file then
return nil, "Could not open file: " .. path
end
local content = file:read("*all")
file:close()
return content
end
function utils.write_file(path, content)
local file = io.open(path, "w")
if not file then
return false, "Could not open file for writing: " .. path
end
file:write(content)
file:close()
return true
end
-- Class-like structure
utils.Logger = {}
utils.Logger.__index = utils.Logger
function utils.Logger:new(name)
local self = setmetatable({}, utils.Logger)
self.name = name or "default"
self.level = "info"
return self
end
function utils.Logger:set_level(level)
self.level = level
end
function utils.Logger:log(message, level)
level = level or self.level
print(string.format("[%s] %s: %s", self.name, level:upper(), message))
end
function utils.Logger:debug(message)
self:log(message, "debug")
end
function utils.Logger:info(message)
self:log(message, "info")
end
function utils.Logger:warn(message)
self:log(message, "warn")
end
function utils.Logger:error(message)
self:log(message, "error")
end
return utils
@@ -0,0 +1,93 @@
-- test_calculator.lua: Unit tests for calculator module
local calculator = require("src.calculator")
local function assert_equals(actual, expected, message)
if actual ~= expected then
error(string.format("%s: expected %s, got %s",
message or "Assertion failed", tostring(expected), tostring(actual)))
end
end
local function assert_error(func, message)
local success = pcall(func)
if success then
error(string.format("%s: expected error but none was thrown",
message or "Assertion failed"))
end
end
local function test_basic_operations()
print("Testing basic operations...")
assert_equals(calculator.add(2, 3), 5, "Addition test")
assert_equals(calculator.add(-5, 5), 0, "Addition with negative")
assert_equals(calculator.add(0, 0), 0, "Addition with zeros")
assert_equals(calculator.subtract(10, 3), 7, "Subtraction test")
assert_equals(calculator.subtract(5, 10), -5, "Subtraction negative result")
assert_equals(calculator.multiply(4, 5), 20, "Multiplication test")
assert_equals(calculator.multiply(-3, 4), -12, "Multiplication with negative")
assert_equals(calculator.multiply(0, 100), 0, "Multiplication with zero")
assert_equals(calculator.divide(10, 2), 5, "Division test")
assert_equals(calculator.divide(7, 2), 3.5, "Division with decimal result")
assert_error(function() calculator.divide(5, 0) end, "Division by zero")
print("✓ Basic operations tests passed")
end
local function test_advanced_operations()
print("Testing advanced operations...")
assert_equals(calculator.power(2, 3), 8, "Power test")
assert_equals(calculator.power(5, 0), 1, "Power of zero")
assert_equals(calculator.power(10, -1), 0.1, "Negative exponent")
assert_equals(calculator.factorial(0), 1, "Factorial of 0")
assert_equals(calculator.factorial(1), 1, "Factorial of 1")
assert_equals(calculator.factorial(5), 120, "Factorial of 5")
assert_equals(calculator.factorial(10), 3628800, "Factorial of 10")
assert_error(function() calculator.factorial(-1) end, "Factorial of negative")
print("✓ Advanced operations tests passed")
end
local function test_statistics()
print("Testing statistics functions...")
-- Mean tests
assert_equals(calculator.mean({1, 2, 3, 4, 5}), 3, "Mean of sequential numbers")
assert_equals(calculator.mean({10}), 10, "Mean of single number")
assert_equals(calculator.mean({-5, 5}), 0, "Mean with negatives")
assert_equals(calculator.mean({}), nil, "Mean of empty array")
-- Median tests
assert_equals(calculator.median({1, 2, 3, 4, 5}), 3, "Median of odd count")
assert_equals(calculator.median({1, 2, 3, 4}), 2.5, "Median of even count")
assert_equals(calculator.median({5, 1, 3, 2, 4}), 3, "Median of unsorted")
assert_equals(calculator.median({7}), 7, "Median of single number")
assert_equals(calculator.median({}), nil, "Median of empty array")
print("✓ Statistics tests passed")
end
-- Run all tests
local function run_all_tests()
print("Running calculator tests...\n")
test_basic_operations()
test_advanced_operations()
test_statistics()
print("\n✅ All calculator tests passed!")
end
-- Execute tests if run directly
if arg and arg[0] and arg[0]:match("test_calculator%.lua$") then
run_all_tests()
end
return {
run_all_tests = run_all_tests,
test_basic_operations = test_basic_operations,
test_advanced_operations = test_advanced_operations,
test_statistics = test_statistics
}
@@ -0,0 +1,11 @@
# Nix specific
result
result-*
.direnv/
# Build artifacts
*.drv
# IDE
.vscode/
.idea/
@@ -0,0 +1,182 @@
# default.nix - Traditional Nix expression for backwards compatibility
{ pkgs ? import <nixpkgs> {} }:
let
# Import library functions
lib = pkgs.lib;
stdenv = pkgs.stdenv;
# Import our custom utilities
utils = import ./lib/utils.nix { inherit lib; };
# Custom function to create a greeting
makeGreeting = name: "Hello, ${name}!";
# List manipulation functions (using imported utils)
listUtils = {
double = list: map (x: x * 2) list;
sum = list: lib.foldl' (acc: x: acc + x) 0 list;
average = list:
if list == []
then 0
else (listUtils.sum list) / (builtins.length list);
# Use function from imported utils
unique = utils.lists.unique;
};
# String utilities
stringUtils = rec {
capitalize = str:
let
first = lib.substring 0 1 str;
rest = lib.substring 1 (-1) str;
in
(lib.toUpper first) + rest;
repeat = n: str: lib.concatStrings (lib.genList (_: str) n);
padLeft = width: char: str:
let
len = lib.stringLength str;
padding = if len >= width then 0 else width - len;
in
(repeat padding char) + str;
};
# Package builder helper
buildSimplePackage = { name, version, script }:
stdenv.mkDerivation {
pname = name;
inherit version;
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out/bin
cat > $out/bin/${name} << EOF
#!/usr/bin/env bash
${script}
EOF
chmod +x $out/bin/${name}
'';
};
in rec {
# Export utilities
inherit listUtils stringUtils makeGreeting;
# Export imported utilities directly
inherit (utils) math strings;
# Example packages
hello = buildSimplePackage {
name = "hello";
version = "1.0";
script = ''
echo "${makeGreeting "World"}"
'';
};
calculator = buildSimplePackage {
name = "calculator";
version = "0.1";
script = ''
if [ $# -ne 3 ]; then
echo "Usage: calculator <num1> <op> <num2>"
exit 1
fi
case $2 in
+) echo $(($1 + $3)) ;;
-) echo $(($1 - $3)) ;;
x) echo $(($1 * $3)) ;;
/) echo $(($1 / $3)) ;;
*) echo "Unknown operator: $2" ;;
esac
'';
};
# Environment with multiple packages
devEnv = pkgs.buildEnv {
name = "dev-environment";
paths = with pkgs; [
git
vim
bash
hello
calculator
];
};
# Shell derivation
shell = pkgs.mkShell {
buildInputs = with pkgs; [
bash
coreutils
findutils
gnugrep
gnused
];
shellHook = ''
echo "Entering Nix shell environment"
echo "Available custom functions: makeGreeting, listUtils, stringUtils"
'';
};
# Configuration example
config = {
system = {
stateVersion = "23.11";
enable = true;
};
services = {
nginx = {
enable = false;
virtualHosts = {
"example.com" = {
root = "/var/www/example";
locations."/" = {
index = "index.html";
};
};
};
};
};
users = {
testUser = {
name = "test";
group = "users";
home = "/home/test";
shell = "${pkgs.bash}/bin/bash";
};
};
};
# Recursive attribute set example
tree = {
root = {
value = 1;
left = {
value = 2;
left = { value = 4; };
right = { value = 5; };
};
right = {
value = 3;
left = { value = 6; };
right = { value = 7; };
};
};
# Tree traversal function
traverse = node:
if node ? left && node ? right
then [ node.value ] ++ (tree.traverse node.left) ++ (tree.traverse node.right)
else if node ? value
then [ node.value ]
else [];
};
}
@@ -0,0 +1,146 @@
{
description = "Test Nix flake for language server testing";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Import our default.nix for shared logic
defaultNix = import ./default.nix { inherit pkgs; };
# Custom derivation for testing
hello-custom = pkgs.stdenv.mkDerivation {
pname = "hello-custom";
version = "1.0.0";
src = ./.;
buildInputs = with pkgs; [
bash
coreutils
];
installPhase = ''
mkdir -p $out/bin
cp ${./scripts/hello.sh} $out/bin/hello-custom
chmod +x $out/bin/hello-custom
'';
meta = with pkgs.lib; {
description = "A custom hello world script";
license = licenses.mit;
platforms = platforms.all;
};
};
# Development shell configuration
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
# Development tools
git
gnumake
gcc
# Nix tools
nix-prefetch-git
nixpkgs-fmt
nil
# Languages
python3
nodejs
rustc
cargo
];
shellHook = ''
echo "Welcome to the Nix development shell!"
echo "Available tools: git, make, gcc, python3, nodejs, rustc"
'';
};
in
{
# Packages
packages = {
default = hello-custom;
inherit hello-custom;
# Another package for testing
utils = pkgs.stdenv.mkDerivation {
pname = "test-utils";
version = "0.1.0";
src = ./.;
installPhase = ''
mkdir -p $out/share
echo "Utility functions" > $out/share/utils.txt
'';
};
};
# Apps
apps = {
default = {
type = "app";
program = "${hello-custom}/bin/hello-custom";
};
hello = {
type = "app";
program = "${hello-custom}/bin/hello-custom";
};
};
# Development shells
devShells = {
default = devShell;
# Minimal shell for testing
minimal = pkgs.mkShell {
buildInputs = with pkgs; [
bash
coreutils
];
};
};
# Overlay
overlays.default = final: prev: {
inherit hello-custom;
};
# NixOS module
nixosModules.default = { config, lib, pkgs, ... }:
with lib;
{
options.services.hello-custom = {
enable = mkEnableOption "hello-custom service";
message = mkOption {
type = types.str;
default = "Hello from NixOS!";
description = "Message to display";
};
};
config = mkIf config.services.hello-custom.enable {
systemd.services.hello-custom = {
description = "Hello Custom Service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${hello-custom}/bin/hello-custom";
Type = "oneshot";
};
};
};
};
}
);
}
@@ -0,0 +1,165 @@
# Utility functions library
{ lib }:
rec {
# Math utilities
math = {
# Calculate factorial
factorial = n:
if n == 0
then 1
else n * factorial (n - 1);
# Calculate fibonacci number
fibonacci = n:
if n <= 1
then n
else (fibonacci (n - 1)) + (fibonacci (n - 2));
# Check if number is prime
isPrime = n:
let
checkDivisible = i:
if i * i > n then true
else if lib.mod n i == 0 then false
else checkDivisible (i + 1);
in
if n <= 1 then false
else if n <= 3 then true
else checkDivisible 2;
# Greatest common divisor
gcd = a: b:
if b == 0
then a
else gcd b (lib.mod a b);
};
# String manipulation
strings = {
# Reverse a string
reverse = str:
let
len = lib.stringLength str;
chars = lib.genList (i: lib.substring (len - i - 1) 1 str) len;
in
lib.concatStrings chars;
# Check if string is palindrome
isPalindrome = str:
str == strings.reverse str;
# Convert to camelCase
toCamelCase = str:
let
words = lib.splitString "-" str;
capitalize = w:
if w == "" then ""
else (lib.toUpper (lib.substring 0 1 w)) + (lib.substring 1 (-1) w);
capitalizedWords = lib.tail (map capitalize words);
in
(lib.head words) + (lib.concatStrings capitalizedWords);
# Convert to snake_case
toSnakeCase = str:
lib.replaceStrings ["-"] ["_"] (lib.toLower str);
};
# List operations
lists = {
# Get unique elements
unique = list:
lib.foldl' (acc: x:
if lib.elem x acc
then acc
else acc ++ [x]
) [] list;
# Zip two lists
zip = list1: list2:
let
len1 = lib.length list1;
len2 = lib.length list2;
minLen = if len1 < len2 then len1 else len2;
in
lib.genList (i: {
fst = lib.elemAt list1 i;
snd = lib.elemAt list2 i;
}) minLen;
# Flatten nested list
flatten = list:
lib.foldl' (acc: x:
if builtins.isList x
then acc ++ (flatten x)
else acc ++ [x]
) [] list;
# Partition list by predicate
partition = pred: list:
lib.foldl' (acc: x:
if pred x
then { yes = acc.yes ++ [x]; no = acc.no; }
else { yes = acc.yes; no = acc.no ++ [x]; }
) { yes = []; no = []; } list;
};
# Attribute set operations
attrs = {
# Deep merge two attribute sets
deepMerge = attr1: attr2:
lib.recursiveUpdate attr1 attr2;
# Filter attributes by predicate
filterAttrs = pred: attrs:
lib.filterAttrs pred attrs;
# Map over attribute values
mapValues = f: attrs:
lib.mapAttrs (name: value: f value) attrs;
# Get nested attribute safely
getAttrPath = path: default: attrs:
lib.attrByPath path default attrs;
};
# File system utilities
files = {
# Read JSON file
readJSON = path:
builtins.fromJSON (builtins.readFile path);
# Read TOML file
readTOML = path:
builtins.fromTOML (builtins.readFile path);
# Check if path exists
pathExists = path:
builtins.pathExists path;
# Get file type
getFileType = path:
let
type = builtins.readFileType path;
in
if type == "directory" then "dir"
else if type == "regular" then "file"
else if type == "symlink" then "link"
else "unknown";
};
# Validation utilities
validate = {
# Check if value is email
isEmail = str:
builtins.match "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" str != null;
# Check if value is URL
isURL = str:
builtins.match "^https?://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" str != null;
# Check if value is valid version
isVersion = str:
builtins.match "^[0-9]+\\.[0-9]+\\.[0-9]+$" str != null;
};
}
@@ -0,0 +1,145 @@
# Example NixOS module
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.example;
# Helper function to generate config file
generateConfig = settings: ''
# Generated configuration
${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${toString v}") settings)}
'';
in {
# Module options
options = {
services.example = {
enable = mkEnableOption "example service";
package = mkOption {
type = types.package;
default = pkgs.hello;
description = "Package to use for the service";
};
port = mkOption {
type = types.port;
default = 8080;
description = "Port to listen on";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Host to bind to";
};
workers = mkOption {
type = types.int;
default = 4;
description = "Number of worker processes";
};
settings = mkOption {
type = types.attrsOf types.anything;
default = {};
description = "Additional settings";
};
users = mkOption {
type = types.listOf types.str;
default = [];
description = "List of users with access";
};
database = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable database support";
};
type = mkOption {
type = types.enum [ "postgresql" "mysql" "sqlite" ];
default = "sqlite";
description = "Database type";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host";
};
name = mkOption {
type = types.str;
default = "example";
description = "Database name";
};
};
};
};
# Module configuration
config = mkIf cfg.enable {
# System packages
environment.systemPackages = [ cfg.package ];
# Systemd service
systemd.services.example = {
description = "Example Service";
after = [ "network.target" ] ++ (optional cfg.database.enable "postgresql.service");
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = "example";
Group = "example";
ExecStart = "${cfg.package}/bin/example --port ${toString cfg.port} --host ${cfg.host}";
Restart = "on-failure";
RestartSec = 5;
# Security hardening
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
};
environment = {
EXAMPLE_WORKERS = toString cfg.workers;
EXAMPLE_CONFIG = generateConfig cfg.settings;
} // optionalAttrs cfg.database.enable {
DATABASE_TYPE = cfg.database.type;
DATABASE_HOST = cfg.database.host;
DATABASE_NAME = cfg.database.name;
};
};
# User and group
users.users.example = {
isSystemUser = true;
group = "example";
description = "Example service user";
};
users.groups.example = {};
# Firewall rules
networking.firewall = mkIf (cfg.host == "0.0.0.0") {
allowedTCPPorts = [ cfg.port ];
};
# Database setup
services.postgresql = mkIf (cfg.database.enable && cfg.database.type == "postgresql") {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [{
name = "example";
ensureDBOwnership = true;
}];
};
};
}
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Simple hello script for testing
echo "Hello from Nix!"
@@ -0,0 +1,5 @@
zig-cache/
zig-out/
.zig-cache/
build/
dist/
@@ -0,0 +1,31 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "test_repo",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const lib_tests = b.addTest(.{
.root_source_file = b.path("src/calculator.zig"),
.target = target,
.optimize = optimize,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_tests.step);
}
@@ -0,0 +1,66 @@
const std = @import("std");
pub const CalculatorError = error{
DivisionByZero,
Overflow,
};
pub const Calculator = struct {
const Self = @This();
pub fn init() Self {
return .{};
}
pub fn add(self: Self, a: i32, b: i32) i32 {
_ = self;
return a + b;
}
pub fn subtract(self: Self, a: i32, b: i32) i32 {
_ = self;
return a - b;
}
pub fn multiply(self: Self, a: i32, b: i32) i32 {
_ = self;
return a * b;
}
pub fn divide(self: Self, a: i32, b: i32) !f64 {
_ = self;
if (b == 0) {
return CalculatorError.DivisionByZero;
}
return @as(f64, @floatFromInt(a)) / @as(f64, @floatFromInt(b));
}
pub fn power(self: Self, base: i32, exponent: u32) i64 {
_ = self;
return std.math.pow(i64, base, exponent);
}
};
test "Calculator add" {
const calc = Calculator.init();
try std.testing.expectEqual(@as(i32, 7), calc.add(3, 4));
try std.testing.expectEqual(@as(i32, 0), calc.add(-5, 5));
}
test "Calculator subtract" {
const calc = Calculator.init();
try std.testing.expectEqual(@as(i32, -1), calc.subtract(3, 4));
try std.testing.expectEqual(@as(i32, 10), calc.subtract(15, 5));
}
test "Calculator multiply" {
const calc = Calculator.init();
try std.testing.expectEqual(@as(i32, 12), calc.multiply(3, 4));
try std.testing.expectEqual(@as(i32, -25), calc.multiply(-5, 5));
}
test "Calculator divide" {
const calc = Calculator.init();
try std.testing.expectEqual(@as(f64, 2.0), try calc.divide(10, 5));
try std.testing.expectError(CalculatorError.DivisionByZero, calc.divide(10, 0));
}
@@ -0,0 +1,32 @@
const std = @import("std");
const calculator = @import("calculator.zig");
const math_utils = @import("math_utils.zig");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const calc = calculator.Calculator.init();
const sum = calc.add(10, 5);
const diff = calc.subtract(10, 5);
const prod = calc.multiply(10, 5);
const quot = calc.divide(10, 5) catch |err| {
try stdout.print("Division error: {}\n", .{err});
return;
};
try stdout.print("10 + 5 = {}\n", .{sum});
try stdout.print("10 - 5 = {}\n", .{diff});
try stdout.print("10 * 5 = {}\n", .{prod});
try stdout.print("10 / 5 = {}\n", .{quot});
const factorial_result = math_utils.factorial(5);
try stdout.print("5! = {}\n", .{factorial_result});
const is_prime = math_utils.isPrime(17);
try stdout.print("Is 17 prime? {}\n", .{is_prime});
}
pub fn greeting(name: []const u8) []const u8 {
return std.fmt.allocPrint(std.heap.page_allocator, "Hello, {s}!", .{name}) catch "Hello!";
}
@@ -0,0 +1,65 @@
const std = @import("std");
pub fn factorial(n: u32) u64 {
if (n == 0 or n == 1) {
return 1;
}
var result: u64 = 1;
var i: u32 = 2;
while (i <= n) : (i += 1) {
result *= i;
}
return result;
}
pub fn isPrime(n: u32) bool {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 or n % 3 == 0) return false;
var i: u32 = 5;
while (i * i <= n) : (i += 6) {
if (n % i == 0 or n % (i + 2) == 0) {
return false;
}
}
return true;
}
pub fn gcd(a: u32, b: u32) u32 {
var x = a;
var y = b;
while (y != 0) {
const temp = y;
y = x % y;
x = temp;
}
return x;
}
pub fn lcm(a: u32, b: u32) u32 {
return (a * b) / gcd(a, b);
}
test "factorial" {
try std.testing.expectEqual(@as(u64, 1), factorial(0));
try std.testing.expectEqual(@as(u64, 1), factorial(1));
try std.testing.expectEqual(@as(u64, 120), factorial(5));
try std.testing.expectEqual(@as(u64, 3628800), factorial(10));
}
test "isPrime" {
try std.testing.expect(!isPrime(0));
try std.testing.expect(!isPrime(1));
try std.testing.expect(isPrime(2));
try std.testing.expect(isPrime(3));
try std.testing.expect(!isPrime(4));
try std.testing.expect(isPrime(17));
try std.testing.expect(!isPrime(100));
}
test "gcd and lcm" {
try std.testing.expectEqual(@as(u32, 6), gcd(12, 18));
try std.testing.expectEqual(@as(u32, 1), gcd(17, 19));
try std.testing.expectEqual(@as(u32, 36), lcm(12, 18));
}
@@ -0,0 +1,12 @@
{
"enable_build_on_save": true,
"build_on_save_args": ["build"],
"enable_autofix": false,
"semantic_tokens": "full",
"enable_inlay_hints": true,
"inlay_hints_show_builtin": true,
"inlay_hints_exclude_single_argument": true,
"inlay_hints_show_parameter_name": true,
"skip_std_references": false,
"max_detail_length": 1048576
}
+255
View File
@@ -0,0 +1,255 @@
"""
Tests for the Lua language server implementation.
These tests validate symbol finding and cross-file reference capabilities
for Lua modules and functions.
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@pytest.mark.lua
class TestLuaLanguageServer:
"""Test Lua language server symbol finding and cross-file references."""
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_find_symbols_in_calculator(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific functions in calculator.lua."""
symbols = language_server.request_document_symbols("src/calculator.lua")
assert symbols is not None
assert len(symbols) > 0
# Extract function names from the returned structure
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
function_names = set()
for symbol in symbol_list:
if isinstance(symbol, dict):
name = symbol.get("name", "")
# Handle both plain names and module-prefixed names
if "." in name:
name = name.split(".")[-1]
if symbol.get("kind") == SymbolKind.Function:
function_names.add(name)
# Verify exact calculator functions exist
expected_functions = {"add", "subtract", "multiply", "divide", "factorial"}
found_functions = function_names & expected_functions
assert found_functions == expected_functions, f"Expected exactly {expected_functions}, found {found_functions}"
# Verify specific functions
assert "add" in function_names, "add function not found"
assert "multiply" in function_names, "multiply function not found"
assert "factorial" in function_names, "factorial function not found"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific functions in utils.lua."""
symbols = language_server.request_document_symbols("src/utils.lua")
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
function_names = set()
all_symbols = set()
for symbol in symbol_list:
if isinstance(symbol, dict):
name = symbol.get("name", "")
all_symbols.add(name)
# Handle both plain names and module-prefixed names
if "." in name:
name = name.split(".")[-1]
if symbol.get("kind") == SymbolKind.Function:
function_names.add(name)
# Verify exact string utility functions
expected_utils = {"trim", "split", "starts_with", "ends_with"}
found_utils = function_names & expected_utils
assert found_utils == expected_utils, f"Expected exactly {expected_utils}, found {found_utils}"
# Verify exact table utility functions
table_utils = {"deep_copy", "table_contains", "table_merge"}
found_table_utils = function_names & table_utils
assert found_table_utils == table_utils, f"Expected exactly {table_utils}, found {found_table_utils}"
# Check for Logger class/table
assert "Logger" in all_symbols or any("Logger" in s for s in all_symbols), "Logger not found in symbols"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_find_symbols_in_main(self, language_server: SolidLanguageServer) -> None:
"""Test finding functions in main.lua."""
symbols = language_server.request_document_symbols("main.lua")
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
function_names = set()
for symbol in symbol_list:
if isinstance(symbol, dict) and symbol.get("kind") == SymbolKind.Function:
function_names.add(symbol.get("name", ""))
# Verify exact main functions exist
expected_funcs = {"print_banner", "test_calculator", "test_utils"}
found_funcs = function_names & expected_funcs
assert found_funcs == expected_funcs, f"Expected exactly {expected_funcs}, found {found_funcs}"
assert "test_calculator" in function_names, "test_calculator function not found"
assert "test_utils" in function_names, "test_utils function not found"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_cross_file_references_calculator_add(self, language_server: SolidLanguageServer) -> None:
"""Test finding cross-file references to calculator.add function."""
symbols = language_server.request_document_symbols("src/calculator.lua")
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find the add function
add_symbol = None
for sym in symbol_list:
if isinstance(sym, dict):
name = sym.get("name", "")
if "add" in name or name == "add":
add_symbol = sym
break
assert add_symbol is not None, "add function not found in calculator.lua"
# Get references to the add function
range_info = add_symbol.get("selectionRange", add_symbol.get("range"))
assert range_info is not None, "add function has no range information"
range_start = range_info["start"]
refs = language_server.request_references("src/calculator.lua", range_start["line"], range_start["character"])
assert refs is not None
assert isinstance(refs, list)
# add function appears in: main.lua (lines 16, 71), test_calculator.lua (lines 22, 23, 24)
# Note: The declaration itself may or may not be included as a reference
assert len(refs) >= 5, f"Should find at least 5 references to calculator.add, found {len(refs)}"
# Verify exact reference locations
ref_files = {}
for ref in refs:
filename = ref.get("uri", "").split("/")[-1]
if filename not in ref_files:
ref_files[filename] = []
ref_files[filename].append(ref["range"]["start"]["line"])
# The declaration may or may not be included
if "calculator.lua" in ref_files:
assert (
5 in ref_files["calculator.lua"]
), f"If declaration is included, it should be at line 6 (0-indexed: 5), found at {ref_files['calculator.lua']}"
# Check main.lua has usages
assert "main.lua" in ref_files, "Should find add usages in main.lua"
assert (
15 in ref_files["main.lua"] or 70 in ref_files["main.lua"]
), f"Should find add usage in main.lua, found at lines {ref_files.get('main.lua', [])}"
# Check for cross-file references from main.lua
main_refs = [ref for ref in refs if "main.lua" in ref.get("uri", "")]
assert len(main_refs) > 0, "calculator.add should be called in main.lua"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_cross_file_references_utils_trim(self, language_server: SolidLanguageServer) -> None:
"""Test finding cross-file references to utils.trim function."""
symbols = language_server.request_document_symbols("src/utils.lua")
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find the trim function
trim_symbol = None
for sym in symbol_list:
if isinstance(sym, dict):
name = sym.get("name", "")
if "trim" in name or name == "trim":
trim_symbol = sym
break
assert trim_symbol is not None, "trim function not found in utils.lua"
# Get references to the trim function
range_info = trim_symbol.get("selectionRange", trim_symbol.get("range"))
assert range_info is not None, "trim function has no range information"
range_start = range_info["start"]
refs = language_server.request_references("src/utils.lua", range_start["line"], range_start["character"])
assert refs is not None
assert isinstance(refs, list)
# trim function appears in: usage (line 32 in main.lua)
# Note: The declaration itself may or may not be included as a reference
assert len(refs) >= 1, f"Should find at least 1 reference to utils.trim, found {len(refs)}"
# Verify exact reference locations
ref_files = {}
for ref in refs:
filename = ref.get("uri", "").split("/")[-1]
if filename not in ref_files:
ref_files[filename] = []
ref_files[filename].append(ref["range"]["start"]["line"])
# The declaration may or may not be included
if "utils.lua" in ref_files:
assert (
5 in ref_files["utils.lua"]
), f"If declaration is included, it should be at line 6 (0-indexed: 5), found at {ref_files['utils.lua']}"
# Check main.lua has usage
assert "main.lua" in ref_files, "Should find trim usage in main.lua"
assert (
31 in ref_files["main.lua"]
), f"Should find trim usage at line 32 (0-indexed: 31) in main.lua, found at lines {ref_files.get('main.lua', [])}"
# Check for cross-file references from main.lua
main_refs = [ref for ref in refs if "main.lua" in ref.get("uri", "")]
assert len(main_refs) > 0, "utils.trim should be called in main.lua"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
"""Test hover information for symbols."""
# Get hover info for a function
hover_info = language_server.request_hover("src/calculator.lua", 5, 10) # Position near add function
assert hover_info is not None, "Should provide hover information"
# Hover info could be a dict with 'contents' or a string
if isinstance(hover_info, dict):
assert "contents" in hover_info or "value" in hover_info, "Hover should have contents"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
"""Test that full symbol tree is not empty."""
symbols = language_server.request_full_symbol_tree()
assert symbols is not None
assert len(symbols) > 0, "Symbol tree should not be empty"
# The tree should have at least one root node
root = symbols[0]
assert isinstance(root, dict), "Root should be a dict"
assert "name" in root, "Root should have a name"
@pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
def test_references_between_test_and_source(self, language_server: SolidLanguageServer) -> None:
"""Test finding references from test files to source files."""
# Check if test_calculator.lua references calculator module
test_symbols = language_server.request_document_symbols("tests/test_calculator.lua")
assert test_symbols is not None
assert len(test_symbols) > 0
# The test file should have some content that references calculator
symbol_list = test_symbols[0] if isinstance(test_symbols, tuple) else test_symbols
assert len(symbol_list) > 0, "test_calculator.lua should have symbols"
+218
View File
@@ -0,0 +1,218 @@
"""
Tests for the Nix language server implementation using nixd.
These tests validate symbol finding and cross-file reference capabilities for Nix expressions.
"""
import platform
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
# Skip all Nix tests on Windows as Nix doesn't support Windows
pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows")
@pytest.mark.nix
class TestNixLanguageServer:
"""Test Nix language server symbol finding capabilities."""
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_find_symbols_in_default_nix(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific symbols in default.nix."""
symbols = language_server.request_document_symbols("default.nix")
assert symbols is not None
assert len(symbols) > 0
# Extract symbol names from the returned structure
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# Verify specific function exists
assert "makeGreeting" in symbol_names, "makeGreeting function not found"
# Verify exact attribute sets are found
expected_attrs = {"listUtils", "stringUtils"}
found_attrs = symbol_names & expected_attrs
assert found_attrs == expected_attrs, f"Expected exactly {expected_attrs}, found {found_attrs}"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in lib/utils.nix."""
symbols = language_server.request_document_symbols("lib/utils.nix")
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# Verify exact utility modules are found
expected_modules = {"math", "strings", "lists", "attrs"}
found_modules = symbol_names & expected_modules
assert found_modules == expected_modules, f"Expected exactly {expected_modules}, found {found_modules}"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_find_symbols_in_flake(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in flake.nix."""
symbols = language_server.request_document_symbols("flake.nix")
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# Flakes must have either inputs or outputs
assert "inputs" in symbol_names or "outputs" in symbol_names, "Flake must have inputs or outputs"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_find_symbols_in_module(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in a NixOS module."""
symbols = language_server.request_document_symbols("modules/example.nix")
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# NixOS modules must have either options or config
assert "options" in symbol_names or "config" in symbol_names, "Module must have options or config"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding references within the same file."""
symbols = language_server.request_document_symbols("default.nix")
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find makeGreeting function
greeting_symbol = None
for sym in symbol_list:
if sym.get("name") == "makeGreeting":
greeting_symbol = sym
break
assert greeting_symbol is not None, "makeGreeting function not found"
assert "range" in greeting_symbol, "Symbol must have range information"
range_start = greeting_symbol["range"]["start"]
refs = language_server.request_references("default.nix", range_start["line"], range_start["character"])
assert refs is not None
assert isinstance(refs, list)
# nixd finds at least the inherit statement (line 66)
assert len(refs) >= 1, f"Should find at least 1 reference to makeGreeting, found {len(refs)}"
# Verify makeGreeting is referenced at expected locations
if refs:
ref_lines = sorted([ref["range"]["start"]["line"] for ref in refs])
# Check if we found the inherit (line 66, 0-indexed: 65)
assert 65 in ref_lines, f"Should find makeGreeting inherit at line 66, found at lines {[l+1 for l in ref_lines]}"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
"""Test hover information for symbols."""
# Get hover info for makeGreeting function
hover_info = language_server.request_hover("default.nix", 9, 5) # Position near makeGreeting
assert hover_info is not None, "Should provide hover information"
if isinstance(hover_info, dict) and len(hover_info) > 0:
# If hover info is provided, it should have proper structure
assert "contents" in hover_info or "value" in hover_info, "Hover should have contents or value"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_cross_file_references_utils_import(self, language_server: SolidLanguageServer) -> None:
"""Test finding cross-file references for imported utils."""
# Find references to 'utils' which is imported in default.nix from lib/utils.nix
# Line 10 in default.nix: utils = import ./lib/utils.nix { inherit lib; };
refs = language_server.request_references("default.nix", 9, 2) # Position of 'utils'
assert refs is not None
assert isinstance(refs, list)
# Should find references within default.nix where utils is used
default_refs = [ref for ref in refs if "default.nix" in ref.get("uri", "")]
# utils is: imported (line 10), used in listUtils.unique (line 24), inherited in exports (line 69)
assert len(default_refs) >= 2, f"Should find at least 2 references to utils in default.nix, found {len(default_refs)}"
# Verify utils is referenced at expected locations (0-indexed)
if default_refs:
ref_lines = sorted([ref["range"]["start"]["line"] for ref in default_refs])
# Check for key references - at least the import (line 10) or usage (line 24)
assert (
9 in ref_lines or 23 in ref_lines
), f"Should find utils import or usage, found references at lines {[l+1 for l in ref_lines]}"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_verify_imports_exist(self, language_server: SolidLanguageServer) -> None:
"""Verify that our test files have proper imports set up."""
# Verify that default.nix imports utils from lib/utils.nix
symbols = language_server.request_document_symbols("default.nix")
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Check that makeGreeting exists (defined in default.nix)
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
assert "makeGreeting" in symbol_names, "makeGreeting should be found in default.nix"
# Verify lib/utils.nix has the expected structure
utils_symbols = language_server.request_document_symbols("lib/utils.nix")
assert utils_symbols is not None
utils_list = utils_symbols[0] if isinstance(utils_symbols, tuple) else utils_symbols
utils_names = {sym.get("name") for sym in utils_list if isinstance(sym, dict)}
# Verify key functions exist in utils
assert "math" in utils_names, "math should be found in lib/utils.nix"
assert "strings" in utils_names, "strings should be found in lib/utils.nix"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_go_to_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Test go-to-definition from default.nix to lib/utils.nix."""
# Line 24 in default.nix: unique = utils.lists.unique;
# Test go-to-definition for 'utils'
definitions = language_server.request_definition("default.nix", 23, 14) # Position of 'utils'
assert definitions is not None
assert isinstance(definitions, list)
if len(definitions) > 0:
# Should point to the import statement or utils.nix
assert any(
"utils" in def_item.get("uri", "") or "default.nix" in def_item.get("uri", "") for def_item in definitions
), "Definition should relate to utils import or utils.nix file"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_definition_navigation_in_flake(self, language_server: SolidLanguageServer) -> None:
"""Test definition navigation in flake.nix."""
# Test that we can navigate to definitions within flake.nix
# Line 69: default = hello-custom;
definitions = language_server.request_definition("flake.nix", 68, 20) # Position of 'hello-custom'
assert definitions is not None
assert isinstance(definitions, list)
# nixd should find the definition of hello-custom in the same file
if len(definitions) > 0:
assert any(
"flake.nix" in def_item.get("uri", "") for def_item in definitions
), "Should find hello-custom definition in flake.nix"
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
"""Test that full symbol tree is not empty."""
symbols = language_server.request_full_symbol_tree()
assert symbols is not None
assert len(symbols) > 0, "Symbol tree should not be empty"
# The tree should have at least one root node
root = symbols[0]
assert isinstance(root, dict), "Root should be a dict"
assert "name" in root, "Root should have a name"
+343
View File
@@ -0,0 +1,343 @@
"""
Basic integration tests for Zig language server functionality.
These tests validate symbol finding and navigation capabilities using the Zig Language Server (ZLS).
Note: ZLS requires files to be open in the editor to find cross-file references (performance optimization).
"""
import os
import sys
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@pytest.mark.zig
@pytest.mark.skipif(
sys.platform == "win32", reason="ZLS is disabled on Windows - cross-file references don't work reliably. Reason unknown."
)
class TestZigLanguageServer:
"""Test Zig language server symbol finding and navigation capabilities.
NOTE: All tests are skipped on Windows as ZLS is disabled on that platform
due to unreliable cross-file reference functionality. Reason unknown.
"""
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_find_symbols_in_main(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific symbols in main.zig."""
file_path = os.path.join("src", "main.zig")
symbols = language_server.request_document_symbols(file_path)
assert symbols is not None
assert len(symbols) > 0
# Extract symbol names from the returned structure
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# Verify specific symbols exist
assert "main" in symbol_names, "main function not found"
assert "greeting" in symbol_names, "greeting function not found"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_find_symbols_in_calculator(self, language_server: SolidLanguageServer) -> None:
"""Test finding Calculator struct and its methods."""
file_path = os.path.join("src", "calculator.zig")
symbols = language_server.request_document_symbols(file_path)
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find Calculator struct
calculator_symbol = None
for sym in symbol_list:
if sym.get("name") == "Calculator":
calculator_symbol = sym
break
assert calculator_symbol is not None, "Calculator struct not found"
# ZLS may use different symbol kinds for structs (14 = Namespace, 5 = Class, 23 = Struct)
assert calculator_symbol.get("kind") in [
SymbolKind.Class,
SymbolKind.Struct,
SymbolKind.Namespace,
5,
14,
23,
], "Calculator should be a struct/class/namespace"
# Check for Calculator methods (init, add, subtract, etc.)
# Methods might be in children or at the same level
all_symbols = []
for sym in symbol_list:
all_symbols.append(sym.get("name"))
if "children" in sym:
for child in sym["children"]:
all_symbols.append(child.get("name"))
# Verify exact calculator methods exist
expected_methods = {"init", "add", "subtract", "multiply", "divide"}
found_methods = set(all_symbols) & expected_methods
assert found_methods == expected_methods, f"Expected exactly {expected_methods}, found: {found_methods}"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_find_symbols_in_math_utils(self, language_server: SolidLanguageServer) -> None:
"""Test finding functions in math_utils.zig."""
file_path = os.path.join("src", "math_utils.zig")
symbols = language_server.request_document_symbols(file_path)
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
# Verify math utility functions exist
assert "factorial" in symbol_names, "factorial function not found"
assert "isPrime" in symbol_names, "isPrime function not found"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding references within the same file."""
file_path = os.path.join("src", "calculator.zig")
symbols = language_server.request_document_symbols(file_path)
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find Calculator struct
calculator_symbol = None
for sym in symbol_list:
if sym.get("name") == "Calculator":
calculator_symbol = sym
break
assert calculator_symbol is not None, "Calculator struct not found"
# Find references to Calculator within the same file
sel_range = calculator_symbol.get("selectionRange", calculator_symbol.get("range"))
assert sel_range is not None, "Calculator symbol has no range information"
sel_start = sel_range["start"]
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert refs is not None
assert isinstance(refs, list)
# ZLS finds references within the same file
# Calculator is used in 4 test usages (lines 45, 51, 57, 63)
# Note: ZLS may not include the declaration itself as a reference
assert len(refs) >= 4, f"Should find at least 4 Calculator references within calculator.zig, found {len(refs)}"
# Verify we found the test usages
ref_lines = sorted([ref["range"]["start"]["line"] for ref in refs])
test_lines = [44, 50, 56, 62] # 0-indexed: tests at lines 45, 51, 57, 63
for line in test_lines:
assert line in ref_lines, f"Should find Calculator reference at line {line + 1}, found at lines {[l + 1 for l in ref_lines]}"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
@pytest.mark.skipif(
sys.platform == "win32", reason="ZLS cross-file references don't work reliably on Windows - URI path handling issues"
)
def test_cross_file_references_with_open_files(self, language_server: SolidLanguageServer) -> None:
"""
Test finding cross-file references with files open.
ZLS limitation: Cross-file references (textDocument/references) only work when
target files are open. This is a performance optimization in ZLS.
NOTE: Disabled on Windows as cross-file references cannot be made to work reliably
due to URI path handling differences between Windows and Unix systems.
"""
import time
# Open the files that contain references to enable cross-file search
with language_server.open_file("build.zig"):
with language_server.open_file(os.path.join("src", "main.zig")):
with language_server.open_file(os.path.join("src", "calculator.zig")):
# Give ZLS a moment to analyze the open files
time.sleep(1)
# Find Calculator struct
symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig"))
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
calculator_symbol = None
for sym in symbol_list:
if sym.get("name") == "Calculator":
calculator_symbol = sym
break
assert calculator_symbol is not None, "Calculator struct not found"
sel_range = calculator_symbol.get("selectionRange", calculator_symbol.get("range"))
assert sel_range is not None, "Calculator symbol has no range information"
# Find references to Calculator
sel_start = sel_range["start"]
refs = language_server.request_references(
os.path.join("src", "calculator.zig"), sel_start["line"], sel_start["character"]
)
assert refs is not None
assert isinstance(refs, list)
# With files open, ZLS should find cross-file references
main_refs = [ref for ref in refs if "main.zig" in ref.get("uri", "")]
assert len(main_refs) >= 1, f"Should find at least 1 Calculator reference in main.zig, found {len(main_refs)}"
# Verify exact location in main.zig (line 8, 0-indexed: 7)
main_ref_line = main_refs[0]["range"]["start"]["line"]
assert (
main_ref_line == 7
), f"Calculator reference in main.zig should be at line 8 (0-indexed: 7), found at line {main_ref_line + 1}"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_cross_file_references_within_file(self, language_server: SolidLanguageServer) -> None:
"""
Test that ZLS finds references within the same file.
Note: ZLS is designed to be lightweight and only analyzes files that are explicitly opened.
Cross-file references require manually opening the relevant files first.
"""
# Find references to Calculator from calculator.zig
file_path = os.path.join("src", "calculator.zig")
symbols = language_server.request_document_symbols(file_path)
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
calculator_symbol = None
for sym in symbol_list:
if sym.get("name") == "Calculator":
calculator_symbol = sym
break
assert calculator_symbol is not None, "Calculator struct not found"
sel_range = calculator_symbol.get("selectionRange", calculator_symbol.get("range"))
assert sel_range is not None, "Calculator symbol has no range information"
sel_start = sel_range["start"]
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert refs is not None
assert isinstance(refs, list)
# ZLS finds references within the same file
# Calculator is used in 4 test usages (lines 45, 51, 57, 63)
# Note: ZLS may not include the declaration itself as a reference
assert len(refs) >= 4, f"Should find at least 4 Calculator references within calculator.zig, found {len(refs)}"
# Verify we found the test usages
ref_lines = sorted([ref["range"]["start"]["line"] for ref in refs])
test_lines = [44, 50, 56, 62] # 0-indexed: tests at lines 45, 51, 57, 63
for line in test_lines:
assert line in ref_lines, f"Should find Calculator reference at line {line + 1}, found at lines {[l + 1 for l in ref_lines]}"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
@pytest.mark.skipif(
sys.platform == "win32", reason="ZLS cross-file references don't work reliably on Windows - URI path handling issues"
)
def test_go_to_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
"""
Test go-to-definition from main.zig to calculator.zig.
ZLS capability: Go-to-definition (textDocument/definition) works cross-file
WITHOUT requiring files to be open.
NOTE: Disabled on Windows as cross-file references cannot be made to work reliably
due to URI path handling differences between Windows and Unix systems.
"""
file_path = os.path.join("src", "main.zig")
# Line 8: const calc = calculator.Calculator.init();
# Test go-to-definition for Calculator
definitions = language_server.request_definition(file_path, 7, 25) # Position of "Calculator"
assert definitions is not None
assert isinstance(definitions, list)
assert len(definitions) > 0, "Should find definition of Calculator"
# Should point to calculator.zig
calc_def = definitions[0]
assert "calculator.zig" in calc_def.get("uri", ""), "Definition should be in calculator.zig"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
@pytest.mark.skipif(
sys.platform == "win32", reason="ZLS cross-file references don't work reliably on Windows - URI path handling issues"
)
def test_cross_file_function_usage(self, language_server: SolidLanguageServer) -> None:
"""Test finding usage of functions from math_utils in main.zig.
NOTE: Disabled on Windows as cross-file references cannot be made to work reliably
due to URI path handling differences between Windows and Unix systems.
"""
# Line 23 in main.zig: const factorial_result = math_utils.factorial(5);
definitions = language_server.request_definition(os.path.join("src", "main.zig"), 22, 40) # Position of "factorial"
assert definitions is not None
assert isinstance(definitions, list)
if len(definitions) > 0:
# Should find factorial definition in math_utils.zig
math_def = [d for d in definitions if "math_utils.zig" in d.get("uri", "")]
assert len(math_def) > 0, "Should find factorial definition in math_utils.zig"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_verify_cross_file_imports(self, language_server: SolidLanguageServer) -> None:
"""Verify that our test files have proper cross-file imports."""
# Verify main.zig imports
main_symbols = language_server.request_document_symbols(os.path.join("src", "main.zig"))
assert main_symbols is not None
main_list = main_symbols[0] if isinstance(main_symbols, tuple) else main_symbols
main_names = {sym.get("name") for sym in main_list if isinstance(sym, dict)}
# main.zig should have main and greeting functions
assert "main" in main_names, "main function should be in main.zig"
assert "greeting" in main_names, "greeting function should be in main.zig"
# Verify calculator.zig exports Calculator
calc_symbols = language_server.request_document_symbols(os.path.join("src", "calculator.zig"))
assert calc_symbols is not None
calc_list = calc_symbols[0] if isinstance(calc_symbols, tuple) else calc_symbols
calc_names = {sym.get("name") for sym in calc_list if isinstance(sym, dict)}
assert "Calculator" in calc_names, "Calculator struct should be in calculator.zig"
# Verify math_utils.zig exports functions
math_symbols = language_server.request_document_symbols(os.path.join("src", "math_utils.zig"))
assert math_symbols is not None
math_list = math_symbols[0] if isinstance(math_symbols, tuple) else math_symbols
math_names = {sym.get("name") for sym in math_list if isinstance(sym, dict)}
assert "factorial" in math_names, "factorial function should be in math_utils.zig"
assert "isPrime" in math_names, "isPrime function should be in math_utils.zig"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
"""Test hover information for symbols."""
file_path = os.path.join("src", "main.zig")
# Get hover info for the main function
hover_info = language_server.request_hover(file_path, 4, 8) # Position of "main" function
assert hover_info is not None, "Should provide hover information for main function"
# Hover info could be a dict with 'contents' or a string
if isinstance(hover_info, dict):
assert "contents" in hover_info or "value" in hover_info, "Hover should have contents"
@pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
"""Test that full symbol tree is not empty."""
symbols = language_server.request_full_symbol_tree()
assert symbols is not None
assert len(symbols) > 0, "Symbol tree should not be empty"
# The tree should have at least one root node
root = symbols[0]
assert isinstance(root, dict), "Root should be a dict"
assert "name" in root, "Root should have a name"