Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/multilspy/language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write this configuration to a separate json file in the same directory, and read from there?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other language server implementations for example

"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()
3 changes: 2 additions & 1 deletion src/multilspy/multilspy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Language(str, Enum):
GO = "go"
RUBY = "ruby"
DART = "dart"
PERL = "perl"

def __str__(self) -> str:
return self.value
Expand All @@ -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
})
})
45 changes: 45 additions & 0 deletions tests/multilspy/test_multilspy_perl.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add actual expected values instead of placeholders?


# 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
41 changes: 41 additions & 0 deletions tests/multilspy/test_sync_multilspy_perl.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

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
Loading