feat(pascal): add Pascal language server support

Add support for Free Pascal/Lazarus projects using pasls.

Features:
- Auto-download pasls binary for Linux, macOS, and Windows
- Platform detection for x64 and arm64 architectures
- Support for FPC environment variables (PP, FPCDIR)

CI: Add FPC compiler installation (fpc-ootb for Windows)

Includes test repository and unit tests.
This commit is contained in:
zen010101 committed 2026-01-10 01:30:55 +08:00
1 parent eb54e834b6
commit fa012910cb
11 files changed
+800 -6

No files matched your search

+53
View File
@@ -367,6 +367,59 @@ jobs:
mv regal.exe "$HOME/bin/"
echo "$HOME/bin" >> $GITHUB_PATH
fi
- name: Install Free Pascal Compiler
shell: bash
run: |
if [[ "${{ runner.os }}" == "Linux" ]]; then
sudo apt-get update
sudo apt-get install -y fpc fpc-source
# Set environment variables for pasls
echo "PP=/usr/bin/fpc" >> $GITHUB_ENV
# Find FPC source directory (version may vary)
FPCDIR=$(ls -d /usr/share/fpcsrc/*/ 2>/dev/null | head -1 || echo "/usr/share/fpcsrc")
echo "FPCDIR=$FPCDIR" >> $GITHUB_ENV
elif [[ "${{ runner.os }}" == "macOS" ]]; then
brew install fpc
# Download FPC source from SourceForge (fpc-src-laz cask is incompatible with ARM64)
FPC_VERSION="3.2.2"
curl -L -o fpc-source.tar.gz "https://sourceforge.net/projects/freepascal/files/Source/${FPC_VERSION}/fpc-${FPC_VERSION}.source.tar.gz/download"
mkdir -p "$HOME/fpcsrc"
tar -xzf fpc-source.tar.gz -C "$HOME/fpcsrc"
rm fpc-source.tar.gz
# Set environment variables for pasls
echo "PP=$(which fpc)" >> $GITHUB_ENV
echo "FPCDIR=$HOME/fpcsrc/fpc-${FPC_VERSION}" >> $GITHUB_ENV
elif [[ "${{ runner.os }}" == "Windows" ]]; then
FPC_VERSION="3.2.2"
# Download freepascal-ootb (includes FPC compiler)
curl -L -o fpc-ootb.zip https://github.com/fredvs/freepascal-ootb/releases/download/${FPC_VERSION}/fpc-ootb-322-x86_64-win64.zip
mkdir -p "$HOME/fpc"
unzip -q fpc-ootb.zip -d "$HOME/fpc"
rm fpc-ootb.zip
# Download FPC source from SourceForge (fpc-ootb only has compiled units, not source)
curl -L -o fpc-source.zip "https://sourceforge.net/projects/freepascal/files/Source/${FPC_VERSION}/fpc-${FPC_VERSION}.source.zip/download"
mkdir -p "$HOME/fpcsrc"
unzip -q fpc-source.zip -d "$HOME/fpcsrc"
rm fpc-source.zip
# Find fpc executable (fpc-ootb uses fpc-ootb.exe as the compiler)
echo "=== FPC directory structure ==="
find "$HOME/fpc" -name "*.exe" -type f 2>/dev/null | head -10
FPC_EXE=$(find "$HOME/fpc" -name "fpc-ootb-64.exe" -type f 2>/dev/null | head -1)
echo "Found FPC executable: $FPC_EXE"
echo "Found FPC source dir: $HOME/fpcsrc/fpc-${FPC_VERSION}"
# Set environment variables for pasls
echo "PP=$FPC_EXE" >> $GITHUB_ENV
echo "FPCDIR=$HOME/fpcsrc/fpc-${FPC_VERSION}" >> $GITHUB_ENV
# Add FPC bin directory to PATH
FPC_BIN_DIR=$(dirname "$FPC_EXE")
echo "$FPC_BIN_DIR" >> $GITHUB_PATH
fi
- name: Verify FPC installation
shell: bash
run: |
echo "PP=$PP"
echo "FPCDIR=$FPCDIR"
fpc -v || echo "FPC not in PATH, using PP directly"
- name: Cache language servers
id: cache-language-servers
uses: actions/cache@v3
+3 -1
View File
@@ -60,7 +60,9 @@ Some languages require additional installations or setup steps, as noted.
(must be explicitly specified via `--language markdown` when generating project config, primarily useful for documentation-heavy projects)
* **Nix**
(requires nixd installation)
* **Perl**
* **Pascal**
(Free Pascal/Lazarus; automatically downloads pasls binary; set PP and FPCDIR environment variables for source navigation)
* **Perl**
(requires installation of Perl::LanguageServer)
* **PHP**
(uses Intelephense LSP; set `INTELEPHENSE_LICENSE_KEY` environment variable for premium features)
+1
View File
@@ -304,6 +304,7 @@ markers = [
"haskell: Haskell language server tests",
"yaml: language server running for YAML",
"powershell: language server running for PowerShell",
"pascal: language server running for Pascal (Free Pascal/Lazarus)",
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
"toml: language server running for TOML",
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
+8 -5
View File
@@ -1,15 +1,18 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran go
# haskell java julia kotlin lua markdown
# nix perl php python python_jedi r
# rego ruby ruby_solargraph rust scala swift
# terraform typescript typescript_vts yaml zig
# dart elixir elm erlang fortran fsharp
# go groovy haskell java julia kotlin
# lua markdown nix pascal perl php
# powershell python python_jedi r rego ruby
# ruby_solargraph rust scala swift terraform toml
# typescript typescript_vts yaml zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal / Lazarus, use pascal
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
@@ -0,0 +1,322 @@
"""
Provides Pascal/Free Pascal specific instantiation of the LanguageServer class using pasls.
Contains various configurations and settings specific to Pascal and Free Pascal.
pasls installation strategy:
1. Use existing pasls from PATH
2. Download prebuilt binary from GitHub releases
Supported platforms for binary download:
- linux-x64, linux-arm64
- osx-x64, osx-arm64
- win-x64
You can pass the following entries in ls_specific_settings["pascal"]:
- (reserved for future use)
"""
import logging
import os
import pathlib
import shutil
import threading
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class PascalLanguageServer(SolidLanguageServer):
"""
Provides Pascal specific instantiation of the LanguageServer class using pasls.
Contains various configurations and settings specific to Free Pascal and Lazarus.
"""
PASLS_VERSION = "0.1.0"
PASLS_RELEASES_URL = "https://github.com/zen010101/pascal-language-server/releases/download"
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a PascalLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
pasls_executable_path = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=pasls_executable_path, cwd=repository_root_path),
"pascal",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.completions_available_event = threading.Event()
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for Pascal Language Server (pasls).
Returns:
str: The command to start the pasls server
"""
# Check if pasls is already in PATH
pasls_in_path = shutil.which("pasls")
if pasls_in_path:
log.info(f"Found pasls in PATH: {pasls_in_path}")
return quote_windows_path(pasls_in_path)
# Use RuntimeDependencyCollection for download
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="PascalLanguageServer",
description="Pascal Language Server for Linux (x64)",
url=f"{cls.PASLS_RELEASES_URL}/v{cls.PASLS_VERSION}/pasls-linux-x64.tar.gz",
platform_id="linux-x64",
archive_type="gztar",
binary_name="pasls",
),
RuntimeDependency(
id="PascalLanguageServer",
description="Pascal Language Server for Linux (arm64)",
url=f"{cls.PASLS_RELEASES_URL}/v{cls.PASLS_VERSION}/pasls-linux-arm64.tar.gz",
platform_id="linux-arm64",
archive_type="gztar",
binary_name="pasls",
),
RuntimeDependency(
id="PascalLanguageServer",
description="Pascal Language Server for macOS (x64)",
url=f"{cls.PASLS_RELEASES_URL}/v{cls.PASLS_VERSION}/pasls-darwin-x64.tar.gz",
platform_id="osx-x64",
archive_type="gztar",
binary_name="pasls",
),
RuntimeDependency(
id="PascalLanguageServer",
description="Pascal Language Server for macOS (arm64)",
url=f"{cls.PASLS_RELEASES_URL}/v{cls.PASLS_VERSION}/pasls-darwin-arm64.tar.gz",
platform_id="osx-arm64",
archive_type="gztar",
binary_name="pasls",
),
RuntimeDependency(
id="PascalLanguageServer",
description="Pascal Language Server for Windows (x64)",
url=f"{cls.PASLS_RELEASES_URL}/v{cls.PASLS_VERSION}/pasls-win32-x64.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="pasls.exe",
),
]
)
pasls_dir = cls.ls_resources_dir(solidlsp_settings)
pasls_executable_path = deps.binary_path(pasls_dir)
if not os.path.exists(pasls_executable_path):
log.info(f"Downloading pasls to {pasls_dir}...")
deps.install(pasls_dir)
assert os.path.exists(pasls_executable_path), f"pasls executable not found at {pasls_executable_path}"
os.chmod(pasls_executable_path, 0o755)
log.info(f"Using pasls at: {pasls_executable_path}")
return quote_windows_path(pasls_executable_path)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Pascal Language Server.
pasls (genericptr/pascal-language-server) reads compiler paths from:
1. Environment variables (PP, FPCDIR, LAZARUSDIR) via TCodeToolsOptions.InitWithEnvironmentVariables
2. Lazarus config files via GuessCodeToolConfig
We only pass target OS/CPU in initializationOptions if explicitly set.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# Build initializationOptions from environment variables
# pasls reads these to configure CodeTools:
# - PP: Path to FPC compiler executable
# - FPCDIR: Path to FPC source directory
# - LAZARUSDIR: Path to Lazarus directory (only needed for LCL projects)
# - FPCTARGET: Target OS
# - FPCTARGETCPU: Target CPU
initialization_options: dict = {}
env_vars = ["PP", "FPCDIR", "LAZARUSDIR", "FPCTARGET", "FPCTARGETCPU"]
for var in env_vars:
value = os.environ.get(var, "")
if value:
initialization_options[var] = value
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": True,
"dynamicRegistration": True,
"willSave": True,
"willSaveWaitUntil": True,
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
},
},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentHighlight": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"codeAction": {
"dynamicRegistration": True,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
},
"formatting": {"dynamicRegistration": True},
"rangeFormatting": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
"executeCommand": {"dynamicRegistration": True},
"configuration": True,
"workspaceEdit": {
"documentChanges": True,
},
},
},
"initializationOptions": initialization_options,
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
def _start_server(self) -> None:
"""
Starts the Pascal Language Server, waits for the server to be ready and yields the LanguageServer instance.
"""
def register_capability_handler(params: dict) -> None:
log.debug(f"Capability registered: {params}")
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
# Mark server as ready when we see initialization messages
message_text = msg.get("message", "")
if "initialized" in message_text.lower() or "ready" in message_text.lower():
log.info("Pascal language server ready signal detected")
self.server_ready.set()
self.completions_available.set()
def publish_diagnostics(params: dict) -> None:
log.debug(f"Diagnostics: {params}")
return
def do_nothing(params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("window/showMessage", window_log_message)
self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
self.server.on_notification("$/progress", do_nothing)
log.info("Starting Pascal server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
log.debug(f"Received initialize response from Pascal server: {init_response}")
# Verify capabilities
capabilities = init_response.get("capabilities", {})
assert "textDocumentSync" in capabilities
# Check for various capabilities
if "completionProvider" in capabilities:
log.info("Pascal server supports code completion")
if "definitionProvider" in capabilities:
log.info("Pascal server supports go to definition")
if "referencesProvider" in capabilities:
log.info("Pascal server supports find references")
if "documentSymbolProvider" in capabilities:
log.info("Pascal server supports document symbols")
self.server.notify.initialized({})
# Wait for server readiness with timeout
log.info("Waiting for Pascal language server to be ready...")
if not self.server_ready.wait(timeout=5.0):
# pasls may not send explicit ready signals, so we proceed after timeout
log.info("Timeout waiting for Pascal server ready signal, assuming server is ready")
self.server_ready.set()
self.completions_available.set()
else:
log.info("Pascal server initialization complete")
def is_ignored_dirname(self, dirname: str) -> bool:
"""
Check if a directory should be ignored for Pascal projects.
Common Pascal/Lazarus directories to ignore.
"""
ignored_dirs = {
"lib",
"backup",
"__history",
"__recovery",
"bin",
".git",
".svn",
".hg",
"node_modules",
}
return dirname.lower() in ignored_dirs
+11
View File
@@ -64,6 +64,11 @@ class Language(str, Enum):
GROOVY = "groovy"
VUE = "vue"
POWERSHELL = "powershell"
PASCAL = "pascal"
"""Pascal Language Server (pasls) for Free Pascal and Lazarus projects.
Automatically downloads pasls binary. Requires FPC for full functionality.
Set PP and FPCDIR environment variables for source navigation.
"""
MATLAB = "matlab"
"""MATLAB language server using the official MathWorks MATLAB Language Server.
Requires MATLAB R2021b or later and Node.js.
@@ -228,6 +233,8 @@ class Language(str, Enum):
return FilenameMatcher(*path_patterns)
case self.POWERSHELL:
return FilenameMatcher("*.ps1", "*.psm1", "*.psd1")
case self.PASCAL:
return FilenameMatcher("*.pas", "*.pp", "*.lpr", "*.dpr", "*.dpk", "*.inc")
case self.GROOVY:
return FilenameMatcher("*.groovy", "*.gvy")
case self.MATLAB:
@@ -393,6 +400,10 @@ class Language(str, Enum):
from solidlsp.language_servers.powershell_language_server import PowerShellLanguageServer
return PowerShellLanguageServer
case self.PASCAL:
from solidlsp.language_servers.pascal_server import PascalLanguageServer
return PascalLanguageServer
case self.GROOVY:
from solidlsp.language_servers.groovy_language_server import GroovyLanguageServer
@@ -0,0 +1,8 @@
backup/
*.o
*.ppu
*.exe
*.lps
*.compiled
__history/
__recovery/
@@ -0,0 +1,54 @@
unit Helper;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils;
type
{ THelper - A helper class with utility functions }
THelper = class
public
class function FormatString(const Template: string; const Args: array of const): string;
class function IsEven(Number: Integer): Boolean;
end;
{ Standalone helper functions }
function GetHelperMessage: string;
function MultiplyNumbers(A, B: Integer): Integer;
procedure LogMessage(const Msg: string);
implementation
{ THelper }
class function THelper.FormatString(const Template: string; const Args: array of const): string;
begin
Result := Format(Template, Args);
end;
class function THelper.IsEven(Number: Integer): Boolean;
begin
Result := (Number mod 2) = 0;
end;
{ Standalone functions }
function GetHelperMessage: string;
begin
Result := 'Hello from Helper unit!';
end;
function MultiplyNumbers(A, B: Integer): Integer;
begin
Result := A * B;
end;
procedure LogMessage(const Msg: string);
begin
WriteLn('[LOG] ', Msg);
end;
end.
@@ -0,0 +1,130 @@
unit Main;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Helper;
type
{ TUser - A simple user class }
TUser = class
private
FName: string;
FAge: Integer;
public
constructor Create(const AName: string; AAge: Integer);
destructor Destroy; override;
function GetInfo: string;
procedure UpdateAge(NewAge: Integer);
property Name: string read FName write FName;
property Age: Integer read FAge write FAge;
end;
{ TUserManager - Manages multiple users }
TUserManager = class
private
FUsers: TList;
public
constructor Create;
destructor Destroy; override;
procedure AddUser(User: TUser);
function GetUserCount: Integer;
function FindUserByName(const AName: string): TUser;
end;
{ Helper functions }
/// Calculates the sum of two integers.
/// @param A First integer value
/// @param B Second integer value
/// @returns The sum of A and B
function CalculateSum(A, B: Integer): Integer;
procedure PrintMessage(const Msg: string);
implementation
{ TUser implementation }
constructor TUser.Create(const AName: string; AAge: Integer);
begin
inherited Create;
FName := AName;
FAge := AAge;
end;
destructor TUser.Destroy;
begin
inherited Destroy;
end;
function TUser.GetInfo: string;
begin
Result := Format('Name: %s, Age: %d', [FName, FAge]);
end;
procedure TUser.UpdateAge(NewAge: Integer);
begin
FAge := NewAge;
end;
{ TUserManager implementation }
constructor TUserManager.Create;
begin
inherited Create;
FUsers := TList.Create;
end;
destructor TUserManager.Destroy;
var
i: Integer;
begin
for i := 0 to FUsers.Count - 1 do
TUser(FUsers[i]).Free;
FUsers.Free;
inherited Destroy;
end;
procedure TUserManager.AddUser(User: TUser);
begin
FUsers.Add(User);
end;
function TUserManager.GetUserCount: Integer;
begin
Result := FUsers.Count;
end;
function TUserManager.FindUserByName(const AName: string): TUser;
var
i: Integer;
begin
Result := nil;
for i := 0 to FUsers.Count - 1 do
begin
if TUser(FUsers[i]).Name = AName then
begin
Result := TUser(FUsers[i]);
Exit;
end;
end;
end;
{ Helper functions }
function CalculateSum(A, B: Integer): Integer;
begin
Result := A + B;
end;
procedure PrintMessage(const Msg: string);
begin
WriteLn(Msg);
end;
end.
+15
View File
@@ -0,0 +1,15 @@
def _check_pascal_available() -> bool:
"""Check if Pascal language server (pasls) is available.
Note: pasls will be auto-downloaded if not present, so Pascal
support is always available.
"""
return True
PASCAL_AVAILABLE = _check_pascal_available()
def is_pascal_available() -> bool:
"""Return True if Pascal language server can be used."""
return PASCAL_AVAILABLE
+195
View File
@@ -0,0 +1,195 @@
"""
Basic integration tests for the Pascal language server functionality.
These tests validate the functionality of the language server APIs
like request_document_symbols using the Pascal test repository.
Uses genericptr/pascal-language-server which returns SymbolInformation[] format:
- Returns classes, structs, enums, typedefs, functions/procedures
- Uses correct SymbolKind values: Class=5, Function=12, Method=6, Struct=23
- Method names don't include parent class prefix; uses containerName instead
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
from test.conftest import language_tests_enabled
pytestmark = [
pytest.mark.pascal,
pytest.mark.skipif(not language_tests_enabled(Language.PASCAL), reason="Pascal tests are disabled (pasls/fpc not available)"),
]
@pytest.mark.pascal
class TestPascalLanguageServerBasics:
"""Test basic functionality of the Pascal language server."""
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
"""Test that Pascal language server can be initialized successfully."""
assert language_server is not None
assert language_server.language == Language.PASCAL
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test request_document_symbols for Pascal files.
genericptr pasls returns proper SymbolKind values:
- Standalone functions: kind=12 (Function)
- Classes: kind=5 (Class)
"""
# Test getting symbols from main.pas
all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
# Should have symbols
assert len(all_symbols) > 0, "Should have symbols in main.pas"
# Should detect standalone functions (SymbolKind.Function = 12)
function_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Function]
function_names = [s["name"] for s in function_symbols]
assert "CalculateSum" in function_names, "Should find CalculateSum function"
assert "PrintMessage" in function_names, "Should find PrintMessage procedure"
# Should detect classes (SymbolKind.Class = 5)
class_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Class]
class_names = [s["name"] for s in class_symbols]
assert "TUser" in class_names, "Should find TUser class"
assert "TUserManager" in class_names, "Should find TUserManager class"
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_class_methods(self, language_server: SolidLanguageServer) -> None:
"""Test detection of class methods in Pascal files.
pasls returns class methods with SymbolKind.Method (kind 6), not Function (kind 12).
"""
all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
# Get all method symbols (pasls returns class methods as SymbolKind.Method = 6)
method_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Method]
method_names = [s["name"] for s in method_symbols]
# Should detect TUser methods
expected_tuser_methods = ["Create", "Destroy", "GetInfo", "UpdateAge"]
for method in expected_tuser_methods:
found = method in method_names
assert found, f"Should find method '{method}'"
# Should detect TUserManager methods
expected_manager_methods = ["Create", "Destroy", "AddUser", "GetUserCount", "FindUserByName"]
for method in expected_manager_methods:
found = method in method_names
assert found, f"Should find method '{method}'"
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_helper_unit_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test function detection in Helper unit."""
# Test with lib/helper.pas
helper_all_symbols, _helper_root_symbols = language_server.request_document_symbols("lib/helper.pas").get_all_symbols_and_roots()
# Should have symbols
assert len(helper_all_symbols) > 0, "Helper unit should have symbols"
# Extract function symbols
function_symbols = [s for s in helper_all_symbols if s.get("kind") == SymbolKind.Function]
function_names = [s["name"] for s in function_symbols]
# Should detect standalone functions
expected_functions = ["GetHelperMessage", "MultiplyNumbers", "LogMessage"]
for func_name in expected_functions:
assert func_name in function_names, f"Should find {func_name} function in Helper unit"
# Should also detect THelper class methods (returned as SymbolKind.Method = 6)
method_symbols = [s for s in helper_all_symbols if s.get("kind") == SymbolKind.Method]
method_names = [s["name"] for s in method_symbols]
assert "FormatString" in method_names, "Should find FormatString method"
assert "IsEven" in method_names, "Should find IsEven method"
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_cross_file_references(self, language_server: SolidLanguageServer) -> None:
"""Test that Pascal LSP can handle cross-file references."""
# main.pas uses Helper unit
main_symbols, _main_roots = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
helper_symbols, _helper_roots = language_server.request_document_symbols("lib/helper.pas").get_all_symbols_and_roots()
# Verify both files have symbols
assert len(main_symbols) > 0, "main.pas should have symbols"
assert len(helper_symbols) > 0, "helper.pas should have symbols"
# Verify GetHelperMessage is in Helper unit
helper_function_names = [s["name"] for s in helper_symbols if s.get("kind") == SymbolKind.Function]
assert "GetHelperMessage" in helper_function_names, "Helper unit should export GetHelperMessage"
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_symbol_locations(self, language_server: SolidLanguageServer) -> None:
"""Test that symbols have correct location information.
Note: genericptr pasls returns the interface declaration location (line ~41),
not the implementation location (line ~115).
"""
all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
# Find CalculateSum function
calc_symbols = [s for s in all_symbols if s.get("name") == "CalculateSum"]
assert len(calc_symbols) > 0, "Should find CalculateSum"
calc_symbol = calc_symbols[0]
# Verify it has location information (SymbolInformation format uses location.range)
if "location" in calc_symbol:
location = calc_symbol["location"]
assert "range" in location, "Location should have range"
assert "start" in location["range"], "Range should have start"
assert "line" in location["range"]["start"], "Start should have line"
line = location["range"]["start"]["line"]
else:
# DocumentSymbol format uses range directly
assert "range" in calc_symbol, "Symbol should have range"
assert "start" in calc_symbol["range"], "Range should have start"
line = calc_symbol["range"]["start"]["line"]
# CalculateSum is declared at line 41 in main.pas (0-indexed would be 40)
# genericptr pasls returns interface declaration location
assert 35 <= line <= 45, f"CalculateSum should be around line 41 (interface), got {line}"
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_namespace_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test that genericptr pasls returns Interface namespace symbol."""
all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
# genericptr pasls adds an "Interface" namespace symbol
symbol_names = [s["name"] for s in all_symbols]
# The Interface section should be represented
# Note: This depends on pasls configuration
assert len(all_symbols) > 0, "Should have symbols"
# Interface namespace may or may not be present depending on pasls configuration
_ = symbol_names # used for potential future assertions
@pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
def test_pascal_hover_with_doc_comments(self, language_server: SolidLanguageServer) -> None:
"""Test that hover returns documentation comments.
CalculateSum has /// style doc comments that should appear in hover.
"""
# CalculateSum is declared at line 46 (1-indexed), so line 45 (0-indexed)
hover = language_server.request_hover("main.pas", 45, 12)
assert hover is not None, "Hover should return a result"
# Extract hover content - handle both dict and object formats
if isinstance(hover, dict):
contents = hover.get("contents", {})
value = contents.get("value", "") if isinstance(contents, dict) else str(contents)
else:
value = hover.contents.value if hasattr(hover.contents, "value") else str(hover.contents)
# Should contain the function signature
assert "CalculateSum" in value, f"Hover should show function name. Got: {value[:500]}"
# Should contain the doc comment
assert "Calculates the sum" in value, f"Hover should include doc comment. Got: {value[:500]}"