solargraph 0.18.2 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/solargraph.rb +33 -28
  3. data/lib/solargraph/api_map.rb +997 -1044
  4. data/lib/solargraph/api_map/source_to_yard.rb +4 -3
  5. data/lib/solargraph/diagnostics/rubocop.rb +4 -3
  6. data/lib/solargraph/language_server/host.rb +140 -70
  7. data/lib/solargraph/language_server/message/base.rb +1 -0
  8. data/lib/solargraph/language_server/message/client.rb +6 -2
  9. data/lib/solargraph/language_server/message/text_document/completion.rb +34 -39
  10. data/lib/solargraph/language_server/message/text_document/definition.rb +1 -1
  11. data/lib/solargraph/language_server/message/text_document/did_close.rb +1 -0
  12. data/lib/solargraph/language_server/message/text_document/did_save.rb +1 -3
  13. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +1 -1
  14. data/lib/solargraph/language_server/message/text_document/hover.rb +25 -30
  15. data/lib/solargraph/language_server/message/text_document/on_type_formatting.rb +1 -1
  16. data/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +8 -7
  17. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +1 -1
  18. data/lib/solargraph/language_server/transport/socket.rb +15 -17
  19. data/lib/solargraph/library.rb +34 -16
  20. data/lib/solargraph/node_methods.rb +96 -96
  21. data/lib/solargraph/pin.rb +1 -0
  22. data/lib/solargraph/pin/base.rb +2 -1
  23. data/lib/solargraph/pin/base_variable.rb +45 -5
  24. data/lib/solargraph/pin/block_parameter.rb +5 -2
  25. data/lib/solargraph/pin/method.rb +22 -0
  26. data/lib/solargraph/pin/namespace.rb +32 -2
  27. data/lib/solargraph/pin/reference.rb +21 -0
  28. data/lib/solargraph/pin/yard_object.rb +9 -0
  29. data/lib/solargraph/shell.rb +136 -136
  30. data/lib/solargraph/source.rb +134 -188
  31. data/lib/solargraph/source/change.rb +70 -0
  32. data/lib/solargraph/source/fragment.rb +120 -66
  33. data/lib/solargraph/source/position.rb +41 -0
  34. data/lib/solargraph/source/updater.rb +48 -0
  35. data/lib/solargraph/version.rb +3 -3
  36. data/lib/solargraph/workspace/config.rb +4 -9
  37. data/lib/solargraph/yard_map/core_docs.rb +0 -1
  38. metadata +5 -2
@@ -18,6 +18,7 @@ module Solargraph
18
18
  code_object_map.clear
19
19
  sources.each do |s|
20
20
  s.namespace_pins.each do |pin|
21
+ next if pin.path.empty?
21
22
  if pin.kind == Solargraph::Suggestion::CLASS
22
23
  code_object_map[pin.path] ||= YARD::CodeObjects::ClassObject.new(root_code_object, pin.path)
23
24
  else
@@ -26,9 +27,9 @@ module Solargraph
26
27
  code_object_map[pin.path].docstring = pin.docstring unless pin.docstring.nil?
27
28
  code_object_map[pin.path].files.push pin.source.filename
28
29
  end
29
- s.namespace_includes.each_pair do |n, i|
30
- i.each do |inc|
31
- code_object_map[n].instance_mixins.push code_object_map[inc] unless code_object_map[inc].nil? or code_object_map[n].nil?
30
+ s.namespace_pins.each do |pin|
31
+ pin.include_references.each do |ref|
32
+ code_object_map[pin.path].instance_mixins.push code_object_map[ref.name] unless code_object_map[ref.name].nil? or code_object_map[pin.path].nil?
32
33
  end
33
34
  end
34
35
  s.attribute_pins.each do |pin|
@@ -2,6 +2,7 @@ require 'open3'
2
2
  require 'shellwords'
3
3
 
4
4
  module Solargraph
5
+
5
6
  module Diagnostics
6
7
  class Rubocop
7
8
  def initialize
@@ -13,10 +14,10 @@ module Solargraph
13
14
  cmd = "rubocop -f j -s #{Shellwords.escape(filename)}"
14
15
  o, e, s = Open3.capture3(cmd, stdin_data: text)
