mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-04 22:13:47 +00:00
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:
1 parent
eb54e834b6
commit
fa012910cb
11 files changed
+800
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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+)",
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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]}"
|
||||
Reference in new issue
Block a user