Class: RubyLanguageServer::ProjectManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_language_server/project_manager.rb

Constant Summary collapse

ROOT_PATH_MUTEX =

GoodCop wants to know where to find its config. So here we are.

Mutex.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, uri = nil) ⇒ ProjectManager

Returns a new instance of ProjectManager.



45
46
47
48
49
50
51
52
53
54
# File 'lib/ruby_language_server/project_manager.rb', line 45

def initialize(path, uri = nil)
  # Should probably lock for read, but I'm feeling crazy!
  self.class.root_path = path if self.class.root_path.nil?
  self.class.root_uri = uri if uri

  @root_uri = "file://#{path}"
  # This is {uri: code_file} where content stuff is like
  # @additional_gems_installed = false
  # @additional_gem_mutex = Mutex.new
end

Instance Attribute Details

#uri_code_file_hashObject (readonly)

Returns the value of attribute uri_code_file_hash.



9
10
11
# File 'lib/ruby_language_server/project_manager.rb', line 9

def uri_code_file_hash
  @uri_code_file_hash
end

Class Method Details

.root_pathObject



22
23
24
25
26
27
28
29
# File 'lib/ruby_language_server/project_manager.rb', line 22

def root_path
  # I'm torn about this.  Should this be set in the Server?  Or is this right.
  # Rather than worry too much, I'll just do this here and change it later if it feels wrong.
  path = ENV.fetch('RUBY_LANGUAGE_SERVER_PROJECT_ROOT') { @_root_path }
  return path if path.nil?

  path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}"
end

.root_path=(path) ⇒ Object



16
17
18
19
20
# File 'lib/ruby_language_server/project_manager.rb', line 16

def root_path=(path)
  ROOT_PATH_MUTEX.synchronize do
    @_root_path = path
  end
end

.root_uriObject



40
41
42
# File 'lib/ruby_language_server/project_manager.rb', line 40

def root_uri
  @_root_uri || "file://#{root_path}"
end

.root_uri=(uri) ⇒ Object



31
32
33
34
35
36
37
38
# File 'lib/ruby_language_server/project_manager.rb', line 31

def root_uri=(uri)
  ROOT_PATH_MUTEX.synchronize do
    if uri
      uri = "#{uri}/" unless uri.end_with?('/')
      @_root_uri = uri
    end
  end
end

Instance Method Details

#all_scopesObject



78
79
80
# File 'lib/ruby_language_server/project_manager.rb', line 78

def all_scopes
  RubyLanguageServer::ScopeData::Scope.all
end

#completion_at(uri, position) ⇒ Object



89
90
91
92
93
94
95
96
97
# File 'lib/ruby_language_server/project_manager.rb', line 89

def completion_at(uri, position)
  context = context_at_location(uri, position)
  return {} if context.blank?

  RubyLanguageServer.logger.debug("scopes_at(uri, position) #{scopes_at(uri, position).map(&:name)}")
  position_scopes = scopes_at(uri, position) || RubyLanguageServer::ScopeData::Scope.where(id: root_scope_for(uri).id)
  context_scope = position_scopes.first
  RubyLanguageServer::Completion.completion(context, context_scope, position_scopes)
end

#context_at_location(uri, position) ⇒ Object

Returns the context of what is being typed in the given line



226
227
228
229
# File 'lib/ruby_language_server/project_manager.rb', line 226

def context_at_location(uri, position)
  code_file = code_file_for_uri(uri)
  code_file&.context_at_location(position)
end

#possible_definitions(uri, position) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/ruby_language_server/project_manager.rb', line 235

def possible_definitions(uri, position)
  name = word_at_location(uri, position)
  return {} if name.blank?

  name = 'initialize' if name == 'new'
  scope = scopes_at(uri, position).first
  results = scope_definitions_for(name, scope, uri)
  return results unless results.empty?

  project_definitions_for(name)
end

#project_definitions_for(name) ⇒ Object



262
263
264
265
266
267
268
# File 'lib/ruby_language_server/project_manager.rb', line 262

def project_definitions_for(name)
  scopes = RubyLanguageServer::ScopeData::Scope.where(name:)
  variables = RubyLanguageServer::ScopeData::Variable.constant_variables.where(name:)
  (scopes + variables).reject { |scope| scope.code_file.nil? }.map do |scope|
    Location.hash(scope.code_file.uri, scope.top_line, 1)
  end
end

#root_scope_for(uri) ⇒ Object



72
73
74
75
76
# File 'lib/ruby_language_server/project_manager.rb', line 72

def root_scope_for(uri)
  code_file = code_file_for_uri(uri)
  RubyLanguageServer.logger.error('code_file.nil?!!!!!!!!!!!!!!') if code_file.nil?
  code_file&.root_scope
end

#scan_all_project_filesObject

interface CompletionItem

/**
 * The label of this completion item. By default
 * also the text that is inserted when selecting
 * this completion.
 */
label: string;
/**
 * The kind of this completion item. Based of the kind
 * an icon is chosen by the editor.
 */
kind?: number;
/**
 * A human-readable string with additional information
 * about this item, like type or symbol information.
 */
detail?: string;
/**
 * A human-readable string that represents a doc-comment.
 */
documentation?: string;
/**
 * A string that shoud be used when comparing this item
 * with other items. When `falsy` the label is used.
 */
