Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/solargraph/pin_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ def serialize_stdlib_require require, pins
save(stdlib_require_path(require), pins)
end

# Every entry in the running Ruby installation's standard library
# directory, as a name that might be requirable. Not all of them
# resolve to a gemspec or to RBS; callers discard the misses.
#
# @return [Array<String>] possible standard library names
def possible_stdlibs
Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir|
basename = File.basename(file_or_dir)
basename.end_with?('.rb') ? basename[0..-4] : basename
end.sort.uniq
rescue StandardError => e
logger.info { "Failed to list possible stdlibs: #{e.message}" }
[]
end

# @return [String]
def core_path
File.join(work_dir, 'core.ser')
Expand Down
5 changes: 3 additions & 2 deletions lib/solargraph/shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ def gems *names
workspace = Solargraph::Workspace.new('.')

if names.empty?
Gem::Specification.to_a.each { |spec| do_cache spec, rebuild: options[:rebuild] }
$stderr.puts "Documentation cached for all #{Gem::Specification.count} gems."
gemspecs = workspace.gemspecs_to_cache
gemspecs.each { |gemspec| do_cache gemspec, rebuild: options[:rebuild] }
$stderr.puts "Documentation cached for #{gemspecs.length} gems."
else
warn("Caching these gems: #{names}")
names.each do |name|
Expand Down
30 changes: 30 additions & 0 deletions lib/solargraph/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ def find_gem name, version = nil, out: nil
Gem::Specification.find_by_name(name, version)
end

# Gemspecs the bundle depends on directly. Empty when the
# workspace has no resolvable bundle.
#
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def all_gemspecs_from_bundle
gemspecs.all_gemspecs_from_bundle
end

# Every gemspec whose pins this workspace could need: the bundle,
# plus any standard library that resolves to a gemspec in this Ruby
# installation. Candidate stdlib names resolving to nothing are dropped.
#
# @return [Array<Gem::Specification>]
def gemspecs_to_cache
# @sg-ignore Wrong argument type for Solargraph::Workspace::Gemspecs#find_gem: out expected IO, nil, received NilClass
stdlib_gemspecs = PinCache.possible_stdlibs.map { |name| gemspecs.find_gem(name, out: nil) }.compact

# Outside a bundle there is nothing to scope to, so every installed gem
# is a candidate - the same set master always cached.
bundled_gemspecs = all_gemspecs_from_bundle
bundled_gemspecs = Gem::Specification.to_a if bundled_gemspecs.empty?

(bundled_gemspecs + stdlib_gemspecs).uniq { |gemspec| [gemspec.name, gemspec.version] }
end

# Synchronize the workspace from the provided updater.
#
# @param updater [Source::Updater]
Expand Down Expand Up @@ -197,6 +222,11 @@ def gemspec_files

private

# @return [Solargraph::Workspace::Gemspecs]
def gemspecs
@gemspecs ||= Gemspecs.new(directory_or_nil)
end

# The language server configuration (or an empty hash if the workspace was
# not initialized from a server).
#
Expand Down
31 changes: 31 additions & 0 deletions spec/bundle_scoped_gems_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

# `solargraph gems` with no arguments reads the current directory, in either
# cache design, and should document what that workspace can actually load
# rather than every gem version RubyGems can see.
describe Solargraph::Shell do
let(:shell) { described_class.new }
let(:directory) { File.expand_path(File.join('spec', 'fixtures', 'bundle-scoped-gems')) }

# @return [Integer] the count the command reports
def gems_in_fixture
output = Dir.chdir(directory) { capture_both { shell.gems } }
# "cached for all N gems" is the every-installed-gem wording.
count = output[/cached for (?:all )?(\d+) gems/, 1]
raise "no gem count in: #{output}" if count.nil?

count.to_i
end

it 'documents fewer gems than RubyGems can see' do
expect(gems_in_fixture).to be < Gem::Specification.to_a.size
end

it 'documents the gem the workspace bundle resolves' do
gems_in_fixture

pins = Solargraph::ApiMap.load(directory).get_path_pins('Backport')

expect(pins).not_to be_empty
end
end
66 changes: 66 additions & 0 deletions spec/cli_finishes_caching_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

require 'open3'

# `solargraph gems` offers its cache directory as something to keep between CI
# runs, so what it leaves there has to be everything a later editor session
# needs.
describe Solargraph::Shell do
let(:shell) do
described_class.new.tap do |cli|
cli.options = Thor::CoreExt::HashWithIndifferentAccess.new({})
end
end

let(:directory) { File.expand_path(File.join('spec', 'fixtures', 'bundle-scoped-gems')) }

# The directory the command names in its own help text.
#
# @return [String]
def cache_dir
ENV['SOLARGRAPH_CACHE'] || File.join(Dir.home, '.cache', 'solargraph')
end

# Scoped to this Solargraph, so another version's run cannot answer for it.
#
# @return [Array<String>]
def cached_backport_files
Dir.glob(File.join(cache_dir, '**', '*backport*'))
.select { |path| File.file?(path) && path.include?("solargraph-#{Solargraph::VERSION}") }
.sort
end

# Gem pins are memoized for the life of a process, so a load here would
# answer from memory and write nothing whatever the cache holds.
#
# @return [void]
def open_the_workspace_elsewhere
script = "require 'solargraph'; " \
"Solargraph::ApiMap.load(#{directory.inspect}).get_path_pins('Backport')"
output, status = Open3.capture2e(RbConfig.ruby, '-e', script)
raise output unless status.success?
end

# @return [Array<String>] cache entries the editor had to write for itself
def written_by_editor
already_cached = cached_backport_files
open_the_workspace_elsewhere
cached_backport_files - already_cached
end

before { capture_both { shell.uncache('backport') } }

it 'finishes the job when told to cache one gem' do
pending 'https://github.com/apiology/solargraph/pull/103'
Dir.chdir(directory) { capture_both { shell.cache('backport') } }

expect(written_by_editor).to be_empty
end

it 'finishes the job when told to cache the workspace' do
pending 'https://github.com/apiology/solargraph/pull/103'
Dir.chdir(directory) { capture_both { shell.gems } }

expect(written_by_editor).to be_empty
end
end
2 changes: 2 additions & 0 deletions spec/fixtures/bundle-scoped-gems/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gem 'backport'
18 changes: 18 additions & 0 deletions spec/fixtures/bundle-scoped-gems/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
GEM
remote: https://rubygems.org/
specs:
backport (1.2.0)

PLATFORMS
arm64-darwin-24
ruby

DEPENDENCIES
backport

CHECKSUMS
backport (1.2.0)
bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac

BUNDLED WITH
4.0.18
1 change: 1 addition & 0 deletions spec/fixtures/bundle-scoped-gems/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'backport'
12 changes: 12 additions & 0 deletions spec/workspace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,16 @@
described_class.new('./path', config)
end.not_to raise_error
end

describe '#gemfile?' do
it 'returns true when the workspace directory has a Gemfile' do
File.write(File.join(dir_path, 'Gemfile'), "source 'https://rubygems.org'")

expect(workspace.gemfile?).to be(true)
end

it 'returns false when the workspace directory has no Gemfile' do
expect(workspace.gemfile?).to be(false)
end
end
end
Loading