diff --git a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb index 46afacca99..70a8407d4e 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb @@ -39,7 +39,7 @@ def initialize # We start the included patterns with only the non excluded directories so that we can avoid paying the price of # traversing large directories that don't include Ruby files like `node_modules` - @included_patterns = ["{#{top_level_directories.join(",")}}/**/*.rb", "*.rb"] #: Array[String] + @included_patterns = ["*.rb", *top_level_directories.map {|name| "#{name}/**/*.rb"}] #: Array[String] @excluded_magic_comments = [ "frozen_string_literal:", "typed:", @@ -56,7 +56,7 @@ def initialize end #: -> Array[URI::Generic] - def indexable_uris + def indexable_uris(&logging) excluded_gems = @excluded_gems - @included_gems locked_gems = Bundler.locked_gems&.specs @@ -68,7 +68,16 @@ def indexable_uris uris = @included_patterns.flat_map do |pattern| load_path_entry = nil #: String? - Dir.glob(File.join(@workspace_path, pattern), flags).map! do |path| + begin + glob_pattern = File.join(@workspace_path, pattern) + glob_results = Dir.glob(glob_pattern, flags) + rescue StandardError => error + message = "Indexation error on included_pattern: '#{pattern}': (#{error.class.name} - #{error.message})" + logging.call(error: message) + next + end + + glob_results.map! do |path| # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current @@ -79,7 +88,7 @@ def indexable_uris URI::Generic.from_path(path: path, load_path_entry: load_path_entry) end - end + end.compact # If the patterns are relative, we make it relative to the workspace path. If they are absolute, then we shouldn't # concatenate anything @@ -177,8 +186,11 @@ def apply_config(config) @excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"] @included_gems.concat(config["included_gems"]) if config["included_gems"] - @excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"] @included_patterns.concat(config["included_patterns"]) if config["included_patterns"] + if config["excluded_patterns"] + @excluded_patterns.concat(config["excluded_patterns"]) + @included_patterns -= config["excluded_patterns"] + end @excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"] end diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index 4cf8895d69..21a0ce1556 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb @@ -354,7 +354,9 @@ def resolve(name, nesting, seen_names = []) # indexing progress. That block is invoked with the current progress percentage and should return `true` to continue # indexing or `false` to stop indexing. #: (?uris: Array[URI::Generic]) ?{ (Integer progress) -> bool } -> void - def index_all(uris: @configuration.indexable_uris, &block) + def index_all(uris: [], &block) + uris = @configuration.indexable_uris(&block) if uris.empty? + # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this # behavior and can take appropriate action. diff --git a/lib/ruby_lsp/addon.rb b/lib/ruby_lsp/addon.rb index c346d6ea61..a1dd23cbfb 100644 --- a/lib/ruby_lsp/addon.rb +++ b/lib/ruby_lsp/addon.rb @@ -49,14 +49,27 @@ def inherited(child_class) # Discovers and loads all add-ons. Returns a list of errors when trying to require add-ons #: (GlobalState global_state, Thread::Queue outgoing_queue, ?include_project_addons: bool) -> Array[StandardError] - def load_addons(global_state, outgoing_queue, include_project_addons: true) + def load_addons(global_state, outgoing_queue, include_project_addons: true, &logging) # Require all add-ons entry points, which should be placed under # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb` or in the workspace under # `your_project/ruby_lsp/project_name/addon.rb` addon_files = Gem.find_files("ruby_lsp/**/addon.rb") if include_project_addons - project_addons = Dir.glob("#{global_state.workspace_path}/**/ruby_lsp/**/addon.rb") + workspace_path = global_state.workspace_path + spaces = top_level_directories.map do |space| + "#{workspace_path}/#{space}/**/ruby_lsp/**/addon.rb" + end + spaces << "#{workspace_path}/ruby_lsp/**/addon.rb" + + project_addons = spaces.flat_map do |pattern| + Dir.glob(pattern) + rescue StandardError => error + message = "Addon discovery failed with pattern: '#{pattern}': (#{error.class.name} - #{error.message})" + logging.call(error: message) + next + end.compact + bundle_path = Bundler.bundle_path.to_s gems_dir = Bundler.bundle_path.join("gems") @@ -162,6 +175,18 @@ def depend_on_ruby_lsp!(*version_constraints) "Add-on is not compatible with this version of the Ruby LSP. Skipping its activation" end end + + private + + #: -> Array[String] + def top_level_directories + Dir.glob("#{Dir.pwd}/*").filter_map do |path| + dir_name = File.basename(path) + next unless File.directory?(path) + + dir_name + end + end end #: -> void diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 170c755daa..88317dd9c1 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -172,7 +172,18 @@ def load_addons(include_project_addons: true) # different versions of the same files. We cannot load add-ons if Bundler.setup failed return if @setup_error - errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons) + errors = Addon.load_addons( + @global_state, + @outgoing_queue, + include_project_addons: include_project_addons + ) do |log: nil, error: nil| + send_log_message(log) if log + if error + send_log_message(error, type: Constant::MessageType::ERROR) + send_message(Notification.window_show_message(error, type: Constant::MessageType::ERROR)) + end + end + return if test_mode? if errors.any? @@ -1232,8 +1243,15 @@ def perform_initial_indexing # stuck indexing files Thread.new do begin - @global_state.index.index_all do |percentage| - progress("indexing-progress", percentage) + @global_state.index.index_all do |percentage = nil, log: nil, error: nil| + send_log_message(log) if log + if error + send_log_message(error, type: Constant::MessageType::ERROR) + send_message(Notification.window_show_message(error, type: Constant::MessageType::ERROR)) + end + + progress("indexing-progress", percentage) unless percentage.nil? + true rescue ClosedQueueError # Since we run indexing on a separate thread, it's possible to kill the server before indexing is complete. @@ -1244,6 +1262,8 @@ def perform_initial_indexing rescue StandardError => error message = "Error while indexing (see [troubleshooting steps]" \ "(https://shopify.github.io/ruby-lsp/troubleshooting#indexing)): #{error.message}" + + send_log_message("#{message}\n\n#{error.backtrace.join("\n")}", type: Constant::MessageType::ERROR) send_message(Notification.window_show_message(message, type: Constant::MessageType::ERROR)) end