diff --git a/jekyll/add-ons.markdown b/jekyll/add-ons.markdown index 2d86a216b..0506b5961 100644 --- a/jekyll/add-ons.markdown +++ b/jekyll/add-ons.markdown @@ -61,6 +61,10 @@ The Ruby LSP discovers add-ons based on the existence of an `addon.rb` file plac example, `my_gem/lib/ruby_lsp/my_gem/addon.rb`. This file must declare the add-on class, which can be used to perform any necessary activation when the server starts. +{: .note } +Projects can also define their own private add-ons for functionality that only applies to a particular application. As +long as a file matching `ruby_lsp/**/addon.rb` exists inside of the workspace (not necessarily at the root), it will be +loaded by the Ruby LSP. ```ruby # frozen_string_literal: true diff --git a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb index eee3fd2e8..e3c4c9b77 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb @@ -239,6 +239,8 @@ def initial_excluded_gems excluded.uniq! excluded.map(&:name) + rescue Bundler::GemfileNotFound + [] end end end diff --git a/lib/ruby_lsp/addon.rb b/lib/ruby_lsp/addon.rb index ed97c4ac1..ac4ce6253 100644 --- a/lib/ruby_lsp/addon.rb +++ b/lib/ruby_lsp/addon.rb @@ -65,6 +65,13 @@ def load_addons(global_state, outgoing_queue) e end + project_addons = Dir.glob(File.join(global_state.workspace_path, "**", "ruby_lsp/**/addon.rb")) + project_addons.each do |addon_path| + require File.expand_path(addon_path) + rescue => e + errors << e + end + # Instantiate all discovered addon classes self.addons = addon_classes.map(&:new) self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) } diff --git a/lib/ruby_lsp/test_helper.rb b/lib/ruby_lsp/test_helper.rb index d01d2dc3b..9784577bc 100644 --- a/lib/ruby_lsp/test_helper.rb +++ b/lib/ruby_lsp/test_helper.rb @@ -7,6 +7,12 @@ module RubyLsp module TestHelper extend T::Sig + sig { void } + def before_setup + @mutex = T.let(Mutex.new, T.nilable(Mutex)) + super + end + sig do type_parameters(:T) .params( @@ -42,12 +48,18 @@ def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typec RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source, ) - server.load_addons if load_addons + + if load_addons + T.must(@mutex).synchronize { server.load_addons } + end + block.call(server, uri) ensure if load_addons - RubyLsp::Addon.addons.each(&:deactivate) - RubyLsp::Addon.addons.clear + T.must(@mutex).synchronize do + RubyLsp::Addon.addons.each(&:deactivate) + RubyLsp::Addon.addons.clear + end end T.must(server).run_shutdown end diff --git a/test/addon_test.rb b/test/addon_test.rb index f3040c81d..66e737a93 100644 --- a/test/addon_test.rb +++ b/test/addon_test.rb @@ -131,5 +131,40 @@ def test_addons_receive_settings ensure T.must(outgoing_queue).close end + + def test_project_specific_addons + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + addon_dir = File.join(dir, "lib", "ruby_lsp", "test_addon") + FileUtils.mkdir_p(addon_dir) + File.write(File.join(addon_dir, "addon.rb"), <<~RUBY) + class ProjectAddon < RubyLsp::Addon + attr_reader :hello + + def activate(global_state, outgoing_queue) + @hello = true + end + + def name + "Project Addon" + end + end + RUBY + + queue = Thread::Queue.new + global_state = GlobalState.new + global_state.apply_options({ + workspaceFolders: [{ uri: URI::Generic.from_path(path: dir).to_s }], + }) + Addon.load_addons(global_state, queue) + + addon = Addon.get("Project Addon") + assert_equal("Project Addon", addon.name) + assert_predicate(T.unsafe(addon), :hello) + ensure + T.must(queue).close + end + end + end end end diff --git a/test/requests/code_lens_expectations_test.rb b/test/requests/code_lens_expectations_test.rb index ad4033ece..d9ab2ed41 100644 --- a/test/requests/code_lens_expectations_test.rb +++ b/test/requests/code_lens_expectations_test.rb @@ -180,8 +180,6 @@ class Test < Minitest::Test; end assert_match("Run In Terminal", response[1].command.title) assert_match("Debug", response[2].command.title) assert_match("Run Test", response[3].command.title) - ensure - RubyLsp::Addon.addon_classes.clear end end end