class RubyLsp::Tapioca::Addon
def activate(global_state, outgoing_queue)
def activate(global_state, outgoing_queue) @global_state = global_state return unless @global_state.enabled_feature?(:tapiocaAddon) @index = @global_state.index @outgoing_queue = outgoing_queue Thread.new do # Get a handle to the Rails add-on's runtime client. The call to `rails_runner_client` will block this thread # until the server has finished booting, but it will not block the main LSP. This has to happen inside of a # thread addon = T.cast(::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.4.0", "< 0.5"), ::RubyLsp::Rails::Addon) @rails_runner_client = addon.rails_runner_client @outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}") @rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__)) @rails_runner_client.delegate_notification( server_addon_name: "Tapioca", request_name: "load_compilers_and_extensions", workspace_path: @global_state.workspace_path, ) send_usage_telemetry("activated") run_gem_rbi_check rescue IncompatibleApiError send_usage_telemetry("incompatible_api_error") # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking # changes @outgoing_queue << Notification.window_log_message( "IncompatibleApiError: Cannot activate Tapioca LSP add-on", type: Constant::MessageType::WARNING, ) end end
def deactivate
def deactivate end
def file_updated?(change, path)
def file_updated?(change, path) case change[:type] when Constant::FileChangeType::CREATED @file_checksums[path] = Zlib.crc32(File.read(path)).to_s return true when Constant::FileChangeType::CHANGED current_checksum = Zlib.crc32(File.read(path)).to_s if @file_checksums[path] == current_checksum T.must(@outgoing_queue) << Notification.window_log_message( "File has not changed. Skipping #{path}", type: Constant::MessageType::INFO, ) else @file_checksums[path] = current_checksum return true end when Constant::FileChangeType::DELETED @file_checksums.delete(path) else T.must(@outgoing_queue) << Notification.window_log_message( "Unexpected file change type: #{change[:type]}", type: Constant::MessageType::WARNING, ) end false end
def initialize
def initialize super @global_state = T.let(nil, T.nilable(RubyLsp::GlobalState)) @rails_runner_client = T.let(Rails::NullClient.new, RubyLsp::Rails::RunnerClient) @index = T.let(nil, T.nilable(RubyIndexer::Index)) @file_checksums = T.let({}, T::Hash[String, String]) @lockfile_diff = T.let(nil, T.nilable(String)) @outgoing_queue = T.let(nil, T.nilable(Thread::Queue)) end
def name
def name "Tapioca" end
def run_gem_rbi_check
def run_gem_rbi_check gem_rbi_check = RunGemRbiCheck.new(T.must(@global_state).workspace_path) gem_rbi_check.run T.must(@outgoing_queue) << Notification.window_log_message( gem_rbi_check.stdout, ) unless gem_rbi_check.stdout.empty? T.must(@outgoing_queue) << Notification.window_log_message( gem_rbi_check.stderr, type: Constant::MessageType::WARNING, ) unless gem_rbi_check.stderr.empty? end
def send_usage_telemetry(feature_name)
def send_usage_telemetry(feature_name) return unless @outgoing_queue && @global_state # Telemetry is not captured by default even if events are produced by the server # See https://github.com/Shopify/ruby-lsp/tree/main/vscode#telemetry @outgoing_queue << Notification.telemetry({ eventName: "tapioca_addon.feature_usage", type: "data", data: { type: "counter", attributes: { label: feature_name, machineId: @global_state.telemetry_machine_id, }, }, }) end
def version
def version "0.1.3" end
def workspace_did_change_watched_files(changes)
def workspace_did_change_watched_files(changes) return unless @global_state&.enabled_feature?(:tapiocaAddon) return unless @rails_runner_client.connected? has_route_change = T.let(false, T::Boolean) has_fixtures_change = T.let(false, T::Boolean) needs_compiler_reload = T.let(false, T::Boolean) constants = changes.flat_map do |change| path = URI(change[:uri]).to_standardized_path next unless file_updated?(change, path) if File.fnmatch("**/fixtures/**/*.yml{,.erb}", path, File::FNM_PATHNAME | File::FNM_EXTGLOB) has_fixtures_change = true next end if File.basename(path) == "routes.rb" || File.fnmatch?("**/routes/**/*.rb", path, File::FNM_PATHNAME) has_route_change = true next end next if File.fnmatch?("**/{test,spec,features}/**/*", path, File::FNM_PATHNAME | File::FNM_EXTGLOB) if File.fnmatch?("**/tapioca/**/compilers/**/*.rb", path, File::FNM_PATHNAME) needs_compiler_reload = true next end entries = T.must(@index).entries_for(change[:uri]) next unless entries entries.filter_map do |entry| entry.name if entry.class == RubyIndexer::Entry::Class || entry.class == RubyIndexer::Entry::Module end end.compact return if constants.empty? && !has_route_change && !has_fixtures_change && !needs_compiler_reload @rails_runner_client.trigger_reload if needs_compiler_reload @rails_runner_client.delegate_notification( server_addon_name: "Tapioca", request_name: "reload_workspace_compilers", workspace_path: @global_state.workspace_path, ) end if has_route_change send_usage_telemetry("route_dsl") @rails_runner_client.delegate_notification(server_addon_name: "Tapioca", request_name: "route_dsl") end if has_fixtures_change send_usage_telemetry("fixtures_dsl") @rails_runner_client.delegate_notification(server_addon_name: "Tapioca", request_name: "fixtures_dsl") end if constants.any? send_usage_telemetry("dsl") @rails_runner_client.delegate_notification( server_addon_name: "Tapioca", request_name: "dsl", constants: constants, ) end end