sortText?: string;
/**
 * A string that should be used when filtering a set of
 * completion items. When `falsy` the label is used.
 */
filterText?: string;
/**
 * A string that should be inserted a document when selecting
 * this completion. When `falsy` the label is used.
 */
insertText?: string;
/**
 * The format of the insert text. The format applies to both the `insertText` property
 * and the `newText` property of a provided `textEdit`.
 */
insertTextFormat?: InsertTextFormat;
/**
 * An edit which is applied to a document when selecting this completion. When an edit is provided the value of
 * `insertText` is ignored.
 *
 * *Note:* The range of the edit must be a single line range and it must contain the position at which completion
 * has been requested.
 */
textEdit?: TextEdit;
/**
 * An optional array of additional text edits that are applied when
 * selecting this completion. Edits must not overlap with the main edit
 * nor with themselves.
 */
additionalTextEdits?: TextEdit[];
/**
 * An optional set of characters that when pressed while this completion is active will accept it first and
 * then type that character. *Note* that all commit characters should have `length=1` and that superfluous
 * characters will be ignored.
 */
commitCharacters?: string[];
/**
 * An optional command that is executed *after* inserting this completion. *Note* that
 * additional modifications to the current document should be described with the
 * additionalTextEdits-property.
 */
command?: Command;
/**
 * An data entry field that is preserved on a completion item between
 * a completion and a completion resolve request.
 */
data?: any



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/ruby_language_server/project_manager.rb', line 173

def scan_all_project_files
  project_ruby_files = Dir.glob("#{self.class.root_path}**/*.rb")
  RubyLanguageServer.logger.debug('Threading up!')
  root_uri = @root_uri
  root_uri += '/' unless root_uri.end_with? '/'
  # Using fork because this is run in a docker container that has fork.
  # If you want to run this on some platform without fork, fork the code and PR it :-)
  fork_id = fork do
    project_ruby_files.each do |container_path|
      # Let's not preload spec/test files or vendor - yet..
      next if container_path.match?(/(spec\.rb|test\.rb|vendor)/)

      text = File.read(container_path)
      relative_path = container_path.delete_prefix(self.class.root_path)
      host_uri = root_uri + relative_path
      RubyLanguageServer.logger.debug("Threading #{host_uri}")
      begin
        ActiveRecord::Base.connection_pool.with_connection do |_connection|
          update_document_content(host_uri, text)
          code_file_for_uri(host_uri).refresh_scopes_if_needed(shallow: true)
        end
      rescue StandardError => e
        RubyLanguageServer.logger.warn("Error updating: #{e}\n#{e.backtrace * "\n"}")
        sleep 5
        retry
      end
    end
  end
  RubyLanguageServer.logger.debug("Forked process id to look at other files: #{fork_id}")
  Process.detach(fork_id)
end

#scope_definitions_for(name, scope, uri) ⇒ Object

Return variables found in the current scope. After all, those are the important ones. Should probably be private…



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ruby_language_server/project_manager.rb', line 249

def scope_definitions_for(name, scope, uri)
  check_scope = scope
  return_array = []
  while check_scope
    scope.variables.each do |variable|
      return_array << Location.hash(uri, variable.line, 1) if variable.name == name
    end
    check_scope = check_scope.parent
  end
  RubyLanguageServer.logger.debug("==============>> scope_definitions_for(#{name}, #{scope.to_json}, #{uri}: #{return_array.uniq})")
  return_array.uniq
end

#scopes_at(uri, position) ⇒ Object

Return the list of scopes [deepest, parent, …, Object]



83
84
85
86
87
# File 'lib/ruby_language_server/project_manager.rb', line 83

def scopes_at(uri, position)
  code_file = code_file_for_uri(uri)
  code_file.refresh_scopes_if_needed
  code_file.scopes.for_line(position.line).where.not(path: nil).by_path_length
end

#tags_for_uri(uri) ⇒ Object



65
66
67
68
69
70
# File 'lib/ruby_language_server/project_manager.rb', line 65

def tags_for_uri(uri)
  code_file = code_file_for_uri(uri)
  return {} if code_file.nil?

  code_file.tags
end

#text_for_uri(uri) ⇒ Object

def diagnostics_ready?

@additional_gem_mutex.synchronize { @additional_gems_installed }

end



60
61
62
63
# File 'lib/ruby_language_server/project_manager.rb', line 60

def text_for_uri(uri)
  code_file = code_file_for_uri(uri)
  code_file&.text || ''
end

#update_document_content(uri, text) ⇒ Object

returns diagnostic info (if possible)



206
207
208
209
210
211
212
213
214
# File 'lib/ruby_language_server/project_manager.rb', line 206

def update_document_content(uri, text)
  RubyLanguageServer.logger.debug("update_document_content: #{uri}")
  # RubyLanguageServer.logger.error("@root_path: #{@root_path}")
  code_file = code_file_for_uri(uri)
  return code_file.diagnostics if code_file.text == text

  code_file.update_text(text)
  # diagnostics_ready? ? updated_diagnostics_for_codefile(code_file) : []
end

#word_at_location(uri, position) ⇒ Object



231
232
233
# File 'lib/ruby_language_server/project_manager.rb', line 231

def word_at_location(uri, position)
  context_at_location(uri, position)&.last
end