From f6dbab978c36703ecbe0adcd1fb2bddd76ea59c1 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Fri, 17 Jan 2025 16:22:20 -0600 Subject: [PATCH 01/21] Initial setup for adding ruby support using solargraph --- src/multilspy/language_server.py | 4 + .../solargraph/initialize_params.json | 93 +++++++++ .../solargraph/runtime_dependencies.json | 11 + .../language_servers/solargraph/solargraph.py | 193 ++++++++++++++++++ src/multilspy/multilspy_config.py | 1 + tests/multilspy/test_multilspy_ruby.py | 109 ++++++++++ tests/multilspy/test_sync_multilspy_ruby.py | 66 ++++++ 7 files changed, 477 insertions(+) create mode 100644 src/multilspy/language_servers/solargraph/initialize_params.json create mode 100644 src/multilspy/language_servers/solargraph/runtime_dependencies.json create mode 100644 src/multilspy/language_servers/solargraph/solargraph.py create mode 100644 tests/multilspy/test_multilspy_ruby.py create mode 100644 tests/multilspy/test_sync_multilspy_ruby.py diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 5093526..665d39c 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -104,6 +104,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo from multilspy.language_servers.gopls.gopls import Gopls return Gopls(config, logger, repository_root_path) + elif config.code_language == Language.RUBY: + from multilspy.language_servers.solargraph.solargraph import Solargraph + + return Solargraph(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/solargraph/initialize_params.json b/src/multilspy/language_servers/solargraph/initialize_params.json new file mode 100644 index 0000000..e7a6ecc --- /dev/null +++ b/src/multilspy/language_servers/solargraph/initialize_params.json @@ -0,0 +1,93 @@ +{ + "_description": "This file contains the initialization parameters for the Solargraph Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "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"] + } + }, + "definition": { + "dynamicRegistration": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true + }, + "codeAction": { + "dynamicRegistration": true + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} diff --git a/src/multilspy/language_servers/solargraph/runtime_dependencies.json b/src/multilspy/language_servers/solargraph/runtime_dependencies.json new file mode 100644 index 0000000..e6c454e --- /dev/null +++ b/src/multilspy/language_servers/solargraph/runtime_dependencies.json @@ -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.50.0.gem", + "installCommand": "gem install solargraph -v 0.50.0", + "binaryName": "solargraph", + "archiveType": "gem" + } + ] +} diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py new file mode 100644 index 0000000..367930b --- /dev/null +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -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 + + +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) + 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) -> str: + """ + Setup runtime dependencies for Solargraph. + """ + 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"] + + dependency = d["runtimeDependencies"][0] + + # Check if Ruby is installed + try: + subprocess.run(["ruby", "--version"], check=True, capture_output=True) + except subprocess.CalledProcessError: + raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + 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=True, capture_output=True, text=True) + if result.stdout.strip() == "false": + logger.info("Installing Solargraph...") + subprocess.run(dependency["installCommand"].split(), check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to check or install Solargraph. {e}") + + # Get the solargraph executable path + try: + result = subprocess.run(["which", "solargraph"], check=True, capture_output=True, text=True) + 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["RubyAnalyzer"]: + """ + Starts the Ruby Analyzer 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 register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + 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() + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting RubyAnalyzer 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": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() + + await self.server_ready.wait() + + 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 221389d..3628e9c 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -17,6 +17,7 @@ class Language(str, Enum): TYPESCRIPT = "typescript" JAVASCRIPT = "javascript" GO = "go" + RUBY = "ruby" def __str__(self) -> str: return self.value diff --git a/tests/multilspy/test_multilspy_ruby.py b/tests/multilspy/test_multilspy_ruby.py new file mode 100644 index 0000000..bd91cfb --- /dev/null +++ b/tests/multilspy/test_multilspy_ruby.py @@ -0,0 +1,109 @@ +""" +This file contains tests for running the Ruby Language Server: Ruby-analyzer +""" + +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 + + +async def test_multilspy_ruby_carbonyl(): + """ + Test the working of multilspy with ruby repository - rails/rails + """ + code_language = Language.RUBY + params = { + "code_language": code_language, + "repo_url": "https://github.com/rails/rails/", + "repo_commit": "abb7035e08c07bb4e2941c1c27003609ce81e77b" + } + 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_definition(str(PurePath("activemodel/lib/active_model.rb")), 132, 18) + + assert isinstance(result, list) + assert len(result) == 1 + item = result[0] + assert item["relativePath"] == str(PurePath("actionview/lib/action_view.rb")) + assert item["range"] == { + "start": {"line": 43, "character": 11}, + "end": {"line": 43, "character": 19}, + } + + result = await lsp.request_references(str(PurePath("activemodel/lib/active_model.rb")), 43, 15) + + assert isinstance(result, list) + assert len(result) == 2 + + for item in result: + del item["uri"] + del item["absolutePath"] + + case = unittest.TestCase() + case.assertCountEqual( + result, + [ + { + "relativePath": str(PurePath("activemodel/lib/active_model.rb")), + "range": { + "start": {"line": 132, "character": 13}, + "end": {"line": 132, "character": 21}, + }, + }, + { + "relativePath": str(PurePath("actionview/lib/action_view.rb")), + "range": { + "start": {"line": 16, "character": 13}, + "end": {"line": 16, "character": 21}, + }, + }, + ], + ) + +# @pytest.mark.asyncio +# async def test_multilspy_rust_completions_mediaplayer() -> None: +# """ +# Test the working of multilspy with Rust repository - mediaplayer +# """ +# code_language = Language.RUST +# params = { +# "code_language": code_language, +# "repo_url": "https://github.com/LakshyAAAgrawal/MediaPlayer_example/", +# "repo_commit": "ba27bb16c7ba1d88808300364af65eb69b1d84a8", +# } + +# with create_test_context(params) as context: +# lsp = LanguageServer.create(context.config, context.logger, context.source_directory) +# filepath = "src/playlist.rs" +# # 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. +# async with lsp.start_server(): +# with lsp.open_file(filepath): +# deleted_text = lsp.delete_text_between_positions( +# filepath, Position(line=10, character=40), Position(line=12, character=4) +# ) +# assert ( +# deleted_text +# == """reset(); +# media_player1 = media_player; +# """ +# ) + +# response = await lsp.request_completions(filepath, 10, 40, allow_incomplete=True) + +# response = [item for item in response if item['kind'] != CompletionItemKind.Snippet] + +# for item in response: +# item['completionText'] = item['completionText'][:item['completionText'].find('(')] + +# assert set([item['completionText'] for item in response]) == {'reset', 'into', 'try_into', 'prepare'} diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py new file mode 100644 index 0000000..181480d --- /dev/null +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -0,0 +1,66 @@ +""" +This file contains tests for running the Ruby Language Server: ruby-analyzer +""" + +import unittest + +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_ruby_rails_rails() -> None: + """ + Test the working of multilspy with ruby repository - carbonyl + """ + code_language = Language.RUBY + params = { + "code_language": code_language, + "repo_url": "https://github.com/rails/rails/", + "repo_commit": "abb7035e08c07bb4e2941c1c27003609ce81e77b" + } + 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_definition(str(PurePath("activemodel/lib/active_model.rb")), 32, 5) + + assert isinstance(result, list) + assert len(result) == 1 + item = result[0] + assert item["relativePath"] == str(PurePath("actionview/lib/action_view.rb")) + assert item["range"] == { + "start": {"line": 43, "character": 11}, + "end": {"line": 43, "character": 19}, + } + + result = lsp.request_references(str(PurePath("actionview/lib/action_view.rb")), 43, 15) + + assert isinstance(result, list) + assert len(result) == 2 + + for item in result: + del item["uri"] + del item["absolutePath"] + + case = unittest.TestCase() + case.assertCountEqual( + result, + [ + { + "relativePath": str(PurePath("activemodel/lib/active_model.rb")), + "range": { + "start": {"line": 132, "character": 13}, + "end": {"line": 132, "character": 21}, + }, + }, + { + "relativePath": str(PurePath("actionview/lib/action_view.rb")), + "range": { + "start": {"line": 16, "character": 13}, + "end": {"line": 16, "character": 21}, + }, + }, + ], + ) From c4deebb5c9871ba5102a7b276c461e903d96e908 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 14:04:59 -0600 Subject: [PATCH 02/21] Skip sending client capabilities otherwise solargraph responds with a minimal set --- .../solargraph/initialize_params.json | 78 ------------------- 1 file changed, 78 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/initialize_params.json b/src/multilspy/language_servers/solargraph/initialize_params.json index e7a6ecc..2cbb41f 100644 --- a/src/multilspy/language_servers/solargraph/initialize_params.json +++ b/src/multilspy/language_servers/solargraph/initialize_params.json @@ -4,84 +4,6 @@ "rootPath": "$rootPath", "rootUri": "$rootUri", "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"] - } - }, - "definition": { - "dynamicRegistration": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "hierarchicalDocumentSymbolSupport": true - }, - "codeAction": { - "dynamicRegistration": true - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true - } - } }, "trace": "verbose", "workspaceFolders": [ From 29c6137083b44ce972201b90a78a231a4210cdd5 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 14:05:37 -0600 Subject: [PATCH 03/21] Run solargraph-related commands in repo directory --- .../language_servers/solargraph/solargraph.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 367930b..954a5b3 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -33,7 +33,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ 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) + solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) super().__init__( config, logger, @@ -43,7 +43,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_ ) self.server_ready = asyncio.Event() - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: """ Setup runtime dependencies for Solargraph. """ @@ -57,7 +57,8 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if Ruby is installed try: - subprocess.run(["ruby", "--version"], check=True, capture_output=True) + result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) + logger.log(f"Ruby version: {result.stdout.strip()}", logging.INFO) except subprocess.CalledProcessError: raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") except FileNotFoundError: @@ -65,16 +66,16 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if solargraph is installed try: - result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=True, capture_output=True, text=True) + result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=True, capture_output=True, text=True, cwd=repository_root_path) if result.stdout.strip() == "false": - logger.info("Installing Solargraph...") + logger.log("Installing Solargraph...", logging.INFO) subprocess.run(dependency["installCommand"].split(), check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to check or install Solargraph. {e}") # Get the solargraph executable path try: - result = subprocess.run(["which", "solargraph"], check=True, capture_output=True, text=True) + result = subprocess.run(["which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) executeable_path = result.stdout.strip() if not os.path.exists(executeable_path): @@ -82,7 +83,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # 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.") @@ -147,10 +148,6 @@ async def execute_client_command_handler(params): async def do_nothing(params): return - async def check_experimental_status(params): - if params["quiescent"] == True: - self.server_ready.set() - async def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) @@ -161,7 +158,6 @@ async def window_log_message(msg): 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 RubyAnalyzer server process", logging.INFO) @@ -179,12 +175,12 @@ async def window_log_message(msg): assert "completionProvider" in init_response["capabilities"] assert init_response["capabilities"]["completionProvider"] == { "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, + "triggerCharacters": [".", ":", "@"], } self.server.notify.initialized({}) self.completions_available.set() + self.server_ready.set() await self.server_ready.wait() yield self From 87f27e15cc2540a77266314be92c6a2f70bea243 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 14:06:13 -0600 Subject: [PATCH 04/21] Fix sync tests to work with rubyland repo --- tests/multilspy/test_sync_multilspy_ruby.py | 80 ++++++++++++++++----- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py index 181480d..9d2f30f 100644 --- a/tests/multilspy/test_sync_multilspy_ruby.py +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -9,36 +9,36 @@ from tests.test_utils import create_test_context from pathlib import PurePath -def test_multilspy_ruby_rails_rails() -> None: +def test_multilspy_ruby_rubyland() -> None: """ - Test the working of multilspy with ruby repository - carbonyl + Test the working of multilspy with ruby repository - rubyland """ code_language = Language.RUBY params = { "code_language": code_language, - "repo_url": "https://github.com/rails/rails/", - "repo_commit": "abb7035e08c07bb4e2941c1c27003609ce81e77b" + "repo_url": "https://github.com/jrochkind/rubyland/", + "repo_commit": "c243ee2533a5822f5699a2475e492927ace039c7" } 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_definition(str(PurePath("activemodel/lib/active_model.rb")), 32, 5) + result = lsp.request_definition(str(PurePath("app/controllers/feed_controller.rb")), 11, 23) assert isinstance(result, list) - assert len(result) == 1 - item = result[0] - assert item["relativePath"] == str(PurePath("actionview/lib/action_view.rb")) + assert len(result) == 2 + item = result[1] + assert item["relativePath"] == str(PurePath("app/models/feed.rb")) assert item["range"] == { - "start": {"line": 43, "character": 11}, - "end": {"line": 43, "character": 19}, + "start": {"line": 0, "character": 0}, + "end": {"line": 42, "character": 3}, } - result = lsp.request_references(str(PurePath("actionview/lib/action_view.rb")), 43, 15) + result = lsp.request_references(str(PurePath("app/models/feed.rb")), 0, 7) assert isinstance(result, list) - assert len(result) == 2 + assert len(result) == 8 for item in result: del item["uri"] @@ -49,18 +49,60 @@ def test_multilspy_ruby_rails_rails() -> None: result, [ { - "relativePath": str(PurePath("activemodel/lib/active_model.rb")), "range": { - "start": {"line": 132, "character": 13}, - "end": {"line": 132, "character": 21}, + "start": {"line": 11, "character": 20}, + "end": {"line": 11, "character": 24}, + }, + "relativePath": "app/controllers/feed_controller.rb", + }, + { + "range": { + "start": {"line": 27, "character": 46}, + "end": {"line": 27, "character": 50}, + }, + "relativePath": "app/controllers/feed_controller.rb", + }, + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 10}, + }, + "relativePath": "app/models/feed.rb", + }, + { + "range": { + "start": {"line": 13, "character": 74}, + "end": {"line": 13, "character": 78}, + }, + "relativePath": "app/updaters/feed_updater.rb", + }, + { + "range": { + "start": {"line": 52, "character": 4}, + "end": {"line": 52, "character": 8}, + }, + "relativePath": "app/updaters/feed_updater.rb", + }, + { + "range": { + "start": {"line": 10, "character": 20}, + "end": {"line": 10, "character": 24}, + }, + "relativePath": "app/updaters/updater.rb", + }, + { + "range": { + "start": {"line": 14, "character": 20}, + "end": {"line": 14, "character": 24}, }, + "relativePath": "app/updaters/updater.rb", }, { - "relativePath": str(PurePath("actionview/lib/action_view.rb")), "range": { - "start": {"line": 16, "character": 13}, - "end": {"line": 16, "character": 21}, + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 10}, }, + "relativePath": "db/migrate/20161029161855_feed.rb", }, - ], + ] ) From 8b1a863eb428a29fb4407b107ff7c8d95340f64d Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 14:28:26 -0600 Subject: [PATCH 05/21] Use solargraph in comments --- src/multilspy/language_servers/solargraph/solargraph.py | 6 +++--- tests/multilspy/test_multilspy_ruby.py | 2 +- tests/multilspy/test_sync_multilspy_ruby.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 954a5b3..01a1206 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -113,9 +113,9 @@ def _get_initialize_params(self, repository_absolute_path: str) -> InitializePar return d @asynccontextmanager - async def start_server(self) -> AsyncIterator["RubyAnalyzer"]: + async def start_server(self) -> AsyncIterator["Solargraph"]: """ - Starts the Ruby Analyzer Language Server, waits for the server to be ready and yields the LanguageServer instance. + Starts the Solargraph Language Server for Ruby, waits for the server to be ready and yields the LanguageServer instance. Usage: ``` @@ -160,7 +160,7 @@ async def window_log_message(msg): self.server.on_notification("language/actionableNotification", do_nothing) async with super().start_server(): - self.logger.log("Starting RubyAnalyzer server process", logging.INFO) + self.logger.log("Starting solargraph server process", logging.INFO) await self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) diff --git a/tests/multilspy/test_multilspy_ruby.py b/tests/multilspy/test_multilspy_ruby.py index bd91cfb..8c34e50 100644 --- a/tests/multilspy/test_multilspy_ruby.py +++ b/tests/multilspy/test_multilspy_ruby.py @@ -1,5 +1,5 @@ """ -This file contains tests for running the Ruby Language Server: Ruby-analyzer +This file contains tests for running the Ruby Language Server: solargraph """ import unittest diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py index 9d2f30f..a22a367 100644 --- a/tests/multilspy/test_sync_multilspy_ruby.py +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -1,5 +1,5 @@ """ -This file contains tests for running the Ruby Language Server: ruby-analyzer +This file contains tests for running the Ruby Language Server: solargraph """ import unittest From 6a39cadb1bd6db3de5106ad32d8883b44af7681a Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 15:05:39 -0600 Subject: [PATCH 06/21] Fix async ruby tests --- tests/multilspy/test_multilspy_ruby.py | 81 +++---------- tests/multilspy/test_sync_multilspy_ruby.py | 119 ++++++++++---------- 2 files changed, 76 insertions(+), 124 deletions(-) diff --git a/tests/multilspy/test_multilspy_ruby.py b/tests/multilspy/test_multilspy_ruby.py index 8c34e50..83acb13 100644 --- a/tests/multilspy/test_multilspy_ruby.py +++ b/tests/multilspy/test_multilspy_ruby.py @@ -10,17 +10,19 @@ 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 -async def test_multilspy_ruby_carbonyl(): +@pytest.mark.asyncio +async def test_multilspy_ruby_rubyland(): """ - Test the working of multilspy with ruby repository - rails/rails + Test the working of multilspy with ruby repository - rubyland """ code_language = Language.RUBY params = { "code_language": code_language, - "repo_url": "https://github.com/rails/rails/", - "repo_commit": "abb7035e08c07bb4e2941c1c27003609ce81e77b" + "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) @@ -29,21 +31,21 @@ async def test_multilspy_ruby_carbonyl(): # 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_definition(str(PurePath("activemodel/lib/active_model.rb")), 132, 18) + result = await lsp.request_definition(str(PurePath("app/controllers/feed_controller.rb")), 11, 23) assert isinstance(result, list) - assert len(result) == 1 - item = result[0] - assert item["relativePath"] == str(PurePath("actionview/lib/action_view.rb")) + assert len(result) == 2 + item = result[1] + assert item["relativePath"] == str(PurePath("app/models/feed.rb")) assert item["range"] == { - "start": {"line": 43, "character": 11}, - "end": {"line": 43, "character": 19}, + "start": {"line": 0, "character": 0}, + "end": {"line": 42, "character": 3}, } - result = await lsp.request_references(str(PurePath("activemodel/lib/active_model.rb")), 43, 15) + result = await lsp.request_references(str(PurePath("app/models/feed.rb")), 0, 7) assert isinstance(result, list) - assert len(result) == 2 + assert len(result) == 8 for item in result: del item["uri"] @@ -52,58 +54,5 @@ async def test_multilspy_ruby_carbonyl(): case = unittest.TestCase() case.assertCountEqual( result, - [ - { - "relativePath": str(PurePath("activemodel/lib/active_model.rb")), - "range": { - "start": {"line": 132, "character": 13}, - "end": {"line": 132, "character": 21}, - }, - }, - { - "relativePath": str(PurePath("actionview/lib/action_view.rb")), - "range": { - "start": {"line": 16, "character": 13}, - "end": {"line": 16, "character": 21}, - }, - }, - ], + EXPECTED_RESULT, ) - -# @pytest.mark.asyncio -# async def test_multilspy_rust_completions_mediaplayer() -> None: -# """ -# Test the working of multilspy with Rust repository - mediaplayer -# """ -# code_language = Language.RUST -# params = { -# "code_language": code_language, -# "repo_url": "https://github.com/LakshyAAAgrawal/MediaPlayer_example/", -# "repo_commit": "ba27bb16c7ba1d88808300364af65eb69b1d84a8", -# } - -# with create_test_context(params) as context: -# lsp = LanguageServer.create(context.config, context.logger, context.source_directory) -# filepath = "src/playlist.rs" -# # 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. -# async with lsp.start_server(): -# with lsp.open_file(filepath): -# deleted_text = lsp.delete_text_between_positions( -# filepath, Position(line=10, character=40), Position(line=12, character=4) -# ) -# assert ( -# deleted_text -# == """reset(); -# media_player1 = media_player; -# """ -# ) - -# response = await lsp.request_completions(filepath, 10, 40, allow_incomplete=True) - -# response = [item for item in response if item['kind'] != CompletionItemKind.Snippet] - -# for item in response: -# item['completionText'] = item['completionText'][:item['completionText'].find('(')] - -# assert set([item['completionText'] for item in response]) == {'reset', 'into', 'try_into', 'prepare'} diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py index a22a367..20fea7d 100644 --- a/tests/multilspy/test_sync_multilspy_ruby.py +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -9,6 +9,66 @@ from tests.test_utils import create_test_context from pathlib import PurePath +EXPECTED_RESULT = [ + { + "range": { + "start": {"line": 11, "character": 20}, + "end": {"line": 11, "character": 24}, + }, + "relativePath": "app/controllers/feed_controller.rb", + }, + { + "range": { + "start": {"line": 27, "character": 46}, + "end": {"line": 27, "character": 50}, + }, + "relativePath": "app/controllers/feed_controller.rb", + }, + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 10}, + }, + "relativePath": "app/models/feed.rb", + }, + { + "range": { + "start": {"line": 13, "character": 74}, + "end": {"line": 13, "character": 78}, + }, + "relativePath": "app/updaters/feed_updater.rb", + }, + { + "range": { + "start": {"line": 52, "character": 4}, + "end": {"line": 52, "character": 8}, + }, + "relativePath": "app/updaters/feed_updater.rb", + }, + { + "range": { + "start": {"line": 10, "character": 20}, + "end": {"line": 10, "character": 24}, + }, + "relativePath": "app/updaters/updater.rb", + }, + { + "range": { + "start": {"line": 14, "character": 20}, + "end": {"line": 14, "character": 24}, + }, + "relativePath": "app/updaters/updater.rb", + }, + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 10}, + }, + "relativePath": "db/migrate/20161029161855_feed.rb", + }, +] + + def test_multilspy_ruby_rubyland() -> None: """ Test the working of multilspy with ruby repository - rubyland @@ -47,62 +107,5 @@ def test_multilspy_ruby_rubyland() -> None: case = unittest.TestCase() case.assertCountEqual( result, - [ - { - "range": { - "start": {"line": 11, "character": 20}, - "end": {"line": 11, "character": 24}, - }, - "relativePath": "app/controllers/feed_controller.rb", - }, - { - "range": { - "start": {"line": 27, "character": 46}, - "end": {"line": 27, "character": 50}, - }, - "relativePath": "app/controllers/feed_controller.rb", - }, - { - "range": { - "start": {"line": 0, "character": 6}, - "end": {"line": 0, "character": 10}, - }, - "relativePath": "app/models/feed.rb", - }, - { - "range": { - "start": {"line": 13, "character": 74}, - "end": {"line": 13, "character": 78}, - }, - "relativePath": "app/updaters/feed_updater.rb", - }, - { - "range": { - "start": {"line": 52, "character": 4}, - "end": {"line": 52, "character": 8}, - }, - "relativePath": "app/updaters/feed_updater.rb", - }, - { - "range": { - "start": {"line": 10, "character": 20}, - "end": {"line": 10, "character": 24}, - }, - "relativePath": "app/updaters/updater.rb", - }, - { - "range": { - "start": {"line": 14, "character": 20}, - "end": {"line": 14, "character": 24}, - }, - "relativePath": "app/updaters/updater.rb", - }, - { - "range": { - "start": {"line": 0, "character": 6}, - "end": {"line": 0, "character": 10}, - }, - "relativePath": "db/migrate/20161029161855_feed.rb", - }, - ] + EXPECTED_RESULT, ) From 635f032d6ef11081eaac781c7058d92791c938b4 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 15:11:48 -0600 Subject: [PATCH 07/21] Check for correct version of ruby --- src/multilspy/language_servers/solargraph/solargraph.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 01a1206..641abd4 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -57,8 +57,13 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if Ruby is installed try: + result = subprocess.run(["cat", ".ruby-version"], capture_output=True, cwd=repository_root_path) + expected_ruby_version = result.stdout.strip() result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) - logger.log(f"Ruby version: {result.stdout.strip()}", logging.INFO) + actual_ruby_version = result.stdout.strip() + if expected_ruby_version not in actual_ruby_version: + raise RuntimeError(f"Expected Ruby version {expected_ruby_version} but found {actual_ruby_version}") + logger.log(f"Ruby version: {actual_ruby_version}", logging.INFO) except subprocess.CalledProcessError: raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") except FileNotFoundError: From 2f2bdbca3d993f708d6f7786410a70fd7c52eaa9 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Wed, 22 Jan 2025 15:35:22 -0600 Subject: [PATCH 08/21] Add support for windows platforms --- .../language_servers/solargraph/solargraph.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 641abd4..f4ab307 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -19,7 +19,7 @@ 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 +from multilspy.multilspy_utils import PlatformUtils, PlatformId class Solargraph(LanguageServer): @@ -48,6 +48,11 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC Setup runtime dependencies for Solargraph. """ platform_id = PlatformUtils.get_platform_id() + cat_cmd = "cat" + which_cmd = "which" + if platform_id in [PlatformId.WIN_x64, PlatformId.WIN_arm64, PlatformId.WIN_x86]: + cat_cmd = "type" + which_cmd = "where" with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: d = json.load(f) @@ -57,7 +62,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if Ruby is installed try: - result = subprocess.run(["cat", ".ruby-version"], capture_output=True, cwd=repository_root_path) + result = subprocess.run([cat_cmd, ".ruby-version"], capture_output=True, cwd=repository_root_path) expected_ruby_version = result.stdout.strip() result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) actual_ruby_version = result.stdout.strip() @@ -80,7 +85,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Get the solargraph executable path try: - result = subprocess.run(["which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) + 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): From 1b934d3c42207da7c55a7011dbd7be17d402201d Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 09:49:15 -0600 Subject: [PATCH 09/21] Make solargraph work gracefully across ruby versions --- .../language_servers/solargraph/solargraph.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index f4ab307..5a1fb35 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -48,10 +48,8 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC Setup runtime dependencies for Solargraph. """ platform_id = PlatformUtils.get_platform_id() - cat_cmd = "cat" which_cmd = "which" if platform_id in [PlatformId.WIN_x64, PlatformId.WIN_arm64, PlatformId.WIN_x86]: - cat_cmd = "type" which_cmd = "where" with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: @@ -62,15 +60,11 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if Ruby is installed try: - result = subprocess.run([cat_cmd, ".ruby-version"], capture_output=True, cwd=repository_root_path) - expected_ruby_version = result.stdout.strip() result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) - actual_ruby_version = result.stdout.strip() - if expected_ruby_version not in actual_ruby_version: - raise RuntimeError(f"Expected Ruby version {expected_ruby_version} but found {actual_ruby_version}") - logger.log(f"Ruby version: {actual_ruby_version}", logging.INFO) - except subprocess.CalledProcessError: - raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + 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.") From f30a3d29d6e98cd39d7b7680031ff569dcb7259d Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 11:35:31 -0600 Subject: [PATCH 10/21] Improve logging for when solargraph install fails --- src/multilspy/language_servers/solargraph/solargraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 5a1fb35..cb21a27 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -75,7 +75,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC logger.log("Installing Solargraph...", logging.INFO) subprocess.run(dependency["installCommand"].split(), check=True) except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to check or install Solargraph. {e}") + raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") # Get the solargraph executable path try: From a9091c24d1061fd5f52a54153ae796ed9a7e5370 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 11:48:00 -0600 Subject: [PATCH 11/21] Temporarily trigger tests on all pushes --- .github/workflows/test-workflow.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index 891b9fd..af32a78 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -3,7 +3,6 @@ name: Test Multilspy on: # Triggers the workflow on push or pull request events but only for the master branch push: - branches: [ main ] pull_request: jobs: From 5db5d7de9fa6ca1d06b7186972c5c9437481fcaa Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 12:36:35 -0600 Subject: [PATCH 12/21] Correctly handle solargraph not installed case --- src/multilspy/language_servers/solargraph/solargraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index cb21a27..ea83347 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -70,7 +70,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC # Check if solargraph is installed try: - result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=True, capture_output=True, text=True, cwd=repository_root_path) + 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) From 9ce8760699812faee2020df28272e26a6acb7337 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 12:56:21 -0600 Subject: [PATCH 13/21] Capture the output of solargraph install failures --- src/multilspy/language_servers/solargraph/solargraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index ea83347..5b47915 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -73,7 +73,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC 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) + subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") From e74f8ba70e93f08d44d023d67d69f6dba6359806 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 13:12:08 -0600 Subject: [PATCH 14/21] Install ruby to address permissions issues in github actions --- .github/workflows/test-workflow.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index af32a78..a1fa103 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -18,6 +18,10 @@ jobs: go-version: '1.21' - name: Install gopls run: go install golang.org/x/tools/gopls@latest + - name: Install ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' - name: Set up Python uses: actions/setup-python@v5 with: From d296c21c77c1f3527a598a714ea1a8166287c8d3 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 13:52:17 -0600 Subject: [PATCH 15/21] Run the solargraph install in the repo directory --- src/multilspy/language_servers/solargraph/solargraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 5b47915..a0a62b8 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -73,7 +73,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC 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) + 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}") From 53e2ee87cf2f4fbb57c2737c1a391ea22d0df58e Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 15:20:04 -0600 Subject: [PATCH 16/21] Add tests for document symbols --- tests/multilspy/test_multilspy_ruby.py | 7 +++++++ tests/multilspy/test_sync_multilspy_ruby.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/tests/multilspy/test_multilspy_ruby.py b/tests/multilspy/test_multilspy_ruby.py index 83acb13..6efebf5 100644 --- a/tests/multilspy/test_multilspy_ruby.py +++ b/tests/multilspy/test_multilspy_ruby.py @@ -31,6 +31,13 @@ async def test_multilspy_ruby_rubyland(): # 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) assert isinstance(result, list) diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py index 20fea7d..e9a6541 100644 --- a/tests/multilspy/test_sync_multilspy_ruby.py +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -84,6 +84,13 @@ def test_multilspy_ruby_rubyland() -> None: # 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_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 = lsp.request_definition(str(PurePath("app/controllers/feed_controller.rb")), 11, 23) assert isinstance(result, list) From eb5205bb547122138c28b103ba0cd646b2923345 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 16:16:35 -0600 Subject: [PATCH 17/21] Make tests resilient to ordering changes --- tests/multilspy/test_multilspy_ruby.py | 10 +++------- tests/multilspy/test_sync_multilspy_ruby.py | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/multilspy/test_multilspy_ruby.py b/tests/multilspy/test_multilspy_ruby.py index 6efebf5..201e3ac 100644 --- a/tests/multilspy/test_multilspy_ruby.py +++ b/tests/multilspy/test_multilspy_ruby.py @@ -40,16 +40,12 @@ async def test_multilspy_ruby_rubyland(): 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 - item = result[1] - assert item["relativePath"] == str(PurePath("app/models/feed.rb")) - assert item["range"] == { - "start": {"line": 0, "character": 0}, - "end": {"line": 42, "character": 3}, - } + assert feed_path in list(map(lambda x: x["relativePath"], result)) - result = await lsp.request_references(str(PurePath("app/models/feed.rb")), 0, 7) + result = await lsp.request_references(feed_path, 0, 7) assert isinstance(result, list) assert len(result) == 8 diff --git a/tests/multilspy/test_sync_multilspy_ruby.py b/tests/multilspy/test_sync_multilspy_ruby.py index e9a6541..a03ddfa 100644 --- a/tests/multilspy/test_sync_multilspy_ruby.py +++ b/tests/multilspy/test_sync_multilspy_ruby.py @@ -93,16 +93,12 @@ def test_multilspy_ruby_rubyland() -> None: result = 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 - item = result[1] - assert item["relativePath"] == str(PurePath("app/models/feed.rb")) - assert item["range"] == { - "start": {"line": 0, "character": 0}, - "end": {"line": 42, "character": 3}, - } + assert feed_path in list(map(lambda x: x["relativePath"], result)) - result = lsp.request_references(str(PurePath("app/models/feed.rb")), 0, 7) + result = lsp.request_references(feed_path, 0, 7) assert isinstance(result, list) assert len(result) == 8 From 78695436edee88ef7e6f12e21ba43c5978202dfd Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 17:00:36 -0600 Subject: [PATCH 18/21] Add tmate to debug on the runner --- .github/workflows/test-workflow.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index a1fa103..39c0043 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -34,3 +34,6 @@ jobs: run: | pip install pytest pytest tests/multilspy + - name: Debug test failures + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 From 6ac4ae123ad8d8df07e5da15a850a30511ca47af Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 17:31:55 -0600 Subject: [PATCH 19/21] Upgrade version of solargraph being used --- .../language_servers/solargraph/runtime_dependencies.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/multilspy/language_servers/solargraph/runtime_dependencies.json b/src/multilspy/language_servers/solargraph/runtime_dependencies.json index e6c454e..38cc237 100644 --- a/src/multilspy/language_servers/solargraph/runtime_dependencies.json +++ b/src/multilspy/language_servers/solargraph/runtime_dependencies.json @@ -2,8 +2,8 @@ "_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.50.0.gem", - "installCommand": "gem install solargraph -v 0.50.0", + "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", + "installCommand": "gem install solargraph -v 0.51.1", "binaryName": "solargraph", "archiveType": "gem" } From 06554c6ae0dd3aa4a283ca505ce0894f083ab686 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 17:37:42 -0600 Subject: [PATCH 20/21] Revert "Add tmate to debug on the runner" This reverts commit 78695436edee88ef7e6f12e21ba43c5978202dfd. --- .github/workflows/test-workflow.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index 39c0043..a1fa103 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -34,6 +34,3 @@ jobs: run: | pip install pytest pytest tests/multilspy - - name: Debug test failures - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3 From 232de050e126b05bb56fcb8229e8895f9755c59d Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 27 Jan 2025 17:38:29 -0600 Subject: [PATCH 21/21] Revert "Temporarily trigger tests on all pushes" This reverts commit a9091c24d1061fd5f52a54153ae796ed9a7e5370. --- .github/workflows/test-workflow.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index a1fa103..bda9a25 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -3,6 +3,7 @@ name: Test Multilspy on: # Triggers the workflow on push or pull request events but only for the master branch push: + branches: [ main ] pull_request: jobs: