lib/solargraph/workspace.rb



module Solargraph
  # A workspace consists of the files in a project's directory and the
  # project's configuration. It provides a Source for each file to be used
  # in an associated Library or ApiMap.
  #
  class Workspace
    autoload :Config, 'solargraph/workspace/config'

    # @return [String]
    attr_reader :directory

    MAX_WORKSPACE_SIZE = 5000

    # @param directory [String]
    def initialize directory, config = nil
      # @todo Convert to an absolute path?
      @directory = directory
      @directory = nil if @directory == ''
      @config = config
      load_sources
    end

    # @return [Solargraph::Workspace::Config]
    def config reload = false
      @config = Solargraph::Workspace::Config.new(directory) if @config.nil? or reload
      @config
    end

    # Merge the source. A merge will update the existing source for the file
    # or add it to the sources if the workspace is configured to include it.
    # The source is ignored if the configuration excludes it.
    #
    # @param source [Solargraph::Source]
    # @return [Boolean] True if the source was added to the workspace
    def merge source
      return false unless config(true).calculated.include?(source.filename)
      source_hash[source.filename] = source
      true
    end

    # Determine whether a file would be merged into the workspace.
    #
    # @param filename [String]
    # @return [Boolean]
    def would_merge? filename
      Solargraph::Workspace::Config.new(directory).calculated.include?(filename)
    end

    # Remove a source from the workspace. The source will not be removed if
    # its file exists and the workspace is configured to include it.
    #
    # @param source [Solargraph::Source]
    # @return [Boolean] True if the source was removed from the workspace
    def remove source
      return false if config(true).calculated.include?(source.filename)
      # @todo This method PROBABLY doesn't care if the file is actually here
      source_hash.delete source.filename
      true
    end

    # @return [Array<String>]
    def filenames
      source_hash.keys
    end

    # @return [Array<Solargraph::Source>]
    def sources
      source_hash.values
    end

    # @return [Boolean]
    def has_source? source
      source_hash.has_value?(source)
    end

    # @return [Boolean]
    def has_file? filename
      source_hash.has_key?(filename)
    end

    # Get a source by its filename.
    #
    # @return [Solargraph::Source]
    def source filename
      source_hash[filename]
    end

    def stime
      return @stime if source_hash.empty?
      @stime = source_hash.values.sort{|a, b| a.stime <=> b.stime}.last.stime
    end

    def require_paths
      @require_paths ||= generate_require_paths
    end

    def would_require? path
      require_paths.each do |rp|
        return true if File.exist?(File.join(rp, "#{path}.rb"))
      end
      false
    end

    def gemspec?
      return true unless gemspecs.empty?
    end

    def gemspecs
      return [] if directory.nil?
      @gemspecs ||= Dir[File.join(directory, '**/*.gemspec')]
    end

    private

    # @return [Hash<String, Solargraph::Source>]
    def source_hash
      @source_hash ||= {}
    end

    def load_sources
      source_hash.clear
      unless directory.nil?
        size = config.calculated.length
        raise WorkspaceTooLargeError.new(size) if config.max_files > 0 and size > config.max_files

        config.calculated.each do |filename|
          source_hash[filename] = Solargraph::Source.load(filename)
        end
      end
      @stime = Time.now
    end

    def generate_require_paths
      return [] if directory.nil?
      return configured_require_paths unless gemspec?
      result = []
      gemspecs.each do |file|
        spec = Gem::Specification.load(file)
        base = File.dirname(file)
        result.concat spec.require_paths.map{ |path| File.join(base, path) } unless spec.nil?
      end
      result.concat config.require_paths
      result.push File.join(directory, 'lib') if result.empty?
      result
    end

    def configured_require_paths
      return [File.join(directory, 'lib')] if config.require_paths.empty?
      config.require_paths.map{|p| File.join(directory, p)}
    end
  end
end