solargraph 0.18.2 → 0.18.3

Sign up to get free protection for your applications and to get access to all the features.
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