From 086f75c6e1af7e255e84c9aca1e80d4d9c026d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 7 Nov 2026 16:27:59 +0300 Subject: [PATCH] Honor editor indexing exclusions with Rubydex --- lib/ruby_lsp/global_state.rb | 5 ++++ lib/ruby_lsp/server.rb | 44 ++++++++++++++++++++++++++++++++++++ test/server_test.rb | 33 +++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/lib/ruby_lsp/global_state.rb b/lib/ruby_lsp/global_state.rb index 7decb9e9f5..60842cb78e 100644 --- a/lib/ruby_lsp/global_state.rb +++ b/lib/ruby_lsp/global_state.rb @@ -48,6 +48,9 @@ class GlobalState #: String? attr_reader :telemetry_machine_id + #: Hash[Symbol, untyped]? + attr_reader :indexing_options + #: -> void def initialize @workspace_uri = URI::Generic.from_path(path: Dir.pwd) #: URI::Generic @@ -71,6 +74,7 @@ def initialize @enabled_feature_flags = {} #: Hash[Symbol, bool] @mutex = Mutex.new #: Mutex @telemetry_machine_id = nil #: String? + @indexing_options = nil #: Hash[Symbol, untyped]? @feature_configuration = { inlayHint: RequestConfig.new({ enableAll: false, @@ -113,6 +117,7 @@ def active_linters #: (Hash[Symbol, untyped] options) -> Array[Notification] def apply_options(options) notifications = [] + @indexing_options = options.dig(:initializationOptions, :indexing) direct_dependencies = gather_direct_dependencies all_dependencies = gather_direct_and_indirect_dependencies workspace_uri = options.dig(:workspaceFolders, 0, :uri) diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 6c939bccd2..b1e573a018 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -1464,11 +1464,55 @@ def benchmark(label, &block) #: () -> void def load_rubydex_config @global_state.graph.load_config(Rubydex::Config.load(@global_state.workspace_path)) + apply_editor_indexing_exclusions rescue Rubydex::ConfigError => e send_message(Notification.window_show_message( "Error loading rubydex config: #{e.message}", type: Constant::MessageType::ERROR, )) end + + #: -> void + def apply_editor_indexing_exclusions + options = @global_state.indexing_options + return unless options + + excluded_patterns = options[:excludedPatterns] + excluded_patterns = [] unless excluded_patterns.is_a?(Array) + + excluded_gems = options[:excludedGems] + excluded_gems = [] unless excluded_gems.is_a?(Array) + included_gems = options[:includedGems] + included_gems = [] unless included_gems.is_a?(Array) + + patterns = excluded_patterns.grep(String).map { |pattern| normalize_indexing_pattern(pattern) } + patterns.concat(excluded_gem_patterns(excluded_gems.grep(String) - included_gems.grep(String))) + + if patterns.any? + # Rubydex exposes this C API publicly, but its generated RBI marks it private. + @global_state.graph.send(:exclude_patterns, patterns) + end + end + + #: (Array[String] gem_names) -> Array[String] + def excluded_gem_patterns(gem_names) + locked_gems = Bundler.locked_gems&.specs + return [] unless locked_gems + + gem_names.filter_map do |gem_name| + locked_spec = locked_gems.find { |spec| spec.name == gem_name } + next unless locked_spec + + spec = Gem::Specification.find_by_name(locked_spec.name, "= #{locked_spec.version}") + normalize_indexing_pattern(File.join(spec.full_gem_path, "**")) + rescue Gem::MissingSpecError + nil + end + end + + #: (String pattern) -> String + def normalize_indexing_pattern(pattern) + File.expand_path(pattern.tr("\\", "/"), @global_state.workspace_path).tr("\\", "/") + end end end diff --git a/test/server_test.rb b/test/server_test.rb index a29cfdd97c..20cf1bfe71 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -188,6 +188,39 @@ def test_server_info_includes_formatter assert_equal("rubocop_internal", hash.dig("formatter")) end + def test_applies_editor_indexing_exclusions_with_rubydex + Dir.mktmpdir do |workspace| + excluded_file = File.join(workspace, "excluded.rb") + included_file = File.join(workspace, "included.rb") + File.write(excluded_file, "class ExcludedFromIndex; end\n") + File.write(included_file, "class IncludedInIndex; end\n") + minitest_file = gem_path("minitest").join("lib", "minitest.rb").to_s + + @server.process_message({ + id: 1, + method: "initialize", + params: { + initializationOptions: { + indexing: { + excludedPatterns: ["excluded.rb"], + excludedGems: ["minitest"], + }, + }, + workspaceFolders: [{ uri: URI::Generic.from_path(path: workspace).to_s }], + }, + }) + + @server.send(:load_rubydex_config) + graph = @server.global_state.graph + graph.index_all([excluded_file, included_file, minitest_file]) + graph.resolve + + assert_empty(graph.search("ExcludedFromIndex").to_a) + assert_equal(["IncludedInIndex"], graph.search("IncludedInIndex").map(&:name).to_a) + assert_empty(graph.search("Minitest").to_a) + end + end + def test_formatting_errors_push_window_notification @server.global_state.expects(:formatter).raises(StandardError, "boom").once