Enhance Solargraph integration: Rails detection, exclude patterns, and error handling (#439)

* Add Bundler support for Ruby projects

This change enhances the Solargraph language server implementation to properly support Ruby projects using Bundler with vendor/bundle configurations.

Key improvements:
- Detects Bundler projects by checking for Gemfile existence
- Supports both 'bin/bundle' and 'bundle' commands
- Checks Gemfile.lock for solargraph availability
- Uses 'bundle exec solargraph' when appropriate
- Provides clear error messages for missing solargraph in Bundler projects
- Maintains backward compatibility with non-Bundler projects

Fixes permission issues encountered with rbenv/system-managed Ruby installations
when trying to install gems globally.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add timeout configuration for Solargraph language server

This change adds proper timeout handling to the Solargraph Ruby language server
to address initialization timeout issues, especially in Bundler environments.

Key improvements:
- Set 120-second request timeout for LSP requests and initialization
- Add 60-second server readiness timeout with graceful fallback
- Enhanced logging for timeout scenarios
- Better handling of slow initialization in large projects or Bundler environments

This resolves connection timeout issues where Solargraph takes longer than
the default 30-second MCP timeout to fully initialize and become ready.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* I've enhanced the Ruby language support in SolidLSP to bring it up to par with the Python implementation, as you requested.

Here are the key changes I made:

1.  **Enhanced Solargraph Initialization:** I updated the `solargraph.py` language server to use a more reliable event-based mechanism to wait for the server to be ready, similar to `pyright_server.py`. This replaces the previous timeout-based approach, reducing the likelihood of race conditions and improving reliability.

2.  **Improved Initialization Parameters:** The initialization request sent to the Solargraph server now includes a comprehensive set of client capabilities, mirroring the Python implementation. This enables more advanced features and is expected to improve the quality and consistency of the server's responses.

These changes make the Ruby language support more robust and feature-rich without altering the existing tests.

* Enhance Ruby language support to match Python implementation

- Add comprehensive Ruby/Rails exclude patterns for better performance
- Implement Rails project detection with automatic configuration
- Enhance error handling for Ruby, Bundler, and Solargraph installation
- Add Ruby version compatibility checks with warnings for old versions
- Improve directory ignoring for Ruby-specific folders
- Add detailed installation instructions for multiple Ruby managers (rbenv, RVM, asdf)

These improvements bring Ruby language server support up to par with Python,
providing better performance, error messages, and project detection.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Ruff linting error: properly separate string literals

Resolves ISC001 'Implicitly concatenated string literals on one line' by
explicitly separating the f-string and regular string literal in the
error message formatting.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Black/Ruff formatting conflict: unify error message string

Combines the f-string and literal string into a single unified string to
resolve the conflict between Black formatter and Ruff ISC001 rule.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Optimize initializationOptions: remove redundant settings

Remove duplicate settings that match Solargraph defaults:
- diagnostics: true (default: true)
- completion: true (default: true)
- hover: true (default: true)
- formatting: true (default: false, but user preference)

Keep only essential exclude patterns for performance improvement.
This addresses reviewer feedback about unnecessary option duplication.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Optimize Ruby language server capabilities configuration

Based on reviewer feedback in PR #439, optimize the Ruby language server
implementation by removing redundant LSP client capabilities that duplicate
Solargraph's default behavior. This maintains essential functionality while
reducing initialization complexity.

Key optimizations:
- Removed redundant textDocument capabilities that match Solargraph defaults
- Kept only essential capabilities: workspaceEdit, hierarchicalDocumentSymbolSupport, and symbolKind
- Maintained all existing exclude patterns and error handling improvements
- Preserved Rails project detection and Ruby-specific optimizations

The optimized configuration reduces from ~30 lines of capabilities to a minimal
essential set while maintaining full Ruby language server functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add CHANGELOG entry for Ruby language server enhancements

Document the comprehensive Ruby/Solargraph improvements from PR #439:
- Automatic Rails project detection
- Ruby/Rails-specific exclude patterns for performance
- Enhanced error handling with Ruby manager support
- Improved LSP capability negotiation
- Better installation error messages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-08-08 13:54:35 +02:00
1 parent 79fec25b9c
commit 965ecc58b2
2 files changed
+146 -30

No files matched your search

+6
View File
@@ -23,6 +23,12 @@ Status of the `main` branch. Changes prior to the next official version change w
* Hybrid function detection (LSP + regex fallback) for reliable symbolic editing
* Automatic Node.js/npm dependency management
* Cross-platform compatibility including Windows
* Enhance Solargraph (Ruby) integration
* Automatic Rails project detection via config/application.rb, Rakefile, and Gemfile analysis
* Ruby/Rails-specific exclude patterns for improved indexing performance (vendor/, .bundle/, tmp/, log/, coverage/)
* Enhanced error handling with detailed diagnostics and Ruby manager-specific installation instructions (rbenv, RVM, asdf)
* Improved LSP capability negotiation and analysis completion detection
* Better Bundler and Solargraph installation error messages with clear resolution steps
Fixes:
* Ignore `.git` in check for ignored paths and improve performance of `find_all_non_ignored_files`
+140 -30
View File
@@ -7,6 +7,7 @@ import json
import logging
import os
import pathlib
import re
import shutil
import subprocess
import threading
@@ -43,7 +44,7 @@ class Solargraph(SolidLanguageServer):
"ruby",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.analysis_complete = threading.Event()
self.service_ready_event = threading.Event()
self.initialize_searcher_command_available = threading.Event()
self.resolve_main_method_available = threading.Event()
@@ -53,7 +54,18 @@ class Solargraph(SolidLanguageServer):
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["vendor"]
ruby_ignored_dirs = [
"vendor", # Ruby vendor directory
".bundle", # Bundler cache
"tmp", # Temporary files
"log", # Log files
"coverage", # Test coverage reports
".yardoc", # YARD documentation cache
"doc", # Generated documentation
"node_modules", # Node modules (for Rails with JS)
"storage", # Active Storage files (Rails)
]
return super().is_ignored_dirname(dirname) or dirname in ruby_ignored_dirs
@staticmethod
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str:
@@ -62,13 +74,30 @@ class Solargraph(SolidLanguageServer):
"""
# Check if Ruby is installed
try:
result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path)
result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path, text=True)
ruby_version = result.stdout.strip()
logger.log(f"Ruby version: {ruby_version}", logging.INFO)
# Extract version number for compatibility checks
version_match = re.search(r"ruby (\d+)\.(\d+)\.(\d+)", ruby_version)
if version_match:
major, minor, patch = map(int, version_match.groups())
if major < 2 or (major == 2 and minor < 6):
logger.log(f"Warning: Ruby {major}.{minor}.{patch} detected. Solargraph works best with Ruby 2.6+", logging.WARNING)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") from e
error_msg = e.stderr.decode() if e.stderr else "Unknown error"
raise RuntimeError(
f"Error checking Ruby installation: {error_msg}. Please ensure Ruby is properly installed and in PATH."
) from e
except FileNotFoundError as e:
raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") from e
raise RuntimeError(
"Ruby is not installed or not found in PATH. Please install Ruby using one of these methods:\n"
" - Using rbenv: rbenv install 3.0.0 && rbenv global 3.0.0\n"
" - Using RVM: rvm install 3.0.0 && rvm use 3.0.0 --default\n"
" - Using asdf: asdf install ruby 3.0.0 && asdf global ruby 3.0.0\n"
" - System package manager (brew install ruby, apt install ruby, etc.)"
) from e
# Check for Bundler project (Gemfile exists)
gemfile_path = os.path.join(repository_root_path, "Gemfile")
@@ -91,7 +120,12 @@ class Solargraph(SolidLanguageServer):
break
if not bundle_path:
raise RuntimeError("Bundler project detected but 'bundle' command not found. Please install Bundler.")
raise RuntimeError(
"Bundler project detected but 'bundle' command not found. Please install Bundler:\n"
" - gem install bundler\n"
" - Or use your Ruby version manager's bundler installation\n"
" - Ensure the bundle command is in your PATH"
)
# Check if solargraph is in Gemfile.lock
solargraph_in_bundle = False
@@ -142,25 +176,106 @@ class Solargraph(SolidLanguageServer):
return "gem exec solargraph"
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e
error_msg = e.stderr.decode() if e.stderr else str(e)
raise RuntimeError(
f"Failed to check or install Solargraph: {error_msg}\nPlease try installing manually: gem install solargraph"
) from e
else:
raise RuntimeError(
"This appears to be a Bundler project, but solargraph is not available. "
"Please add 'gem \"solargraph\"' to your Gemfile and run 'bundle install'."
)
@staticmethod
def _detect_rails_project(repository_root_path: str) -> bool:
"""
Detect if this is a Rails project by checking for Rails-specific files.
"""
rails_indicators = [
"config/application.rb",
"config/environment.rb",
"app/controllers/application_controller.rb",
"Rakefile",
]
for indicator in rails_indicators:
if os.path.exists(os.path.join(repository_root_path, indicator)):
return True
# Check for Rails in Gemfile
gemfile_path = os.path.join(repository_root_path, "Gemfile")
if os.path.exists(gemfile_path):
try:
with open(gemfile_path) as f:
content = f.read().lower()
if "gem 'rails'" in content or 'gem "rails"' in content:
return True
except Exception:
pass
return False
@staticmethod
def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]:
"""
Get Ruby and Rails-specific exclude patterns for better performance.
"""
base_patterns = [
"**/vendor/**", # Ruby vendor directory (similar to node_modules)
"**/.bundle/**", # Bundler cache
"**/tmp/**", # Temporary files
"**/log/**", # Log files
"**/coverage/**", # Test coverage reports
"**/.yardoc/**", # YARD documentation cache
"**/doc/**", # Generated documentation
"**/.git/**", # Git directory
"**/node_modules/**", # Node modules (for Rails with JS)
"**/public/assets/**", # Rails compiled assets
]
# Add Rails-specific patterns if this is a Rails project
if Solargraph._detect_rails_project(repository_root_path):
rails_patterns = [
"**/public/packs/**", # Webpacker output
"**/public/webpack/**", # Webpack output
"**/storage/**", # Active Storage files
"**/tmp/cache/**", # Rails cache
"**/tmp/pids/**", # Process IDs
"**/tmp/sessions/**", # Session files
"**/tmp/sockets/**", # Socket files
"**/db/*.sqlite3", # SQLite databases
]
base_patterns.extend(rails_patterns)
return base_patterns
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Solargraph Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"capabilities": {},
"trace": "verbose",
exclude_patterns = Solargraph._get_ruby_exclude_patterns(repository_absolute_path)
initialize_params: InitializeParams = { # type: ignore
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
"rootUri": root_uri,
"initializationOptions": {
"exclude": exclude_patterns,
},
"capabilities": {
"workspace": {
"workspaceEdit": {"documentChanges": True},
},
"textDocument": {
"documentSymbol": {
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"trace": "verbose",
"workspaceFolders": [
{
"uri": root_uri,
@@ -184,11 +299,11 @@ class Solargraph(SolidLanguageServer):
return
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()
self.logger.log(f"LSP: language/status: {params}", logging.INFO)
if params.get("type") == "ServiceReady" and params.get("message") == "Service is ready.":
self.logger.log("Solargraph service is ready.", logging.INFO)
self.analysis_complete.set()
self.completions_available.set()
def execute_client_command_handler(params):
return []
@@ -225,19 +340,14 @@ class Solargraph(SolidLanguageServer):
"triggerCharacters": [".", ":", "@"],
}
self.server.notify.initialized({})
self.completions_available.set()
self.server_ready.set()
# Wait for server to be ready with timeout, especially important for Bundler environments
server_ready_timeout = 60.0
self.logger.log(f"Waiting up to {server_ready_timeout} seconds for Solargraph to become ready...", logging.INFO)
if self.server_ready.wait(timeout=server_ready_timeout):
self.logger.log("Solargraph is ready and available for requests", logging.INFO)
# Wait for Solargraph to complete its initial workspace analysis
# This prevents issues by ensuring background tasks finish
self.logger.log("Waiting for Solargraph to complete initial workspace analysis...", logging.INFO)
if self.analysis_complete.wait(timeout=60.0):
self.logger.log("Solargraph initial analysis complete, server ready", logging.INFO)
else:
self.logger.log(
f"Timeout waiting for Solargraph to become ready within {server_ready_timeout} seconds, proceeding anyway. "
"This may indicate slow initialization in Bundler environment or large project indexing.",
logging.WARNING,
)
self.logger.log("Timeout waiting for Solargraph analysis completion, proceeding anyway", logging.WARNING)
# Fallback: assume analysis is complete after timeout
self.analysis_complete.set()
self.completions_available.set()