-
Notifications
You must be signed in to change notification settings - Fork 0
Replication of original PR #46: Add ruby support via solargraph #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: replicated-pr-46-before
Are you sure you want to change the base?
Changes from all commits
f6dbab9
c4deebb
29c6137
87f27e1
8b1a863
6a39cad
635f032
2f2bdbc
1b934d3
f30a3d2
a9091c2
5db5d7d
9ce8760
e74f8ba
d296c21
53e2ee8
eb5205b
7869543
6ac4ae1
06554c6
232de05
65a3aa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "_description": "This file contains the initialization parameters for the Solargraph Language Server.", | ||
| "processId": "$processId", | ||
| "rootPath": "$rootPath", | ||
| "rootUri": "$rootUri", | ||
| "capabilities": { | ||
| }, | ||
| "trace": "verbose", | ||
| "workspaceFolders": [ | ||
| { | ||
| "uri": "$uri", | ||
| "name": "$name" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.", | ||
| "runtimeDependencies": [ | ||
| { | ||
| "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", | ||
| "installCommand": "gem install solargraph -v 0.51.1", | ||
| "binaryName": "solargraph", | ||
| "archiveType": "gem" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||
| """ | ||||||||||
| Provides Ruby specific instantiation of the LanguageServer class using Solargraph. | ||||||||||
| Contains various configurations and settings specific to Ruby. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| import asyncio | ||||||||||
| import json | ||||||||||
| import logging | ||||||||||
| import os | ||||||||||
| import stat | ||||||||||
| import subprocess | ||||||||||
| 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_utils import FileUtils | ||||||||||
| from multilspy.multilspy_utils import PlatformUtils, PlatformId | ||||||||||
|
|
||||||||||
|
|
||||||||||
| class Solargraph(LanguageServer): | ||||||||||
| """ | ||||||||||
| Provides Ruby specific instantiation of the LanguageServer class using Solargraph. | ||||||||||
| Contains various configurations and settings specific to Ruby. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): | ||||||||||
| """ | ||||||||||
| Creates a Solargraph instance. This class is not meant to be instantiated directly. | ||||||||||
| Use LanguageServer.create() instead. | ||||||||||
| """ | ||||||||||
| solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) | ||||||||||
| super().__init__( | ||||||||||
| config, | ||||||||||
| logger, | ||||||||||
| repository_root_path, | ||||||||||
| ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), | ||||||||||
| "ruby", | ||||||||||
| ) | ||||||||||
| self.server_ready = asyncio.Event() | ||||||||||
|
|
||||||||||
| def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: | ||||||||||
| """ | ||||||||||
| Setup runtime dependencies for Solargraph. | ||||||||||
| """ | ||||||||||
| platform_id = PlatformUtils.get_platform_id() | ||||||||||
| which_cmd = "which" | ||||||||||
| if platform_id in [PlatformId.WIN_x64, PlatformId.WIN_arm64, PlatformId.WIN_x86]: | ||||||||||
| which_cmd = "where" | ||||||||||
|
|
||||||||||
| with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: | ||||||||||
| d = json.load(f) | ||||||||||
| del d["_description"] | ||||||||||
|
|
||||||||||
| dependency = d["runtimeDependencies"][0] | ||||||||||
|
|
||||||||||
| # Check if Ruby is installed | ||||||||||
| try: | ||||||||||
| result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) | ||||||||||
| ruby_version = result.stdout.strip() | ||||||||||
| logger.log(f"Ruby version: {ruby_version}", logging.INFO) | ||||||||||
| except subprocess.CalledProcessError as e: | ||||||||||
| raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") | ||||||||||
| except FileNotFoundError: | ||||||||||
| raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") | ||||||||||
|
|
||||||||||
| # Check if solargraph is installed | ||||||||||
| try: | ||||||||||
| result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) | ||||||||||
| if result.stdout.strip() == "false": | ||||||||||
| logger.log("Installing Solargraph...", logging.INFO) | ||||||||||
| subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) | ||||||||||
| except subprocess.CalledProcessError as e: | ||||||||||
| raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") | ||||||||||
|
|
||||||||||
| # Get the solargraph executable path | ||||||||||
| try: | ||||||||||
| result = subprocess.run([which_cmd, "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) | ||||||||||
| executeable_path = result.stdout.strip() | ||||||||||
|
|
||||||||||
| if not os.path.exists(executeable_path): | ||||||||||
| raise RuntimeError(f"Solargraph executable not found at {executeable_path}") | ||||||||||
|
|
||||||||||
| # Ensure the executable has the right permissions | ||||||||||
| os.chmod(executeable_path, os.stat(executeable_path).st_mode | stat.S_IEXEC) | ||||||||||
|
|
||||||||||
| return executeable_path | ||||||||||
| except subprocess.CalledProcessError: | ||||||||||
| raise RuntimeError("Failed to locate Solargraph executable.") | ||||||||||
|
|
||||||||||
| def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: | ||||||||||
| """ | ||||||||||
| Returns the initialize params for the Solargraph 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["Solargraph"]: | ||||||||||
| """ | ||||||||||
| Starts the Solargraph Language Server for Ruby, 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 register_capability_handler(params): | ||||||||||
| assert "registrations" in params | ||||||||||
| for registration in params["registrations"]: | ||||||||||
| if registration["method"] == "workspace/executeCommand": | ||||||||||
| self.initialize_searcher_command_available.set() | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐛 Correctness Issue Undefined class attribute usage. The code attempts to use 'initialize_searcher_command_available' which is not defined in the class, causing a runtime AttributeError. Current Code (Diff): - self.initialize_searcher_command_available.set()
+ if hasattr(self, 'initialize_searcher_command_available'):
+ self.initialize_searcher_command_available.set()📝 Committable suggestion
Suggested change
|
||||||||||
| self.resolve_main_method_available.set() | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐛 Correctness Issue Undefined class attribute usage. The code attempts to use 'resolve_main_method_available' which is not defined in the class, causing a runtime AttributeError. Current Code (Diff): - self.resolve_main_method_available.set()
+ if hasattr(self, 'resolve_main_method_available'):
+ self.resolve_main_method_available.set()📝 Committable suggestion
Suggested change
|
||||||||||
| return | ||||||||||
|
|
||||||||||
| async def lang_status_handler(params): | ||||||||||
| # TODO: Should we wait for | ||||||||||
| # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} | ||||||||||
| # Before proceeding? | ||||||||||
| if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": | ||||||||||
| self.service_ready_event.set() | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐛 Correctness Issue Undefined class attribute usage. The code attempts to use 'service_ready_event' which is not defined in the class, causing a runtime AttributeError. Current Code (Diff): - self.service_ready_event.set()
+ if hasattr(self, 'service_ready_event'):
+ self.service_ready_event.set()📝 Committable suggestion
Suggested change
|
||||||||||
|
|
||||||||||
| async def execute_client_command_handler(params): | ||||||||||
| return [] | ||||||||||
|
|
||||||||||
| async def do_nothing(params): | ||||||||||
| return | ||||||||||
|
|
||||||||||
| async def window_log_message(msg): | ||||||||||
| self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) | ||||||||||
|
|
||||||||||
| self.server.on_request("client/registerCapability", register_capability_handler) | ||||||||||
| self.server.on_notification("language/status", lang_status_handler) | ||||||||||
| 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) | ||||||||||
|
|
||||||||||
| async with super().start_server(): | ||||||||||
| self.logger.log("Starting solargraph 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, | ||||||||||
| ) | ||||||||||
| self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) | ||||||||||
| init_response = await self.server.send.initialize(initialize_params) | ||||||||||
| self.logger.log(f"Received init response: {init_response}", logging.INFO) | ||||||||||
| assert init_response["capabilities"]["textDocumentSync"] == 2 | ||||||||||
| assert "completionProvider" in init_response["capabilities"] | ||||||||||
| assert init_response["capabilities"]["completionProvider"] == { | ||||||||||
| "resolveProvider": True, | ||||||||||
| "triggerCharacters": [".", ":", "@"], | ||||||||||
| } | ||||||||||
| self.server.notify.initialized({}) | ||||||||||
| self.completions_available.set() | ||||||||||
|
|
||||||||||
| self.server_ready.set() | ||||||||||
| await self.server_ready.wait() | ||||||||||
|
Comment on lines
+187
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐛 Correctness Issue Potential deadlock in event handling. Setting and immediately waiting on the same event ('server_ready') creates a deadlock as the event is never reset between operations. Current Code (Diff): - self.server_ready.set()
- await self.server_ready.wait()
+ # self.server_ready.set()
+ # await self.server_ready.wait()📝 Committable suggestion
Suggested change
|
||||||||||
|
|
||||||||||
| yield self | ||||||||||
|
|
||||||||||
| await self.server.shutdown() | ||||||||||
| await self.server.stop() | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| """ | ||
| This file contains tests for running the Ruby Language Server: solargraph | ||
| """ | ||
|
|
||
| import unittest | ||
| import pytest | ||
|
|
||
| from multilspy import LanguageServer | ||
| from multilspy.multilspy_config import Language | ||
| from multilspy.multilspy_types import Position, CompletionItemKind | ||
| from tests.test_utils import create_test_context | ||
| from pathlib import PurePath | ||
| from tests.multilspy.test_sync_multilspy_ruby import EXPECTED_RESULT | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_multilspy_ruby_rubyland(): | ||
| """ | ||
| Test the working of multilspy with ruby repository - rubyland | ||
| """ | ||
| code_language = Language.RUBY | ||
| params = { | ||
| "code_language": code_language, | ||
| "repo_url": "https://github.com/jrochkind/rubyland/", | ||
| "repo_commit": "c243ee2533a5822f5699a2475e492927ace039c7" | ||
| } | ||
| 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(): | ||
| result = await lsp.request_document_symbols(str(PurePath("app/controllers/application_controller.rb"))) | ||
|
|
||
| assert isinstance(result, tuple) | ||
| assert len(result) == 2 | ||
| symbol_names = list(map(lambda x: x["name"], result[0])) | ||
| assert symbol_names == ['ApplicationController', 'protected_demo_authentication'] | ||
|
|
||
| result = await lsp.request_definition(str(PurePath("app/controllers/feed_controller.rb")), 11, 23) | ||
|
|
||
| feed_path = str(PurePath("app/models/feed.rb")) | ||
| assert isinstance(result, list) | ||
| assert len(result) == 2 | ||
| assert feed_path in list(map(lambda x: x["relativePath"], result)) | ||
|
|
||
| result = await lsp.request_references(feed_path, 0, 7) | ||
|
|
||
| assert isinstance(result, list) | ||
| assert len(result) == 8 | ||
|
|
||
| for item in result: | ||
| del item["uri"] | ||
| del item["absolutePath"] | ||
|
|
||
| case = unittest.TestCase() | ||
| case.assertCountEqual( | ||
| result, | ||
| EXPECTED_RESULT, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐛 Correctness Issue
Typo in variable name causes runtime error.
The variable 'executeable_path' has a typo and should be 'executable_path', which will cause inconsistency when referencing this variable later.
Current Code (Diff):
📝 Committable suggestion
🔄 Dependencies Affected
multilspy/language_servers/solargraph/solargraph.py
Function:
Solargraph.setup_runtime_dependenciesIssue: Variable name mismatch in subsequent references
Suggestion: Fix all references to use the correct variable name
Current Code (Diff):