From 4e478a2b4ad1574dc88fd06a1c30d776d56d6074 Mon Sep 17 00:00:00 2001 From: m3thom <39703347+m3thom@users.noreply.github.com> Date: Fri, 15 May 2026 11:52:16 +0700 Subject: [PATCH 1/2] feat(ruby): allow vendored engine indexing --- src/solidlsp/language_servers/ruby_lsp.py | 97 +++++++++++++++++++++- test/solidlsp/ruby/test_ruby_lsp_config.py | 67 +++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 test/solidlsp/ruby/test_ruby_lsp_config.py diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index fd72b2890..aff02c049 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -6,6 +6,11 @@ - 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 repository-relative paths under ``vendor/`` + that should remain indexed by ruby-lsp. Example: + ``["vendor/engines"]``. When set, Serena replaces the blanket + ``**/vendor/**`` exclusion with exclusions for the other top-level + ``vendor`` subdirectories it finds in the repository. """ import json @@ -55,7 +60,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 +75,15 @@ 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.""" + normalized_path = pathlib.PurePosixPath(pathlib.Path(relative_path).as_posix()) + if self._path_contains_vendor_directory(normalized_path): + if not self._is_included_vendor_path(normalized_path): + 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 +311,88 @@ def _detect_rails_project(repository_root_path: str) -> bool: return False + def _get_vendor_include_roots(self) -> set[str]: + """Return top-level vendor directory names that should remain indexed.""" + vendor_include_paths = self._solidlsp_settings.get_ls_specific_settings(Language.RUBY).get("vendor_include_paths", []) + vendor_include_roots: set[str] = set() + if isinstance(vendor_include_paths, list): + for path in vendor_include_paths: + normalized_path = pathlib.PurePosixPath(str(path).strip("/")) + if len(normalized_path.parts) >= 2 and normalized_path.parts[0] == "vendor": + vendor_include_roots.add(normalized_path.parts[1]) + else: + log.warning("Ignoring invalid ruby.vendor_include_paths entry %r; expected a path under vendor/.", path) + elif vendor_include_paths: + log.warning("Ignoring ruby.vendor_include_paths because it is not a list: %r", vendor_include_paths) + return vendor_include_roots + @staticmethod - def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]: + def _path_contains_vendor_directory(relative_path: pathlib.PurePosixPath) -> bool: + """Return whether the path traverses through any vendor directory.""" + return "vendor" in relative_path.parts + + def _is_included_vendor_path(self, relative_path: pathlib.PurePosixPath) -> bool: + """Return whether the path belongs to an allowlisted vendor subtree only.""" + vendor_include_roots = self._get_vendor_include_roots() + if not vendor_include_roots: + return False + + found_included_vendor_root = False + for index, part in enumerate(relative_path.parts): + if part != "vendor": + continue + + if index != 0: + return False + + if index + 1 >= len(relative_path.parts): + return False + + if relative_path.parts[index + 1] not in vendor_include_roots: + return False + + found_included_vendor_root = True + + return found_included_vendor_root + + def _get_vendor_exclude_patterns(self, repository_root_path: str) -> list[str]: + """Return vendor-specific exclude patterns honoring allowlisted subtrees.""" + vendor_include_roots = self._get_vendor_include_roots() + if not vendor_include_roots: + return ["**/vendor/**"] + + vendor_patterns: set[str] = set() + repository_root = pathlib.Path(repository_root_path) + root_vendor_dir = repository_root / "vendor" + + if root_vendor_dir.is_dir(): + for child in root_vendor_dir.iterdir(): + if child.name not in vendor_include_roots: + vendor_patterns.add(f"vendor/{child.name}/**") + + for current_root, dirnames, _filenames in os.walk(repository_root_path): + current_root_path = pathlib.Path(current_root) + relative_root = current_root_path.relative_to(repository_root).as_posix() + + if relative_root == ".": + continue + + if current_root_path.name == "vendor": + relative_vendor_path = pathlib.PurePosixPath(relative_root) + if relative_vendor_path.parts == ("vendor",): + continue + + if not self._is_included_vendor_path(relative_vendor_path): + vendor_patterns.add(f"{relative_vendor_path.as_posix()}/**") + dirnames[:] = [] + + return sorted(vendor_patterns) + + 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 +404,9 @@ def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]: "**/public/assets/**", # Rails compiled assets ] + # keeping vendor/engines while excluding every other vendored subtree + base_patterns.extend(self._get_vendor_exclude_patterns(repository_root_path)) + # 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..be35aba1d --- /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/blog/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) From a014b1f606458d43cda0eb4c767e0e293090c5b8 Mon Sep 17 00:00:00 2001 From: m3thom <39703347+m3thom@users.noreply.github.com> Date: Fri, 15 May 2026 14:43:16 +0700 Subject: [PATCH 2/2] refactor(ruby): simplify vendor include paths --- src/solidlsp/language_servers/ruby_lsp.py | 101 +++------------------ test/solidlsp/ruby/test_ruby_lsp_config.py | 2 +- 2 files changed, 15 insertions(+), 88 deletions(-) diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index aff02c049..d5f1b5b2c 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -6,11 +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 repository-relative paths under ``vendor/`` - that should remain indexed by ruby-lsp. Example: - ``["vendor/engines"]``. When set, Serena replaces the blanket - ``**/vendor/**`` exclusion with exclusions for the other top-level - ``vendor`` subdirectories it finds in the repository. + - vendor_include_paths: List of top-level paths under ``vendor/`` that + should remain indexed. Example: ``["vendor/engines"]``. """ import json @@ -77,9 +74,10 @@ def is_ignored_dirname(self, dirname: str) -> bool: def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: """Override to keep only configured vendor subtrees visible to Serena.""" - normalized_path = pathlib.PurePosixPath(pathlib.Path(relative_path).as_posix()) - if self._path_contains_vendor_directory(normalized_path): - if not self._is_included_vendor_path(normalized_path): + 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) @@ -311,83 +309,6 @@ def _detect_rails_project(repository_root_path: str) -> bool: return False - def _get_vendor_include_roots(self) -> set[str]: - """Return top-level vendor directory names that should remain indexed.""" - vendor_include_paths = self._solidlsp_settings.get_ls_specific_settings(Language.RUBY).get("vendor_include_paths", []) - vendor_include_roots: set[str] = set() - if isinstance(vendor_include_paths, list): - for path in vendor_include_paths: - normalized_path = pathlib.PurePosixPath(str(path).strip("/")) - if len(normalized_path.parts) >= 2 and normalized_path.parts[0] == "vendor": - vendor_include_roots.add(normalized_path.parts[1]) - else: - log.warning("Ignoring invalid ruby.vendor_include_paths entry %r; expected a path under vendor/.", path) - elif vendor_include_paths: - log.warning("Ignoring ruby.vendor_include_paths because it is not a list: %r", vendor_include_paths) - return vendor_include_roots - - @staticmethod - def _path_contains_vendor_directory(relative_path: pathlib.PurePosixPath) -> bool: - """Return whether the path traverses through any vendor directory.""" - return "vendor" in relative_path.parts - - def _is_included_vendor_path(self, relative_path: pathlib.PurePosixPath) -> bool: - """Return whether the path belongs to an allowlisted vendor subtree only.""" - vendor_include_roots = self._get_vendor_include_roots() - if not vendor_include_roots: - return False - - found_included_vendor_root = False - for index, part in enumerate(relative_path.parts): - if part != "vendor": - continue - - if index != 0: - return False - - if index + 1 >= len(relative_path.parts): - return False - - if relative_path.parts[index + 1] not in vendor_include_roots: - return False - - found_included_vendor_root = True - - return found_included_vendor_root - - def _get_vendor_exclude_patterns(self, repository_root_path: str) -> list[str]: - """Return vendor-specific exclude patterns honoring allowlisted subtrees.""" - vendor_include_roots = self._get_vendor_include_roots() - if not vendor_include_roots: - return ["**/vendor/**"] - - vendor_patterns: set[str] = set() - repository_root = pathlib.Path(repository_root_path) - root_vendor_dir = repository_root / "vendor" - - if root_vendor_dir.is_dir(): - for child in root_vendor_dir.iterdir(): - if child.name not in vendor_include_roots: - vendor_patterns.add(f"vendor/{child.name}/**") - - for current_root, dirnames, _filenames in os.walk(repository_root_path): - current_root_path = pathlib.Path(current_root) - relative_root = current_root_path.relative_to(repository_root).as_posix() - - if relative_root == ".": - continue - - if current_root_path.name == "vendor": - relative_vendor_path = pathlib.PurePosixPath(relative_root) - if relative_vendor_path.parts == ("vendor",): - continue - - if not self._is_included_vendor_path(relative_vendor_path): - vendor_patterns.add(f"{relative_vendor_path.as_posix()}/**") - dirnames[:] = [] - - return sorted(vendor_patterns) - def _get_ruby_exclude_patterns(self, repository_root_path: str) -> list[str]: """ Get Ruby and Rails-specific exclude patterns for better performance. @@ -404,8 +325,14 @@ def _get_ruby_exclude_patterns(self, repository_root_path: str) -> list[str]: "**/public/assets/**", # Rails compiled assets ] - # keeping vendor/engines while excluding every other vendored subtree - base_patterns.extend(self._get_vendor_exclude_patterns(repository_root_path)) + 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): diff --git a/test/solidlsp/ruby/test_ruby_lsp_config.py b/test/solidlsp/ruby/test_ruby_lsp_config.py index be35aba1d..3a372a55a 100644 --- a/test/solidlsp/ruby/test_ruby_lsp_config.py +++ b/test/solidlsp/ruby/test_ruby_lsp_config.py @@ -35,7 +35,7 @@ def test_ruby_lsp_can_keep_vendor_engines_indexed(tmp_path: Path) -> None: assert "vendor/bundle/**" in patterns assert "vendor/cache/**" in patterns assert "vendor/engines/**" not in patterns - assert "vendor/engines/blog/vendor/**" in patterns + assert "vendor/engines/**/vendor/**" in patterns def test_ruby_lsp_ignores_non_allowlisted_vendor_paths(tmp_path: Path) -> None: