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



87
88
89
# File 'lib/ruby_language_server/project_manager.rb', line 87

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

#completion_at(uri, position) ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'lib/ruby_language_server/project_manager.rb', line 98

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



235
236
237
238
# File 'lib/ruby_language_server/project_manager.rb', line 235

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

#diagnostics_ready?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/ruby_language_server/project_manager.rb', line 56

def diagnostics_ready?
  @additional_gem_mutex.synchronize { @additional_gems_installed }
end

#install_additional_gems(gem_names) ⇒ Object



60
61
62
63
64
65
66
67
# File 'lib/ruby_language_server/project_manager.rb', line 60

def install_additional_gems(gem_names)
  Thread.new do
    RubyLanguageServer::GemInstaller.install_gems(gem_names)
    @additional_gem_mutex.synchronize { @additional_gems_installed = true }
  rescue StandardError => e
    RubyLanguageServer.logger.error("Issue installing rubocop gems: #{e} #{e.backtrace}")
  end
end

#possible_definitions(uri, position) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
# File 'lib/ruby_language_server/project_manager.rb', line 244

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



271
272
273
274
275
276
277
# File 'lib/ruby_language_server/project_manager.rb', line 271

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



81
82
83
84
85
# File 'lib/ruby_language_server/project_manager.rb', line 81

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



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/ruby_language_server/project_manager.rb', line 182

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…



258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/ruby_language_server/project_manager.rb', line 258

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]



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

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



74
75
76
77
78
79
# File 'lib/ruby_language_server/project_manager.rb', line 74

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



69
70
71
72
# File 'lib/ruby_language_server/project_manager.rb', line 69

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)



215
216
217
218
219
220
221
222
223
# File 'lib/ruby_language_server/project_manager.rb', line 215

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

#updated_diagnostics_for_codefile(code_file) ⇒ Object



225
226
227
228
229
230
231
232
# File 'lib/ruby_language_server/project_manager.rb', line 225

def updated_diagnostics_for_codefile(code_file)
  # Maybe we should be sharing this GoodCop across instances
  RubyLanguageServer.logger.debug("updated_diagnostics_for_codefile: #{code_file.uri}")
  project_relative_filename = filename_relative_to_project(code_file.uri)
  code_file.diagnostics = GoodCop.instance&.diagnostics(code_file.text, project_relative_filename)
  RubyLanguageServer.logger.debug("code_file.diagnostics: #{code_file.diagnostics}")
  code_file.diagnostics
end

#word_at_location(uri, position) ⇒ Object



240
241
242
# File 'lib/ruby_language_server/project_manager.rb', line 240

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