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['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
107
108
109
# File 'lib/ruby_language_server/project_manager.rb', line 98

def completion_at(uri, position)
  relative_position = position.dup
  relative_position.character = relative_position.character # To get before the . or ::
  # RubyLanguageServer.logger.debug("relative_position #{relative_position}")
  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
  context = context_at_location(uri, relative_position)
  return {} if context.nil? || context == ''

  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

#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}")
  end
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



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

def project_definitions_for(name)
  scopes = RubyLanguageServer::ScopeData::Scope.where(name: name)
  variables = RubyLanguageServer::ScopeData::Variable.constant_variables.where(name: name)
  (scopes + variables).reject { |scope| scope.code_file.nil? }.map do |scope|
    Location.hash(scope.code_file.uri.delete_prefix(self.class.root_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_files(mutex) ⇒ Object

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



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/ruby_language_server/project_manager.rb', line 185

def scan_all_project_files(mutex)
  project_ruby_files = Dir.glob("#{self.class.root_path}**/*.rb")
  Thread.new do
    RubyLanguageServer.logger.error('Threading up!')
    project_ruby_files.each do |container_path|
      # Let's not preload spec/test or vendor - yet..
      next if container_path.match?(/^(.?spec|test|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 "Locking scan for #{container_path}"
      mutex.synchronize do
        RubyLanguageServer.logger.debug("Threading #{host_uri}")
        update_document_content(host_uri, text)
        code_file_for_uri(host_uri).refresh_scopes_if_needed
      end
      RubyLanguageServer.logger.debug "Unlocking scan for #{container_path}"
    end
  end
end

#scope_definitions_for(name, scope, uri) ⇒ Object



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

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.delete_prefix(self.class.root_uri), variable.line, 1) if variable.name == name
    end
    check_scope = check_scope.parent
  end
  RubyLanguageServer.logger.debug("scope_definitions_for(#{name}, #{scope}, #{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)



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

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



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

def updated_diagnostics_for_codefile(code_file)
  # Maybe we should be sharing this GoodCop across instances
  @good_cop ||= GoodCop.new
  project_relative_filename = filename_relative_to_project(code_file.uri)
  code_file.diagnostics = @good_cop.diagnostics(code_file.text, project_relative_filename)
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