15
16
  make_array text, JSON.parse(o)
17
+ rescue JSON::ParserError
18
+ raise DiagnosticsError, 'RuboCop returned invalid data'
16
19
  rescue Exception => e
17
- STDERR.puts "#{e}"
18
- STDERR.puts "#{e.backtrace}"
19
- nil
20
+ raise DiagnosticsError, 'An internal error occurred while running diagnostics'
20
21
  end
21
22
  end
22
23
 
@@ -1,4 +1,3 @@
1
- # require 'rubocop'
2
1
  require 'thread'
3
2
  require 'set'
4
3
 
@@ -10,18 +9,15 @@ module Solargraph
10
9
  class Host
11
10
  include Solargraph::LanguageServer::UriHelpers
12
11
 
13
- # @return [Solargraph::Library]
14
- attr_reader :library
15
-
16
12
  def initialize
17
13
  @change_semaphore = Mutex.new
14
+ @cancel_semaphore = Mutex.new
18
15
  @buffer_semaphore = Mutex.new
19
16
  @change_queue = []
20
17
  @diagnostics_queue = []
21
18
  @cancel = []
22
19
  @buffer = ''
23
20
  @stopped = false
24
- @library = nil # @todo How to initialize the library
25
21
  start_change_thread
26
22
  start_diagnostics_thread
27
23
  end
@@ -37,15 +33,17 @@ module Solargraph
37
33
  end
38
34
 
39
35
  def cancel id
40
- @cancel.push id
36
+ @cancel_semaphore.synchronize { @cancel.push id }
41
37
  end
42
38
 
43
39
  def cancel? id
44
- @cancel.include? id
40
+ result = false
41
+ @cancel_semaphore.synchronize { result = @cancel.include? id }
42
+ result
45
43
  end
46
44
 
47
45
  def clear id
48
- @cancel.delete id
46
+ @cancel_semaphore.synchronize { @cancel.delete id }
49
47
  end
50
48
 
51
49
  def start request
@@ -53,37 +51,69 @@ module Solargraph
53
51
  begin
54
52
  message.process
55
53
  rescue Exception => e
56
- STDERR.puts e.message
57
- STDERR.puts e.backtrace
58
- message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, e.message
54
+ message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}"
59
55
  end
60
56
  message
61
57
  end
62
58
 
63
59
  def create uri
64
- filename = uri_to_file(uri)
65
- library.create filename, File.read(filename)
60
+ @change_semaphore.synchronize do
61
+ filename = uri_to_file(uri)
62
+ library.create filename, File.read(filename)
63
+ end
66
64
  end
67
65
 
68
66
  def delete uri
69
- filename = uri_to_file(uri)
70
- library.delete filename
67
+ @change_semaphore.synchronize do
68
+ filename = uri_to_file(uri)
69
+ library.delete filename
70
+ end
71
71
  end
72
72
 
73
73
  def open uri, text, version
74
- library.open uri_to_file(uri), text, version
75
- @change_semaphore.synchronize { @diagnostics_queue.push uri }
74
+ @change_semaphore.synchronize do
75
+ library.open uri_to_file(uri), text, version
76
+ @diagnostics_queue.push uri
77
+ end
78
+ end
79
+
80
+ def open? uri
81
+ result = nil
82
+ @change_semaphore.synchronize do
83
+ result = library.open?(uri_to_file(uri))
84
+ end
85
+ result
86
+ end
87
+
88
+ def close uri
89
+ @change_semaphore.synchronize do
90
+ library.close uri_to_file(uri)
91
+ end
92
+ end
93
+
94
+ def save params
95
+ @change_semaphore.synchronize do
96
+ uri = params['textDocument']['uri']
97
+ filename = uri_to_file(uri)
98
+ version = params['textDocument']['version']
99
+ @change_queue.delete_if do |change|
100
+ return true if change['textDocument']['uri'] == uri and change['textDocument']['version'] <= version
101
+ false
102
+ end
103
+ library.overwrite filename, version
104
+ end
76
105
  end
77
106
 
78
107
  def change params
79
108
  @change_semaphore.synchronize do
80
- if changing? params['textDocument']['uri']
109
+ if unsafe_changing? params['textDocument']['uri']
81
110
  @change_queue.push params
