From 7b85f39636e1971b0e9e0b9e69f4f8d7be51f931 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:25:02 -0300 Subject: [PATCH 01/15] chore: undo styling changes --- src/multilspy/language_server.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 665d39c..8ccc0d6 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -27,7 +27,7 @@ from .multilspy_exceptions import MultilspyException from .multilspy_utils import PathUtils, FileUtils, TextUtils from pathlib import PurePath -from typing import AsyncIterator, Iterator, List, Dict, Union, Tuple +from typing import AsyncIterator, Iterator, List, Dict, Optional, Union, Tuple from .type_helpers import ensure_all_methods_implemented @@ -656,14 +656,15 @@ class SyncLanguageServer: It is used to communicate with Language Servers of different programming languages. """ - def __init__(self, language_server: LanguageServer) -> None: + def __init__(self, language_server: LanguageServer, timeout: Optional[int] = None): self.language_server = language_server self.loop = None self.loop_thread = None + self.timeout = timeout @classmethod def create( - cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str + cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: Optional[int] = None ) -> "SyncLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -676,7 +677,7 @@ def create( :return SyncLanguageServer: A language specific LanguageServer instance. """ - return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path)) + return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path), timeout=timeout) @contextmanager def open_file(self, relative_file_path: str) -> Iterator[None]: @@ -751,7 +752,7 @@ def request_definition(self, file_path: str, line: int, column: int) -> List[mul """ result = asyncio.run_coroutine_threadsafe( self.language_server.request_definition(file_path, line, column), self.loop - ).result() + ).result(timeout=self.timeout) return result def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: @@ -767,7 +768,7 @@ def request_references(self, file_path: str, line: int, column: int) -> List[mul """ result = asyncio.run_coroutine_threadsafe( self.language_server.request_references(file_path, line, column), self.loop - ).result() + ).result(timeout=self.timeout) return result def request_completions( @@ -786,7 +787,7 @@ def request_completions( result = asyncio.run_coroutine_threadsafe( self.language_server.request_completions(relative_file_path, line, column, allow_incomplete), self.loop, - ).result() + ).result(timeout=self.timeout) return result def request_document_symbols(self, relative_file_path: str) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], Union[List[multilspy_types.TreeRepr], None]]: @@ -800,7 +801,7 @@ def request_document_symbols(self, relative_file_path: str) -> Tuple[List[multil """ result = asyncio.run_coroutine_threadsafe( self.language_server.request_document_symbols(relative_file_path), self.loop - ).result() + ).result(timeout=self.timeout) return result def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: @@ -816,5 +817,5 @@ def request_hover(self, relative_file_path: str, line: int, column: int) -> Unio """ result = asyncio.run_coroutine_threadsafe( self.language_server.request_hover(relative_file_path, line, column), self.loop - ).result() + ).result(timeout=self.timeout) return result From 04a86cb85f208d3cdd7040f046d402633374450a Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:39:20 -0300 Subject: [PATCH 02/15] feat: added test for timeout --- .../multilspy/test_sync_multilspy_timeout.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/multilspy/test_sync_multilspy_timeout.py diff --git a/tests/multilspy/test_sync_multilspy_timeout.py b/tests/multilspy/test_sync_multilspy_timeout.py new file mode 100644 index 0000000..be4f14a --- /dev/null +++ b/tests/multilspy/test_sync_multilspy_timeout.py @@ -0,0 +1,37 @@ +""" +This file contains tests for running the Python Language Server: jedi-language-server +""" + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from tests.test_utils import create_test_context +from pathlib import PurePath +import time + +def test_multilspy_timeout() -> None: + """ + Test timeout error in multilspy + """ + code_language = Language.PYTHON + params = { + "code_language": code_language, + "repo_url": "https://github.com/psf/black/", + "repo_commit": "f3b50e466969f9142393ec32a4b2a383ffbe5f23" + } + with create_test_context(params) as context: + lsp = SyncLanguageServer.create(context.config, context.logger, context.source_directory, timeout=1) + + # Mock the request_definition method to simulate a long running process + async def request_definition(*args, **kwargs): + time.sleep(5) + return [] + + lsp.language_server.request_definition = request_definition + + with lsp.start_server(): + try: + lsp.request_definition(str(PurePath("src/black/mode.py")), 163, 4) + except TimeoutError as e: + assert True, f"TimeoutError: {e}" + except Exception as e: + assert False, f"Unexpected error: {e}" From 90d535417de0109c72b48fc05f1e41a75512d498 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:32:38 -0300 Subject: [PATCH 03/15] test: improve timeout error handling in multilspy tests --- tests/multilspy/test_sync_multilspy_timeout.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/multilspy/test_sync_multilspy_timeout.py b/tests/multilspy/test_sync_multilspy_timeout.py index be4f14a..3b7dd4e 100644 --- a/tests/multilspy/test_sync_multilspy_timeout.py +++ b/tests/multilspy/test_sync_multilspy_timeout.py @@ -2,6 +2,7 @@ This file contains tests for running the Python Language Server: jedi-language-server """ +import pytest from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language from tests.test_utils import create_test_context @@ -29,9 +30,5 @@ async def request_definition(*args, **kwargs): lsp.language_server.request_definition = request_definition with lsp.start_server(): - try: + with pytest.raises(TimeoutError): lsp.request_definition(str(PurePath("src/black/mode.py")), 163, 4) - except TimeoutError as e: - assert True, f"TimeoutError: {e}" - except Exception as e: - assert False, f"Unexpected error: {e}" From 11c0da5450d0ce032b5ea832561e87ead6a0ca5b Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:43:32 -0300 Subject: [PATCH 04/15] Add psutil dependency and enhance process termination handling --- pyproject.toml | 3 ++- src/multilspy/lsp_protocol_handler/server.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0c66b92..2bdc15c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ classifiers = [ dependencies = [ "jedi-language-server==0.41.1", "requests==2.32.3", - "typing-extensions>=4.2.0" + "typing-extensions>=4.2.0", + "psutil (>=7.0.0,<8.0.0)" ] [project.urls] diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index a408686..865c2f0 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -32,6 +32,7 @@ import dataclasses import json import os +import psutil from typing import Any, Dict, List, Optional, Union from .lsp_requests import LspNotification, LspRequest @@ -241,6 +242,10 @@ async def stop(self) -> None: except asyncio.TimeoutError: process.kill() + for child in psutil.Process(process.pid).children(recursive=True): + child.kill() + + async def shutdown(self) -> None: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit From 0a0ff959b5ba05396cdc70365c3f38914a102f84 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:05:03 -0300 Subject: [PATCH 05/15] feat: add Dart language support and corresponding tests --- src/multilspy/language_server.py | 4 + .../dart_language_server.py | 120 ++++++++++++++++++ .../initialize_params.json | 14 ++ src/multilspy/multilspy_config.py | 1 + tests/multilspy/test_multilspy_dart.py | 76 +++++++++++ tests/multilspy/test_sync_multilspy_dart.py | 63 +++++++++ 6 files changed, 278 insertions(+) create mode 100644 src/multilspy/language_servers/dart_language_server/dart_language_server.py create mode 100644 src/multilspy/language_servers/dart_language_server/initialize_params.json create mode 100644 tests/multilspy/test_multilspy_dart.py create mode 100644 tests/multilspy/test_sync_multilspy_dart.py diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 665d39c..1f72e24 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -108,6 +108,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo from multilspy.language_servers.solargraph.solargraph import Solargraph return Solargraph(config, logger, repository_root_path) + elif config.code_language == Language.DART: + from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer + + return DartLanguageServer(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/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py new file mode 100644 index 0000000..09026f1 --- /dev/null +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -0,0 +1,120 @@ +from contextlib import asynccontextmanager +import logging +import os +import pathlib +import shutil +from typing import AsyncIterator +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +import json + + +class DartLanguageServer(LanguageServer): + """ + Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. + """ + + def __init__(self, config, logger, repository_root_path): + """ + Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + + executable_path = self.setup_runtime_dependencies() + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), + "dart", + ) + + def setup_runtime_dependencies(self): + dart_executable_path = shutil.which("dart") + assert dart_executable_path, "Dart executable not found in PATH" + + return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" + + + def _get_initialize_params(self, repository_absolute_path: str): + """ + Returns the initialize params for the Dart Language Server. + """ + with open( + os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" + ) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path( + repository_absolute_path + ).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["DartLanguageServer"]: + """ + Start the language server and yield when the server is ready. + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + pass + + 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 dart-language-server server process", logging.INFO + ) + await self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log( + "Sending initialize request to dart-language-server", + logging.DEBUG, + ) + init_response = await self.server.send_request( + "initialize", initialize_params + ) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) + + self.server.notify.initialized({}) + + yield self + + await self.server.shutdown() + await self.server.stop() diff --git a/src/multilspy/language_servers/dart_language_server/initialize_params.json b/src/multilspy/language_servers/dart_language_server/initialize_params.json new file mode 100644 index 0000000..bb8d695 --- /dev/null +++ b/src/multilspy/language_servers/dart_language_server/initialize_params.json @@ -0,0 +1,14 @@ +{ + "_description": "This file contains the initialization parameters for the Dart Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": {}, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 3628e9c..f4014d0 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -18,6 +18,7 @@ class Language(str, Enum): JAVASCRIPT = "javascript" GO = "go" RUBY = "ruby" + DART = "dart" def __str__(self) -> str: return self.value diff --git a/tests/multilspy/test_multilspy_dart.py b/tests/multilspy/test_multilspy_dart.py new file mode 100644 index 0000000..cb20a7f --- /dev/null +++ b/tests/multilspy/test_multilspy_dart.py @@ -0,0 +1,76 @@ +""" +This file contains tests for running the Dart Language Server. +""" + +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_dart(): + """ + Test the working of multilspy with a Dart repository. + """ + code_language = Language.DART + params = { + "code_language": code_language, + "repo_url": "https://github.com/simonoppowa/OpenNutriTracker/", + "repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805", + } + with create_test_context(params) as context: + lsp = LanguageServer.create( + context.config, context.logger, context.source_directory + ) + + async with lsp.start_server(): + result = await lsp.request_document_symbols( + str( + PurePath("lib/core/presentation/widgets/copy_or_delete_dialog.dart") + ) + ) + + assert isinstance(result, tuple) + assert len(result) == 2 + + symbols = result[0] + for symbol in symbols: + del symbol["deprecated"] + del symbol["kind"] + del symbol["location"]["uri"] + + assert symbols == [ + { + "location": { + "range": { + "end": {"character": 24, "line": 3}, + "start": {"character": 6, "line": 3}, + }, + }, + "name": "CopyOrDeleteDialog", + }, + { + "containerName": "CopyOrDeleteDialog", + "location": { + "range": { + "end": {"character": 26, "line": 4}, + "start": {"character": 8, "line": 4}, + }, + }, + "name": "CopyOrDeleteDialog", + }, + { + "containerName": "CopyOrDeleteDialog", + "location": { + "range": { + "end": {"character": 14, "line": 7}, + "start": {"character": 9, "line": 7}, + }, + }, + "name": "build", + }, + ] diff --git a/tests/multilspy/test_sync_multilspy_dart.py b/tests/multilspy/test_sync_multilspy_dart.py new file mode 100644 index 0000000..fcad8d3 --- /dev/null +++ b/tests/multilspy/test_sync_multilspy_dart.py @@ -0,0 +1,63 @@ +""" +This file contains tests for running the Python Language Server: jedi-language-server +""" + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from tests.test_utils import create_test_context +from pathlib import PurePath + + +def test_multilspy_python_black() -> None: + """ + Test the working of multilspy with python repository - black + """ + code_language = Language.DART + params = { + "code_language": code_language, + "repo_url": "https://github.com/simonoppowa/OpenNutriTracker/", + "repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805", + } + 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(): + result = lsp.request_references( + file_path=str( + PurePath("lib/features/add_meal/presentation/add_meal_screen.dart") + ), + line=19, + column=5, + ) + + for item in result: + del item["uri"] + del item["absolutePath"] + + assert result == [ + { + "range": { + "end": {"character": 21, "line": 20}, + "start": {"character": 8, "line": 20}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + { + "range": { + "end": {"character": 21, "line": 23}, + "start": {"character": 8, "line": 23}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + { + "range": { + "end": {"character": 53, "line": 26}, + "start": {"character": 40, "line": 26}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + ] From e193501fb605ba86d52513f9fe044c2576bfc4cd Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:05:03 -0300 Subject: [PATCH 06/15] feat: add Dart language support and corresponding tests --- src/multilspy/language_server.py | 4 + .../dart_language_server.py | 120 ++++++++++++++++++ .../initialize_params.json | 14 ++ src/multilspy/multilspy_config.py | 1 + tests/multilspy/test_multilspy_dart.py | 76 +++++++++++ tests/multilspy/test_sync_multilspy_dart.py | 63 +++++++++ 6 files changed, 278 insertions(+) create mode 100644 src/multilspy/language_servers/dart_language_server/dart_language_server.py create mode 100644 src/multilspy/language_servers/dart_language_server/initialize_params.json create mode 100644 tests/multilspy/test_multilspy_dart.py create mode 100644 tests/multilspy/test_sync_multilspy_dart.py diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 665d39c..1f72e24 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -108,6 +108,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo from multilspy.language_servers.solargraph.solargraph import Solargraph return Solargraph(config, logger, repository_root_path) + elif config.code_language == Language.DART: + from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer + + return DartLanguageServer(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/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py new file mode 100644 index 0000000..09026f1 --- /dev/null +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -0,0 +1,120 @@ +from contextlib import asynccontextmanager +import logging +import os +import pathlib +import shutil +from typing import AsyncIterator +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +import json + + +class DartLanguageServer(LanguageServer): + """ + Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. + """ + + def __init__(self, config, logger, repository_root_path): + """ + Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + + executable_path = self.setup_runtime_dependencies() + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), + "dart", + ) + + def setup_runtime_dependencies(self): + dart_executable_path = shutil.which("dart") + assert dart_executable_path, "Dart executable not found in PATH" + + return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" + + + def _get_initialize_params(self, repository_absolute_path: str): + """ + Returns the initialize params for the Dart Language Server. + """ + with open( + os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" + ) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path( + repository_absolute_path + ).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["DartLanguageServer"]: + """ + Start the language server and yield when the server is ready. + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + pass + + 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 dart-language-server server process", logging.INFO + ) + await self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log( + "Sending initialize request to dart-language-server", + logging.DEBUG, + ) + init_response = await self.server.send_request( + "initialize", initialize_params + ) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) + + self.server.notify.initialized({}) + + yield self + + await self.server.shutdown() + await self.server.stop() diff --git a/src/multilspy/language_servers/dart_language_server/initialize_params.json b/src/multilspy/language_servers/dart_language_server/initialize_params.json new file mode 100644 index 0000000..bb8d695 --- /dev/null +++ b/src/multilspy/language_servers/dart_language_server/initialize_params.json @@ -0,0 +1,14 @@ +{ + "_description": "This file contains the initialization parameters for the Dart Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": {}, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 3628e9c..f4014d0 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -18,6 +18,7 @@ class Language(str, Enum): JAVASCRIPT = "javascript" GO = "go" RUBY = "ruby" + DART = "dart" def __str__(self) -> str: return self.value diff --git a/tests/multilspy/test_multilspy_dart.py b/tests/multilspy/test_multilspy_dart.py new file mode 100644 index 0000000..cb20a7f --- /dev/null +++ b/tests/multilspy/test_multilspy_dart.py @@ -0,0 +1,76 @@ +""" +This file contains tests for running the Dart Language Server. +""" + +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_dart(): + """ + Test the working of multilspy with a Dart repository. + """ + code_language = Language.DART + params = { + "code_language": code_language, + "repo_url": "https://github.com/simonoppowa/OpenNutriTracker/", + "repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805", + } + with create_test_context(params) as context: + lsp = LanguageServer.create( + context.config, context.logger, context.source_directory + ) + + async with lsp.start_server(): + result = await lsp.request_document_symbols( + str( + PurePath("lib/core/presentation/widgets/copy_or_delete_dialog.dart") + ) + ) + + assert isinstance(result, tuple) + assert len(result) == 2 + + symbols = result[0] + for symbol in symbols: + del symbol["deprecated"] + del symbol["kind"] + del symbol["location"]["uri"] + + assert symbols == [ + { + "location": { + "range": { + "end": {"character": 24, "line": 3}, + "start": {"character": 6, "line": 3}, + }, + }, + "name": "CopyOrDeleteDialog", + }, + { + "containerName": "CopyOrDeleteDialog", + "location": { + "range": { + "end": {"character": 26, "line": 4}, + "start": {"character": 8, "line": 4}, + }, + }, + "name": "CopyOrDeleteDialog", + }, + { + "containerName": "CopyOrDeleteDialog", + "location": { + "range": { + "end": {"character": 14, "line": 7}, + "start": {"character": 9, "line": 7}, + }, + }, + "name": "build", + }, + ] diff --git a/tests/multilspy/test_sync_multilspy_dart.py b/tests/multilspy/test_sync_multilspy_dart.py new file mode 100644 index 0000000..fcad8d3 --- /dev/null +++ b/tests/multilspy/test_sync_multilspy_dart.py @@ -0,0 +1,63 @@ +""" +This file contains tests for running the Python Language Server: jedi-language-server +""" + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from tests.test_utils import create_test_context +from pathlib import PurePath + + +def test_multilspy_python_black() -> None: + """ + Test the working of multilspy with python repository - black + """ + code_language = Language.DART + params = { + "code_language": code_language, + "repo_url": "https://github.com/simonoppowa/OpenNutriTracker/", + "repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805", + } + 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(): + result = lsp.request_references( + file_path=str( + PurePath("lib/features/add_meal/presentation/add_meal_screen.dart") + ), + line=19, + column=5, + ) + + for item in result: + del item["uri"] + del item["absolutePath"] + + assert result == [ + { + "range": { + "end": {"character": 21, "line": 20}, + "start": {"character": 8, "line": 20}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + { + "range": { + "end": {"character": 21, "line": 23}, + "start": {"character": 8, "line": 23}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + { + "range": { + "end": {"character": 53, "line": 26}, + "start": {"character": 40, "line": 26}, + }, + "relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart", + }, + ] From a949997262703602870f2cda0333cf5ded27033b Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:15:30 -0300 Subject: [PATCH 07/15] Add psutil dependency to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4175153..33079cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ pytest==7.3.1 pytest-asyncio==0.21.1 requests==2.32.3 typing-extensions>=4.2.0 +psutil>=7.0.0,<8.0.0 From 2390a21479553e2b42e06eb10049d5c09ab9ef0a Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:54:58 -0300 Subject: [PATCH 08/15] fix race codnition in process termination --- src/multilspy/lsp_protocol_handler/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index 865c2f0..e697e63 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -240,10 +240,10 @@ async def stop(self) -> None: try: await asyncio.wait_for(wait_for_end, timeout=60) except asyncio.TimeoutError: - process.kill() - for child in psutil.Process(process.pid).children(recursive=True): child.kill() + + process.kill() async def shutdown(self) -> None: From ca5e3e91e7093838f3cc33ef5fb5561fdf9cf7ee Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:12:53 -0300 Subject: [PATCH 09/15] feat: implement Dart Language Server runtime dependency setup --- .gitignore | 1 + .../dart_language_server.py | 34 ++++++++++++++++--- .../runtime_dependencies.json | 13 +++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/multilspy/language_servers/dart_language_server/runtime_dependencies.json diff --git a/.gitignore b/.gitignore index a7115ea..8464aab 100644 --- a/.gitignore +++ b/.gitignore @@ -402,6 +402,7 @@ src/multilspy/language_servers/gopls/static/ src/multilspy/language_servers/omnisharp/static/ src/multilspy/language_servers/rust_analyzer/static/ src/multilspy/language_servers/typescript_language_server/static/ +src/multilspy/language_servers/dart_language_server/static/ # Virtual Environment .venv/ diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py index 09026f1..b814306 100644 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -3,10 +3,12 @@ import os import pathlib import shutil +import stat from typing import AsyncIterator from multilspy.language_server import LanguageServer from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo import json +from multilspy.multilspy_utils import FileUtils, PlatformUtils class DartLanguageServer(LanguageServer): @@ -19,7 +21,7 @@ def __init__(self, config, logger, repository_root_path): Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - executable_path = self.setup_runtime_dependencies() + executable_path = self.setup_runtime_dependencies(logger) super().__init__( config, logger, @@ -28,9 +30,33 @@ def __init__(self, config, logger, repository_root_path): "dart", ) - def setup_runtime_dependencies(self): - dart_executable_path = shutil.which("dart") - assert dart_executable_path, "Dart executable not found in PATH" + def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + + assert len(runtime_dependencies) == 1 + dependency = runtime_dependencies[0] + + dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server") + dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"]) + + if not os.path.exists(dart_ls_dir): + os.makedirs(dart_ls_dir) + FileUtils.download_and_extract_archive( + logger, dependency["url"], dart_ls_dir, dependency["archiveType"] + ) + + + assert os.path.exists(dart_executable_path) + os.chmod(dart_executable_path, stat.S_IEXEC) return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" diff --git a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json b/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json new file mode 100644 index 0000000..e4ad4de --- /dev/null +++ b/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json @@ -0,0 +1,13 @@ +{ + "_description": "Used to download the runtime dependencies for running Dart Language Server", + "runtimeDependencies": [ + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Linux (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + } + ] +} \ No newline at end of file From 807ec41b1098a2aecccbbe074f80d8c2dbd15d5d Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:15:38 -0300 Subject: [PATCH 10/15] fix: update description for Dart Language Server runtime dependencies --- .../dart_language_server/runtime_dependencies.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json b/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json index e4ad4de..8010b94 100644 --- a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json +++ b/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json @@ -1,5 +1,5 @@ { - "_description": "Used to download the runtime dependencies for running Dart Language Server", + "_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive", "runtimeDependencies": [ { "id": "DartLanguageServer", From e2cc4dd5f54adf89a2e4e8140387fc71fd5ebc56 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:25:38 -0300 Subject: [PATCH 11/15] feat: add initialization options for Dart Language Server --- .../dart_language_server/initialize_params.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/multilspy/language_servers/dart_language_server/initialize_params.json b/src/multilspy/language_servers/dart_language_server/initialize_params.json index bb8d695..e90e408 100644 --- a/src/multilspy/language_servers/dart_language_server/initialize_params.json +++ b/src/multilspy/language_servers/dart_language_server/initialize_params.json @@ -4,6 +4,14 @@ "rootPath": "$rootPath", "rootUri": "$rootUri", "capabilities": {}, + "initializationOptions": { + "onlyAnalyzeProjectsWithOpenFiles": false, + "suggestFromUnimportedLibraries": true, + "closingLabels": false, + "outline": false, + "flutterOutline": false, + "allowOpenUri": false + }, "trace": "verbose", "workspaceFolders": [ { @@ -11,4 +19,5 @@ "name": "$name" } ] + } \ No newline at end of file From 93116f2cc12d6d9d786ea22b9f03434a72f36b11 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:34:42 -0300 Subject: [PATCH 12/15] feat: update README and pyproject.toml to include Dart language support --- README.md | 19 +++++++++++++++++-- pyproject.toml | 5 ++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90a4e58..5e70e98 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Introduction This repository hosts `multilspy`, a library developed as part of research conducted for NeruIPS 2023 paper titled ["Monitor-Guided Decoding of Code LMs with Static Analysis of Repository Context"](https://neurips.cc/virtual/2023/poster/70362) (["Guiding Language Models of Code with Global Context using Monitors"](https://arxiv.org/abs/2306.10763) on Arxiv). The paper introduces Monitor-Guided Decoding (MGD) for code generation using Language Models, where a monitor uses static analysis to guide the decoding, ensuring that the generated code follows various correctness properties, like absence of hallucinated symbol names, valid order of method calls, etc. For further details about Monitor-Guided Decoding, please refer to the paper and GitHub repository [microsoft/monitors4codegen](https://github.com/microsoft/monitors4codegen). -`multilspy` is a cross-platform library designed to simplify the process of creating language server clients to query and obtain results of various static analyses from a wide variety of language servers that communicate over the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). It is easily extensible to support any [language that has a Language Server](https://microsoft.github.io/language-server-protocol/implementors/servers/) and currently supports Java, Rust, C#, Go, JavaScript and Python. We aim to continuously add support for more language servers and languages. +`multilspy` is a cross-platform library designed to simplify the process of creating language server clients to query and obtain results of various static analyses from a wide variety of language servers that communicate over the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). It is easily extensible to support any [language that has a Language Server](https://microsoft.github.io/language-server-protocol/implementors/servers/) and we aim to continuously add support for more language servers and languages. [Language servers]((https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/)) are tools that perform a variety of static analyses on code repositories and provide useful information such as type-directed code completion suggestions, symbol definition locations, symbol references, etc., over the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/). Since LSP is language-agnostic, `multilspy` can provide the results for static analyses of code in different languages over a common interface. @@ -35,6 +35,21 @@ To install `multilspy` using pip, execute the following command: pip install multilspy ``` +## Supported Languages +`multilspy` currently supports the following languages: +| Code Language | Language Server | +| --- | --- | +| java | Eclipse JDTLS | +| python | jedi-language-server | +| rust | Rust Analyzer | +| csharp | OmniSharp / RazorSharp | +| typescript | TypeScriptLanguageServer | +| javascript | TypeScriptLanguageServer | +| go | gopls | +| dart | Dart | +| ruby | Solargraph | + + ## Usage Example usage: ```python @@ -42,7 +57,7 @@ from multilspy import SyncLanguageServer from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger ... -config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go" +config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby" logger = MultilspyLogger() lsp = SyncLanguageServer.create(config, logger, "/abs/path/to/project/root/") with lsp.start_server(): diff --git a/pyproject.toml b/pyproject.toml index 0c66b92..cf787f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ version = "0.0.13" authors = [ { name="Lakshya A Agrawal", email="lakshya.aagrawal@gmail.com" }, ] -description = "A language-agnostic LSP client in Python, with a library interface. Intended to be used to build applications around language servers. Currently multilspy supports language servers for Python, Rust, Java, Go, JavaScript, Ruby and C#. Originally appeared as part of Monitor-Guided Decoding (https://github.com/microsoft/monitors4codegen)" +description = "A language-agnostic LSP client in Python, with a library interface. Intended to be used to build applications around language servers. Currently multilspy supports language servers for Python, Rust, Java, Go, JavaScript, Ruby, C# and Dart. Originally appeared as part of Monitor-Guided Decoding (https://github.com/microsoft/monitors4codegen)" readme = "README.md" requires-python = ">=3.8, <4.0" classifiers = [ @@ -23,6 +23,9 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Rust", "Programming Language :: JavaScript" + "Programming Language :: Go", + "Programming Language :: Ruby", + "Programming Language :: Dart", ] dependencies = [ From d66f7361ac8ef09940f8fe0be722254961433e89 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 21:04:40 -0300 Subject: [PATCH 13/15] fix: missing comma --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf787f8..6c1a800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Java", "Programming Language :: Python", "Programming Language :: Rust", - "Programming Language :: JavaScript" + "Programming Language :: JavaScript", "Programming Language :: Go", "Programming Language :: Ruby", "Programming Language :: Dart", From c33215c90fdf3a2c9982d25dc299f5b706bf5093 Mon Sep 17 00:00:00 2001 From: v4rgas <66626747+v4rgas@users.noreply.github.com> Date: Sat, 1 Mar 2025 21:15:20 -0300 Subject: [PATCH 14/15] refactor: rename test functions for clarity and specificity in Dart Language Server tests --- tests/multilspy/test_multilspy_dart.py | 2 +- tests/multilspy/test_sync_multilspy_dart.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/multilspy/test_multilspy_dart.py b/tests/multilspy/test_multilspy_dart.py index cb20a7f..6002cde 100644 --- a/tests/multilspy/test_multilspy_dart.py +++ b/tests/multilspy/test_multilspy_dart.py @@ -12,7 +12,7 @@ @pytest.mark.asyncio -async def test_multilspy_dart(): +async def test_multilspy_dart_open_nutri_tracker(): """ Test the working of multilspy with a Dart repository. """ diff --git a/tests/multilspy/test_sync_multilspy_dart.py b/tests/multilspy/test_sync_multilspy_dart.py index 2a17ea7..6072cb5 100644 --- a/tests/multilspy/test_sync_multilspy_dart.py +++ b/tests/multilspy/test_sync_multilspy_dart.py @@ -8,9 +8,9 @@ from pathlib import PurePath -def test_sync_multilspy_dart() -> None: +def test_sync_multilspy_dart_open_nutri_tracker() -> None: """ - Test the working of multilspy with python repository - black + Test the working of multilspy with the Dart Language Server """ code_language = Language.DART params = { From be54e59bf1c6a4218b33c13b2077aa2021808673 Mon Sep 17 00:00:00 2001 From: Lakshya A Agrawal Date: Mon, 3 Mar 2025 09:04:52 +0530 Subject: [PATCH 15/15] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 41b67d3..9c7b398 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "flit_core.buildapi" [project] name = "multilspy" -version = "0.0.13" +version = "0.0.14" authors = [ { name="Lakshya A Agrawal", email="lakshya.aagrawal@gmail.com" }, ]