diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 622b6c4b..0d654c52 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -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 diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index a748505e..b2f1e58b 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 53bb740d..375bbda5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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+)", diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 362dc7f4..3ae11f41 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -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. diff --git a/src/solidlsp/language_servers/pascal_server.py b/src/solidlsp/language_servers/pascal_server.py new file mode 100644 index 00000000..bb1c9e13 --- /dev/null +++ b/src/solidlsp/language_servers/pascal_server.py @@ -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 diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 7489b4f9..04627009 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -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 diff --git a/test/resources/repos/pascal/test_repo/.gitignore b/test/resources/repos/pascal/test_repo/.gitignore new file mode 100644 index 00000000..42e0384a --- /dev/null +++ b/test/resources/repos/pascal/test_repo/.gitignore @@ -0,0 +1,8 @@ +backup/ +*.o +*.ppu +*.exe +*.lps +*.compiled +__history/ +__recovery/ diff --git a/test/resources/repos/pascal/test_repo/lib/helper.pas b/test/resources/repos/pascal/test_repo/lib/helper.pas new file mode 100644 index 00000000..71c772bf --- /dev/null +++ b/test/resources/repos/pascal/test_repo/lib/helper.pas @@ -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. diff --git a/test/resources/repos/pascal/test_repo/main.pas b/test/resources/repos/pascal/test_repo/main.pas new file mode 100644 index 00000000..f240bee2 --- /dev/null +++ b/test/resources/repos/pascal/test_repo/main.pas @@ -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. diff --git a/test/solidlsp/pascal/__init__.py b/test/solidlsp/pascal/__init__.py new file mode 100644 index 00000000..b9d1f3df --- /dev/null +++ b/test/solidlsp/pascal/__init__.py @@ -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 diff --git a/test/solidlsp/pascal/test_pascal_basic.py b/test/solidlsp/pascal/test_pascal_basic.py new file mode 100644 index 00000000..d926207a --- /dev/null +++ b/test/solidlsp/pascal/test_pascal_basic.py @@ -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]}"