82
111
  else
83
112
  source = library.checkout(uri_to_file(params['textDocument']['uri']))
84
113
  @change_queue.push params
85
114
  if params['textDocument']['version'] == source.version + params['contentChanges'].length
86
- source.synchronize(params['contentChanges'], params['textDocument']['version'])
115
+ updater = generate_updater(params)
116
+ library.synchronize updater
87
117
  library.refresh
88
118
  @change_queue.pop
89
119
  @diagnostics_queue.push params['textDocument']['uri']
@@ -128,7 +158,11 @@ module Solargraph
128
158
  end
129
159
 
130
160
  def changing? file_uri
131
- @change_queue.any?{|change| change['textDocument']['uri'] == file_uri}
161
+ result = false
162
+ @change_semaphore.synchronize do
163
+ result = unsafe_changing?(file_uri)
164
+ end
165
+ result
132
166
  end
133
167
 
134
168
  def stop
@@ -139,12 +173,6 @@ module Solargraph
139
173
  @stopped
140
174
  end
141
175
 
142
- def synchronize &block
143
- @change_semaphore.synchronize do
144
- block.call
145
- end
146
- end
147
-
148
176
  def locate_pin params
149
177
  pin = nil
150
178
  @change_semaphore.synchronize do
@@ -159,7 +187,11 @@ module Solargraph
159
187
 
160
188
  def read_text uri
161
189
  filename = uri_to_file(uri)
162
- library.read_text(filename)
190
+ text = nil
191
+ @change_semaphore.synchronize do
192
+ text = library.read_text(filename)
193
+ end
194
+ text
163
195
  end
164
196
 
165
197
  def completions_at filename, line, column
@@ -172,7 +204,7 @@ module Solargraph
172
204
 
173
205
  # @return [Array<Solargraph::Pin::Base>]
174
206
  def definitions_at filename, line, column
175
- results = nil
207
+ results = []
176
208
  @change_semaphore.synchronize do
177
209
  results = library.definitions_at(filename, line, column)
178
210
  end
@@ -187,45 +219,69 @@ module Solargraph
187
219
  results
188
220
  end
189
221
 
222
+ def query_symbols query
223
+ results = nil
224
+ @change_semaphore.synchronize { results = library.query_symbols(query) }
225
+ results
226
+ end
227
+
228
+ def file_symbols uri
229
+ library.file_symbols(uri_to_file(uri))
230
+ end
231
+
190
232
  private
191
233
 
234
+ # @return [Solargraph::Library]
235
+ def library
236
+ @library
237
+ end
238
+
239
+ def unsafe_changing? file_uri
240
+ @change_queue.any?{|change| change['textDocument']['uri'] == file_uri}
241
+ end
242
+
192
243
  def start_change_thread
193
244
  Thread.new do
194
245
  until stopped?
195
246
  @change_semaphore.synchronize do
196
247
  begin
197
248
  changed = false
249
+ @change_queue.sort!{|a, b| a['textDocument']['version'] <=> b['textDocument']['version']}
198
250
  @change_queue.delete_if do |change|
199
251
  filename = uri_to_file(change['textDocument']['uri'])
200
252
  source = library.checkout(filename)
201
253
  if change['textDocument']['version'] == source.version + change['contentChanges'].length
202
- source.synchronize(change['contentChanges'], change['textDocument']['version'])
254
+ updater = generate_updater(change)
255
+ library.synchronize updater
203
256
  @diagnostics_queue.push change['textDocument']['uri']
204
257
  changed = true
205
- true
258
+ next true
206
259
  elsif change['textDocument']['version'] == source.version + 1 #and change['contentChanges'].length == 0
207
260
  # HACK: This condition fixes the fact that formatting
208
261
  # increments the version by one regardless of the number
209
262
  # of changes
210
- source.synchronize(change['contentChanges'], change['textDocument']['version'])
263
+ STDERR.puts "Warning: change applied to #{uri_to_file(change['textDocument']['uri'])} is possibly out of sync"
264
+ updater = generate_updater(change)
265
+ library.synchronize updater
211
266
  @diagnostics_queue.push change['textDocument']['uri']
212
- true
267
+ changed = true
268
+ next true
213
269
  elsif change['textDocument']['version'] <= source.version
