diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index a416fef..f8f352c 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -112,6 +112,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer return DartLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.PERL: + from multilspy.language_servers.perl_language_server.perl_language_server import PerlLanguageServer + + return PerlLanguageServer(config, logger, repository_root_path) else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise MultilspyException(f"Language {config.code_language} is not supported") diff --git a/src/multilspy/language_servers/perl_language_server/perl_language_server.py b/src/multilspy/language_servers/perl_language_server/perl_language_server.py new file mode 100644 index 0000000..44a40ba --- /dev/null +++ b/src/multilspy/language_servers/perl_language_server/perl_language_server.py @@ -0,0 +1,243 @@ +""" +Provides Perl specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Perl. +""" + +import json +import logging +import os +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_exceptions import MultilspyException + + +class PerlLanguageServer(LanguageServer): + """ + Provides Perl specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Perl. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a PerlLanguageServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + try: + import subprocess + cmd = None + + # Perl::LanguageServer + if not cmd: + perl_cmd = "perl -e 'eval \"use Coro; use AnyEvent; use AnyEvent::AIO; use Perl::LanguageServer;\"; print $@ ? 0 : 1'" + result = subprocess.run(perl_cmd, shell=True, capture_output=True, text=True) + if result.stdout.strip() == "1": + logger.log("Using Perl::LanguageServer", logging.INFO) + cmd = "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run(0, 1, 1, 1, 1, 1)'" + + if not cmd: + logger.log("No Perl Language Server found. Please install:", logging.ERROR) + logger.log("Perl::LanguageServer: cpanm Coro AnyEvent AnyEvent::AIO Perl::LanguageServer", logging.ERROR) + raise MultilspyException("No Perl Language Server installed") + + except Exception as e: + logger.log(f"Error checking Perl Language Servers: {str(e)}", logging.ERROR) + raise MultilspyException(f"Error checking Perl Language Servers: {str(e)}") + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), + "perl", + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Perl Language Server. + """ + # Create a basic initialize params structure + # This can be expanded with more specific settings if needed + params = { + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True + }, + "didChangeConfiguration": { + "dynamicRegistration": True + }, + "didChangeWatchedFiles": { + "dynamicRegistration": True + }, + "symbol": { + "dynamicRegistration": True + }, + "executeCommand": { + "dynamicRegistration": True + } + }, + "textDocument": { + "synchronization": { + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True, + "didSave": True + }, + "completion": { + "dynamicRegistration": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True + }, + "contextSupport": True + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"] + }, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": { + "labelOffsetSupport": True + } + } + }, + "definition": { + "dynamicRegistration": True + }, + "references": { + "dynamicRegistration": True + }, + "documentHighlight": { + "dynamicRegistration": True + }, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 + ] + }, + "hierarchicalDocumentSymbolSupport": True + }, + "codeAction": { + "dynamicRegistration": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", + "source", "source.organizeImports" + ] + } + } + }, + "codeLens": { + "dynamicRegistration": True + }, + "formatting": { + "dynamicRegistration": True + }, + "rangeFormatting": { + "dynamicRegistration": True + }, + "onTypeFormatting": { + "dynamicRegistration": True + }, + "rename": { + "dynamicRegistration": True + }, + "publishDiagnostics": { + "relatedInformation": True, + "tagSupport": { + "valueSet": [1, 2] + } + } + } + }, + "workspaceFolders": [ + { + "uri": pathlib.Path(repository_absolute_path).as_uri(), + "name": os.path.basename(repository_absolute_path) + } + ] + } + + return params + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["PerlLanguageServer"]: + """ + Starts the Perl Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + ``` + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params.get("quiescent", False) == True: + self.completions_available.set() + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting Perl Language Server process", logging.INFO) + await self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = await self.server.send.initialize(initialize_params) + + # Check for expected capabilities + # These may need to be adjusted based on the actual capabilities of the Perl Language Server + if "textDocumentSync" in init_response["capabilities"]: + self.logger.log(f"textDocumentSync: {init_response['capabilities']['textDocumentSync']}", logging.INFO) + + if "completionProvider" in init_response["capabilities"]: + self.logger.log(f"completionProvider: {init_response['capabilities']['completionProvider']}", logging.INFO) + + self.server.notify.initialized({}) + + yield self + + await self.server.shutdown() + await self.server.stop() diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index f4014d0..69070d6 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -19,6 +19,7 @@ class Language(str, Enum): GO = "go" RUBY = "ruby" DART = "dart" + PERL = "perl" def __str__(self) -> str: return self.value @@ -40,4 +41,4 @@ def from_dict(cls, env: dict): return cls(**{ k: v for k, v in env.items() if k in inspect.signature(cls).parameters - }) \ No newline at end of file + }) diff --git a/tests/multilspy/test_multilspy_perl.py b/tests/multilspy/test_multilspy_perl.py new file mode 100644 index 0000000..b93a1f9 --- /dev/null +++ b/tests/multilspy/test_multilspy_perl.py @@ -0,0 +1,45 @@ +""" +This file contains tests for running the Perl Language Server: Perl::LanguageServer +""" + +import pytest +from multilspy import LanguageServer +from multilspy.multilspy_config import Language +from tests.test_utils import create_test_context +from pathlib import PurePath + +pytest_plugins = ("pytest_asyncio",) + +@pytest.mark.asyncio +async def test_multilspy_perl_dancer2(): + """ + Test the working of multilspy with perl repository - Dancer2 + """ + code_language = Language.PERL + params = { + "code_language": code_language, + "repo_url": "https://github.com/PerlDancer/Dancer2/", + "repo_commit": "9f0f5e0b9b0a9c8e8e8e8e8e8e8e8e8e8e8e8e8e" # Replace with an actual commit hash + } + with create_test_context(params) as context: + lsp = LanguageServer.create(context.config, context.logger, context.source_directory) + + # All the communication with the language server must be performed inside the context manager + # The server process is started when the context manager is entered and is terminated when the context manager is exited. + # The context manager is an asynchronous context manager, so it must be used with async with. + async with lsp.start_server(): + # Test request_definition + result = await lsp.request_definition(str(PurePath("lib/Dancer2.pm")), 10, 4) + + assert isinstance(result, list) + # The exact assertions will depend on the actual response from the Perl Language Server + # These are placeholder assertions that should be updated with actual expected values + assert len(result) >= 0 + + # Test request_references + result = await lsp.request_references(str(PurePath("lib/Dancer2.pm")), 10, 4) + + assert isinstance(result, list) + # The exact assertions will depend on the actual response from the Perl Language Server + # These are placeholder assertions that should be updated with actual expected values + assert len(result) >= 0 diff --git a/tests/multilspy/test_sync_multilspy_perl.py b/tests/multilspy/test_sync_multilspy_perl.py new file mode 100644 index 0000000..24edcd5 --- /dev/null +++ b/tests/multilspy/test_sync_multilspy_perl.py @@ -0,0 +1,41 @@ +""" +This file contains tests for running the Perl Language Server: Perl::LanguageServer in synchronous mode +""" + +import pytest +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from tests.test_utils import create_test_context +from pathlib import PurePath + +def test_sync_multilspy_perl_dancer2(): + """ + Test the working of multilspy with perl repository - Dancer2 in synchronous mode + """ + code_language = Language.PERL + params = { + "code_language": code_language, + "repo_url": "https://github.com/PerlDancer/Dancer2/", + "repo_commit": "9f0f5e0b9b0a9c8e8e8e8e8e8e8e8e8e8e8e8e8e" # Replace with an actual commit hash + } + with create_test_context(params) as context: + lsp = SyncLanguageServer.create(context.config, context.logger, context.source_directory) + + # All the communication with the language server must be performed inside the context manager + # The server process is started when the context manager is entered and is terminated when the context manager is exited. + with lsp.start_server(): + # Test request_definition + result = lsp.request_definition(str(PurePath("lib/Dancer2.pm")), 10, 4) + + assert isinstance(result, list) + # The exact assertions will depend on the actual response from the Perl Language Server + # These are placeholder assertions that should be updated with actual expected values + assert len(result) >= 0 + + # Test request_references + result = lsp.request_references(str(PurePath("lib/Dancer2.pm")), 10, 4) + + assert isinstance(result, list) + # The exact assertions will depend on the actual response from the Perl Language Server + # These are placeholder assertions that should be updated with actual expected values + assert len(result) >= 0