diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index fd72b2890..d5f1b5b2c 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -6,6 +6,8 @@ - ruby_lsp_version: Override the pinned ruby-lsp gem version installed by Serena when no project-local or global ruby-lsp is already available (default: the bundled Serena version). + - vendor_include_paths: List of top-level paths under ``vendor/`` that + should remain indexed. Example: ``["vendor/engines"]``. """ import json @@ -55,7 +57,6 @@ def __init__(self, config: LanguageServerConfig, repository_root_path: str, soli def is_ignored_dirname(self, dirname: str) -> bool: """Override to ignore Ruby-specific directories that cause performance issues.""" ruby_ignored_dirs = [ - "vendor", # Ruby vendor directory ".bundle", # Bundler cache "tmp", # Temporary files "log", # Log files @@ -71,6 +72,16 @@ def is_ignored_dirname(self, dirname: str) -> bool: ] return super().is_ignored_dirname(dirname) or dirname in ruby_ignored_dirs + def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: + """Override to keep only configured vendor subtrees visible to Serena.""" + parts = pathlib.PurePosixPath(pathlib.Path(relative_path).as_posix()).parts + vendor_include_roots = {pathlib.PurePosixPath(path).parts[1] for path in self._custom_settings.get("vendor_include_paths", [])} + if "vendor" in parts: + if len(parts) < 2 or parts[:1] != ("vendor",) or parts[1] not in vendor_include_roots or "vendor" in parts[2:]: + return True + + return super().is_ignored_path(relative_path, ignore_unsupported_files) + @override def _get_wait_time_for_cross_file_referencing(self) -> float: """Override to provide optimal wait time for ruby-lsp cross-file reference resolution. @@ -298,13 +309,11 @@ def _detect_rails_project(repository_root_path: str) -> bool: return False - @staticmethod - def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]: + def _get_ruby_exclude_patterns(self, repository_root_path: str) -> list[str]: """ Get Ruby and Rails-specific exclude patterns for better performance. """ base_patterns = [ - "**/vendor/**", # Ruby vendor directory "**/.bundle/**", # Bundler cache "**/tmp/**", # Temporary files "**/log/**", # Log files @@ -316,6 +325,15 @@ def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]: "**/public/assets/**", # Rails compiled assets ] + vendor_include_roots = {pathlib.PurePosixPath(path).parts[1] for path in self._custom_settings.get("vendor_include_paths", [])} + if vendor_include_roots: + vendor_dir = pathlib.Path(repository_root_path) / "vendor" + if vendor_dir.is_dir(): + base_patterns.extend(f"vendor/{child.name}/**" for child in vendor_dir.iterdir() if child.name not in vendor_include_roots) + base_patterns.extend(f"vendor/{root}/**/vendor/**" for root in vendor_include_roots) + else: + base_patterns.append("**/vendor/**") + # Add Rails-specific patterns if this is a Rails project if RubyLsp._detect_rails_project(repository_root_path): base_patterns.extend( diff --git a/test/solidlsp/ruby/test_ruby_lsp_config.py b/test/solidlsp/ruby/test_ruby_lsp_config.py new file mode 100644 index 000000000..3a372a55a --- /dev/null +++ b/test/solidlsp/ruby/test_ruby_lsp_config.py @@ -0,0 +1,67 @@ +from pathlib import Path + +from solidlsp.language_servers.ruby_lsp import RubyLsp +from solidlsp.ls_config import Language +from solidlsp.settings import SolidLSPSettings + + +def _build_ruby_lsp(settings: SolidLSPSettings) -> RubyLsp: + language_server = RubyLsp.__new__(RubyLsp) + language_server._solidlsp_settings = settings + language_server.repository_root_path = "" + return language_server + + +def test_ruby_lsp_excludes_vendor_by_default(tmp_path: Path) -> None: + language_server = _build_ruby_lsp(SolidLSPSettings()) + + patterns = language_server._get_ruby_exclude_patterns(str(tmp_path)) + + assert "**/vendor/**" in patterns + + +def test_ruby_lsp_can_keep_vendor_engines_indexed(tmp_path: Path) -> None: + (tmp_path / "vendor" / "engines").mkdir(parents=True) + (tmp_path / "vendor" / "bundle").mkdir(parents=True) + (tmp_path / "vendor" / "cache").mkdir(parents=True) + (tmp_path / "vendor" / "engines" / "blog" / "vendor" / "bundle").mkdir(parents=True) + + settings = SolidLSPSettings(ls_specific_settings={Language.RUBY: {"vendor_include_paths": ["vendor/engines"]}}) + language_server = _build_ruby_lsp(settings) + + patterns = language_server._get_ruby_exclude_patterns(str(tmp_path)) + + assert "**/vendor/**" not in patterns + assert "vendor/bundle/**" in patterns + assert "vendor/cache/**" in patterns + assert "vendor/engines/**" not in patterns + assert "vendor/engines/**/vendor/**" in patterns + + +def test_ruby_lsp_ignores_non_allowlisted_vendor_paths(tmp_path: Path) -> None: + bundle_file = tmp_path / "vendor" / "bundle" / "tool.rb" + bundle_file.parent.mkdir(parents=True) + bundle_file.write_text("class Tool; end\n") + + nested_vendor_file = tmp_path / "vendor" / "engines" / "blog" / "vendor" / "cache" / "tool.rb" + nested_vendor_file.parent.mkdir(parents=True) + nested_vendor_file.write_text("class Tool; end\n") + + settings = SolidLSPSettings(ls_specific_settings={Language.RUBY: {"vendor_include_paths": ["vendor/engines"]}}) + language_server = _build_ruby_lsp(settings) + language_server.repository_root_path = str(tmp_path) + + assert language_server.is_ignored_path("vendor/bundle/tool.rb") + assert language_server.is_ignored_path("vendor/engines/blog/vendor/cache/tool.rb") + + +def test_ruby_lsp_keeps_allowlisted_vendor_engines_paths(tmp_path: Path) -> None: + engine_file = tmp_path / "vendor" / "engines" / "blog" / "app" / "models" / "post.rb" + engine_file.parent.mkdir(parents=True) + engine_file.write_text("class Post; end\n") + + settings = SolidLSPSettings(ls_specific_settings={Language.RUBY: {"vendor_include_paths": ["vendor/engines"]}}) + language_server = _build_ruby_lsp(settings) + language_server.repository_root_path = str(tmp_path) + + assert not language_server.is_ignored_path("vendor/engines/blog/app/models/post.rb", ignore_unsupported_files=False)