214
270
  # @todo Is deleting outdated changes correct behavior?
215
- STDERR.puts "Deleting stale change"
271
+ STDERR.puts "Warning: outdated to change to #{change['textDocument']['uri']} was ignored"
216
272
  @diagnostics_queue.push change['textDocument']['uri']
217
- changed = true
218
- true
273
+ next true
219
274
  else
220
275
  # @todo Change is out of order. Save it for later
221
- STDERR.puts "Kept in queue: #{change['textDocument']['uri']} from #{source.version} to #{change['textDocument']['version']}"
222
- false
276
+ next false
223
277
  end
224
278
  end
225
- STDERR.puts "#{@change_queue.length} pending" unless @change_queue.empty?
226
- library.refresh if changed
279
+ refreshable = changed and @change_queue.empty?
280
+ library.refresh if refreshable
227
281
  rescue Exception => e
228
- STDERR.puts e.message
282
+ # Trying to get anything out of the error except its class
283
+ # hangs the thread for some reason
284
+ STDERR.puts "An error occurred in the change thread: #{e.class}"
229
285
  end
230
286
  end
231
287
  sleep 0.1
@@ -237,43 +293,39 @@ module Solargraph
237
293
  Thread.new do
238
294
  diagnoser = Diagnostics::Rubocop.new
239
295
  until stopped?
296
+ sleep 1
240
297
  if options['diagnostics'] != 'rubocop'
241
298
  @change_semaphore.synchronize { @diagnostics_queue.clear }
242
- sleep 1
243
299
  next
244
300
  end
245
- unless @change_semaphore.locked?
246
- begin
247
- current = nil
248
- @change_semaphore.synchronize do
249
- current = @diagnostics_queue.shift
250
- end
251
- unless current.nil?
252
- already_changing = false
253
- @change_semaphore.synchronize { already_changing = (changing?(current) or @diagnostics_queue.include?(current)) }
254
- unless already_changing
255
- filename = nil
256
- text = nil
257
- @change_semaphore.synchronize do
258
- filename = uri_to_file(current)
259
- text = library.read_text(filename)
260
- end
261
- results = diagnoser.diagnose text, filename
262
- @change_semaphore.synchronize { already_changing = (changing?(current) or @diagnostics_queue.include?(current)) }
263
- # publish_diagnostics current, resp unless already_changing
264
- unless already_changing
265
- send_notification "textDocument/publishDiagnostics", {
266
- uri: current,
267
- diagnostics: results
268
- }
269
- end
270
- end
301
+ begin
302
+ # Diagnosis is broken into two parts to reduce the amount of times it runs while
303
+ # a document is changing
304
+ current = nil
305
+ already_changing = nil
306
+ @change_semaphore.synchronize do
307
+ current = @diagnostics_queue.shift
308
+ break if current.nil?
309
+ already_changing = unsafe_changing?(current)
310
+ @diagnostics_queue.delete current unless already_changing
311
+ end
312
+ next if current.nil? or already_changing
313
+ filename = uri_to_file(current)
314
+ text = library.read_text(filename)
315
+ results = diagnoser.diagnose text, filename
316
+ @change_semaphore.synchronize do
317
+ already_changing = (unsafe_changing?(current) or @diagnostics_queue.include?(current))
318
+ # publish_diagnostics current, resp unless already_changing
319
+ unless already_changing
320
+ send_notification "textDocument/publishDiagnostics", {
321
+ uri: current,
322
+ diagnostics: results
323
+ }
271
324
  end
272
- rescue Exception => e
273
- STDERR.puts e.message
274
325
  end
326
+ rescue Exception => e
327
+ STDERR.puts "Error in diagnostics: #{e.class}"
275
328
  end
276
- sleep 0.1
277
329
  end
278
330
  end
279
331
  end
@@ -282,6 +334,24 @@ module Solargraph
282
334
  return path if File::ALT_SEPARATOR.nil?
283
335
  path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
284
336
  end
