diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index 58beb1fc6..485c490f2 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -19,8 +19,6 @@ module Tapioca class Addon < ::RubyLsp::Addon extend T::Sig - GEMFILE_LOCK_SNAPSHOT = "tmp/tapioca/.gemfile_lock_snapshot" - sig { void } def initialize super @@ -29,6 +27,7 @@ def initialize @rails_runner_client = T.let(nil, T.nilable(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)) end sig { override.params(global_state: RubyLsp::GlobalState, outgoing_queue: Thread::Queue).void } @@ -47,7 +46,7 @@ def activate(global_state, outgoing_queue) 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__)) - generate_gem_rbis if lockfile_changed? + generate_gem_rbis if git_repo? && lockfile_changed? rescue IncompatibleApiError # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking # changes @@ -113,36 +112,28 @@ def workspace_did_change_watched_files(changes) private + sig { returns(T::Boolean) } + def git_repo? + Dir.exist?(".git") + end + sig { returns(T::Boolean) } def lockfile_changed? - return false unless Dir.exist?(".git") + !fetch_lockfile_diff.empty? + end - gemfile_status = %x(git status --porcelain Gemfile.lock).strip - !gemfile_status.empty? + sig { returns(String) } + def fetch_lockfile_diff + @lockfile_diff = %x(git diff HEAD Gemfile.lock).strip end sig { void } def generate_gem_rbis - current_lockfile = File.read("Gemfile.lock") - snapshot_lockfile = File.read(GEMFILE_LOCK_SNAPSHOT) if File.exist?(GEMFILE_LOCK_SNAPSHOT) - - unless snapshot_lockfile - $stdout.puts("Creating initial Gemfile.lock snapshot at #{GEMFILE_LOCK_SNAPSHOT}") - FileUtils.mkdir_p(File.dirname(GEMFILE_LOCK_SNAPSHOT)) - File.write(GEMFILE_LOCK_SNAPSHOT, current_lockfile) - return - end - - return if current_lockfile == snapshot_lockfile - T.must(@rails_runner_client).delegate_notification( server_addon_name: "Tapioca", request_name: "gem", - snapshot_lockfile: snapshot_lockfile, - current_lockfile: current_lockfile, + diff: T.must(@lockfile_diff), ) - - File.write(GEMFILE_LOCK_SNAPSHOT, current_lockfile) end end end diff --git a/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb b/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb new file mode 100644 index 000000000..8b61ea871 --- /dev/null +++ b/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb @@ -0,0 +1,44 @@ +# typed: true +# frozen_string_literal: true + +module RubyLsp + module Tapioca + class LockfileDiffParser + GEM_PATTERN = /[+-](.*[\w\-]+)\s*\(/ + ADDED_LINE_PATTERN = /^\+.*[\w\-]+ \(.*\)/ + REMOVED_LINE_PATTERN = /^-.*[\w\-]+ \(.*\)/ + + attr_reader :added_or_modified_gems + attr_reader :removed_gems + + def initialize(diff_content) + @diff_content = diff_content + @added_or_modified_gems = parse_added_or_modified_gems + @removed_gems = parse_removed_gems + end + + private + + def parse_added_or_modified_gems + @diff_content + .lines + .filter { |line| line.match?(ADDED_LINE_PATTERN) } + .map { |line| extract_gem(line) } + .uniq + end + + def parse_removed_gems + @diff_content + .lines + .filter { |line| line.match?(REMOVED_LINE_PATTERN) } + .map { |line| extract_gem(line) } + .reject { |gem| @added_or_modified_gems.include?(gem) } + .uniq + end + + def extract_gem(line) + line.match(GEM_PATTERN)[1].strip + end + end + end +end diff --git a/lib/ruby_lsp/tapioca/server_addon.rb b/lib/ruby_lsp/tapioca/server_addon.rb index 0a7599df6..7e3d12bc9 100644 --- a/lib/ruby_lsp/tapioca/server_addon.rb +++ b/lib/ruby_lsp/tapioca/server_addon.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "tapioca/internal" +require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -27,32 +28,23 @@ def dsl(params) end def gem(params) - snapshot_specs = parse_lockfile(params[:snapshot_lockfile]) - current_specs = parse_lockfile(params[:current_lockfile]) + gem_changes = LockfileDiffParser.new(params[:diff]) - removed_gems = snapshot_specs.keys - current_specs.keys - changed_gems = current_specs.select { |name, version| snapshot_specs[name] != version }.keys - - return $stdout.puts("No gem changes detected") if removed_gems.empty? && changed_gems.empty? + removed_gems = gem_changes.removed_gems + added_or_modified_gems = gem_changes.added_or_modified_gems if removed_gems.any? $stdout.puts("Removing RBIs for deleted gems: #{removed_gems.join(", ")}") FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) end - if changed_gems.any? - $stdout.puts("Generating RBIs for changed gems: #{changed_gems.join(", ")}") + if added_or_modified_gems.any? + $stdout.puts("Generating RBIs for added or modified gems: #{added_or_modified_gems.join(", ")}") load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests - ::Tapioca::Cli.start(["gem"] + changed_gems) + ::Tapioca::Cli.start(["gem"] + added_or_modified_gems) end end - - def parse_lockfile(content) - return {} if content.to_s.empty? - - Bundler::LockfileParser.new(content).specs.to_h { |spec| [spec.name, spec.version.to_s] } - end end end end