337
+
338
+ def generate_updater params
339
+ changes = []
340
+ params['contentChanges'].each do |chng|
341
+ changes.push Solargraph::Source::Change.new(
342
+ (chng['range'].nil? ?
343
+ nil :
344
+ Solargraph::Source::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], chng['range']['end']['line'], chng['range']['end']['character'])
345
+ ),
346
+ chng['text']
347
+ )
348
+ end
349
+ Solargraph::Source::Updater.new(
350
+ uri_to_file(params['textDocument']['uri']),
351
+ params['textDocument']['version'],
352
+ changes
353
+ )
354
+ end
285
355
  end
286
356
  end
287
357
  end
@@ -52,6 +52,7 @@ module Solargraph
52
52
  }
53
53
  response[:result] = result unless result.nil?
54
54
  response[:error] = error unless error.nil?
55
+ response[:result] = nil if result.nil? and error.nil?
55
56
  json = response.to_json
56
57
  envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
57
58
  host.queue envelope
@@ -1,5 +1,9 @@
1
1
  module Solargraph
2
- module Client
3
- autoload :RegisterCapability, 'solargraph/language_server/message/client/register_capability'
2
+ module LanguageServer
3
+ module Message
4
+ module Client
5
+ autoload :RegisterCapability, 'solargraph/language_server/message/client/register_capability'
6
+ end
7
+ end
4
8
  end
5
9
  end
@@ -6,33 +6,20 @@ module Solargraph
6
6
  module TextDocument
7
7
  class Completion < Base
8
8
  def process
9
- begin
10
- start = Time.now
11
- processed = false
12
- until processed
13
- if host.changing?(params['textDocument']['uri'])
14
- if Time.now - start > 1
15
- set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, 'Completion request timed out'
16
- processed = true
17
- end
18
- else
19
- inner_process
9
+ start = Time.now
10
+ processed = false
11
+ until processed
12
+ if host.changing?(params['textDocument']['uri'])
13
+ if Time.now - start > 1
14
+ # set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, 'Completion request timed out'
15
+ set_result empty_result
20
16
  processed = true
21
17
  end
22
- sleep 0.1 unless processed
23
- end
24
- rescue Exception => e
25
- STDERR.puts e.message
26
- STDERR.puts e.backtrace
27
- # Ignore 'Invalid offset' errors, since they usually just mean
28
- # that the document is in the process of changing.
29
- if e.message.include?('Invalid offset')
30
- # @todo Should this result be marked as incomplete? It might
31
- # be possible to resolve it after changes are finished.
32
- set_result empty_result
33
18
  else
34
- set_error ErrorCodes::INTERNAL_ERROR, e.message
19
+ inner_process
20
+ processed = true
35
21
  end
22
+ sleep 0.1 unless processed
36
23
  end
37
24
  end
38
25
 
@@ -42,23 +29,31 @@ module Solargraph
42
29
  filename = uri_to_file(params['textDocument']['uri'])
43
30
  line = params['position']['line']
44
31
  col = params['position']['character']
45
- completion = host.completions_at(filename, line, col)
46
- items = []
47
- idx = 0
48
- completion.pins.each do |pin|
49
- items.push pin.completion_item.merge({
50
- textEdit: {
51
- range: completion.range.to_hash,
52
- newText: pin.name
53
- },
54
- sortText: "#{pin.name}#{idx.to_s.rjust(4, '0')}"
55
- })
56
- idx += 1
32
+ begin
33
+ completion = host.completions_at(filename, line, col)
34
+ if host.cancel?(id)
35
+ return set_result(empty_result) if host.cancel?(id)
36
+ end
37
+ items = []
38
+ idx = 0
39
+ completion.pins.each do |pin|
40
+ items.push pin.completion_item.merge({
41
+ textEdit: {
42
+ range: completion.range.to_hash,
43
+ newText: pin.name
44
+ },
45
+ sortText: "#{pin.name}#{idx.to_s.rjust(4, '0')}"
46
+ })
47
+ idx += 1
48
+ end
49
+ set_result(
50
+ isIncomplete: false,
51
+ items: items
52
+ )
53
+ rescue InvalidOffsetError => e
54
+ STDERR.puts "Skipping invalid offset: #{filename}, line #{line}, character #{col}"
55
+ set_result empty_result
57
56
  end
58
- set_result(
59
- isIncomplete: false,
60
- items: items
61
- )
62
57
  end
63
58
 
64
59
  def empty_result