solargraph 0.40.1 → 0.41.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +15 -0
  4. data/SPONSORS.md +1 -0
  5. data/lib/.rubocop.yml +1 -1
  6. data/lib/solargraph.rb +8 -7
  7. data/lib/solargraph/api_map.rb +52 -75
  8. data/lib/solargraph/api_map/store.rb +5 -0
  9. data/lib/solargraph/bench.rb +19 -18
  10. data/lib/solargraph/compat.rb +15 -1
  11. data/lib/solargraph/diagnostics/rubocop.rb +10 -2
  12. data/lib/solargraph/diagnostics/rubocop_helpers.rb +19 -20
  13. data/lib/solargraph/language_server/host.rb +74 -1
  14. data/lib/solargraph/language_server/message/completion_item/resolve.rb +1 -0
  15. data/lib/solargraph/language_server/message/extended/environment.rb +3 -3
  16. data/lib/solargraph/language_server/message/initialize.rb +30 -35
  17. data/lib/solargraph/language_server/message/text_document/formatting.rb +68 -18
  18. data/lib/solargraph/language_server/message/text_document/hover.rb +1 -1
  19. data/lib/solargraph/library.rb +94 -21
  20. data/lib/solargraph/parser/legacy/node_methods.rb +4 -0
  21. data/lib/solargraph/parser/rubyvm/node_chainer.rb +0 -1
  22. data/lib/solargraph/parser/rubyvm/node_methods.rb +9 -2
  23. data/lib/solargraph/parser/rubyvm/node_processors/args_node.rb +11 -12
  24. data/lib/solargraph/parser/rubyvm/node_processors/opt_arg_node.rb +1 -6
  25. data/lib/solargraph/parser/rubyvm/node_processors/send_node.rb +1 -1
  26. data/lib/solargraph/source.rb +1 -1
  27. data/lib/solargraph/source/chain/head.rb +0 -16
  28. data/lib/solargraph/source/source_chainer.rb +1 -0
  29. data/lib/solargraph/source_map/mapper.rb +0 -5
  30. data/lib/solargraph/type_checker.rb +49 -39
  31. data/lib/solargraph/type_checker/checks.rb +9 -5
  32. data/lib/solargraph/type_checker/rules.rb +5 -1
  33. data/lib/solargraph/version.rb +1 -1
  34. data/lib/solargraph/workspace/config.rb +19 -3
  35. data/lib/solargraph/yard_map/core_fills.rb +1 -0
  36. data/solargraph.gemspec +1 -1
  37. metadata +4 -4
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'observer'
4
4
  require 'set'
5
+ require 'securerandom'
5
6
 
6
7
  module Solargraph
7
8
  module LanguageServer
@@ -192,7 +193,7 @@ module Solargraph
192
193
  def diagnose uri
193
194
  if sources.include?(uri)
194
195
  library = library_for(uri)
195
- if library.synchronized?
196
+ if library.mapped? && library.synchronized?
196
197
  logger.info "Diagnosing #{uri}"
197
198
  begin
198
199
  results = library.diagnose uri_to_file(uri)
@@ -277,6 +278,7 @@ module Solargraph
277
278
  begin
278
279
  lib = Solargraph::Library.load(path, name)
279
280
  libraries.push lib
281
+ async_library_map lib
280
282
  rescue WorkspaceTooLargeError => e
281
283
  send_notification 'window/showMessage', {
282
284
  'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
@@ -494,6 +496,11 @@ module Solargraph
494
496
  library.read_text(filename)
495
497
  end
496
498
 
499
+ def formatter_config uri
500
+ library = library_for(uri)
501
+ library.workspace.config.formatter
502
+ end
503
+
497
504
  # @param uri [String]
498
505
  # @param line [Integer]
499
506
  # @param column [Integer]
@@ -626,6 +633,7 @@ module Solargraph
626
633
 
627
634
  # @return [void]
628
635
  def catalog
636
+ return unless libraries.all?(&:mapped?)
629
637
  libraries.each(&:catalog)
630
638
  end
631
639
 
@@ -736,6 +744,71 @@ module Solargraph
736
744
  def prepare_rename?
737
745
  client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
738
746
  end
747
+
748
+ def client_supports_progress?
749
+ client_capabilities['window'] && client_capabilities['window']['workDoneProgress']
750
+ end
751
+
752
+ # @param library [Library]
753
+ # @return [void]
754
+ def async_library_map library
755
+ return if library.mapped?
756
+ Thread.new do
757
+ if client_supports_progress?
758
+ uuid = SecureRandom.uuid
759
+ send_request 'window/workDoneProgress/create', {
760
+ token: uuid
761
+ } do |response|
762
+ do_async_library_map library, response.nil? ? uuid : nil
763
+ end
764
+ else
765
+ do_async_library_map library
766
+ end
767
+ end
768
+ end
769
+
770
+ def do_async_library_map library, uuid = nil
771
+ total = library.workspace.sources.length
772
+ if uuid
773
+ send_notification '$/progress', {
774
+ token: uuid,
775
+ value: {
776
+ kind: 'begin',
777
+ title: "Mapping workspace",
778
+ message: "0/#{total} files",
779
+ cancellable: false,
780
+ percentage: 0
781
+ }
782
+ }
783
+ end
784
+ pct = 0
785
+ mod = 10
786
+ while library.next_map
787
+ next unless uuid
788
+ cur = ((library.source_map_hash.keys.length.to_f / total.to_f) * 100).to_i
789
+ if cur > pct && cur % mod == 0
790
+ pct = cur
791
+ send_notification '$/progress', {
792
+ token: uuid,
793
+ value: {
794
+ kind: 'report',
795
+ cancellable: false,
796
+ message: "#{library.source_map_hash.keys.length}/#{total} files",
797
+ percentage: pct
798
+ }
799
+ }
800
+ end
801
+ end
802
+ if uuid
803
+ send_notification '$/progress', {
804
+ token: uuid,
805
+ value: {
806
+ kind: 'end',
807
+ message: 'Mapping complete'
808
+ }
809
+ }
810
+ end
811
+ end
739
812
  end
740
813
  end
741
814
  end
@@ -21,6 +21,7 @@ module Solargraph
21
21
  docs = pins
22
22
  .reject { |pin| pin.documentation.empty? && pin.return_type.undefined? }
23
23
  result = params
24
+ .transform_keys(&:to_sym)
24
25
  .merge(pins.first.resolve_completion_item)
25
26
  .merge(documentation: markup_content(join_docs(docs)))
26
27
  result[:detail] = pins.first.detail
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Make sure the environment page can report RuboCop's version
4
- require 'rubocop'
5
-
6
3
  module Solargraph
7
4
  module LanguageServer
8
5
  module Message
@@ -12,6 +9,9 @@ module Solargraph
12
9
  #
13
10
  class Environment < Base
14
11
  def process
12
+ # Make sure the environment page can report RuboCop's version
13
+ require 'rubocop'
14
+
15
15
  page = Solargraph::Page.new(host.options['viewsPath'])
16
16
  content = page.render('environment', layout: true, locals: { config: host.options, folders: host.folders })
17
17
  set_result(
@@ -1,49 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'benchmark'
4
-
5
3
  module Solargraph
6
4
  module LanguageServer
7
5
  module Message
8
6
  class Initialize < Base
9
7
  def process
10
- bm = Benchmark.measure {
11
- host.configure params['initializationOptions']
12
- host.client_capabilities = params['capabilities']
13
- if support_workspace_folders?
14
- host.prepare_folders params['workspaceFolders']
15
- elsif params['rootUri']
16
- host.prepare UriHelpers.uri_to_file(params['rootUri'])
17
- else
18
- host.prepare params['rootPath']
19
- end
20
- result = {
21
- capabilities: {
22
- textDocumentSync: 2, # @todo What should this be?
23
- workspace: {
24
- workspaceFolders: {
25
- supported: true,
26
- changeNotifications: true
27
- }
8
+ host.configure params['initializationOptions']
9
+ host.client_capabilities = params['capabilities']
10
+ if support_workspace_folders?
11
+ host.prepare_folders params['workspaceFolders']
12
+ elsif params['rootUri']
13
+ host.prepare UriHelpers.uri_to_file(params['rootUri'])
14
+ else
15
+ host.prepare params['rootPath']
16
+ end
17
+ result = {
18
+ capabilities: {
19
+ textDocumentSync: 2, # @todo What should this be?
20
+ workspace: {
21
+ workspaceFolders: {
22
+ supported: true,
23
+ changeNotifications: true
28
24
  }
29
25
  }
30
26
  }
31
- result[:capabilities].merge! static_completion unless dynamic_registration_for?('textDocument', 'completion')
32
- result[:capabilities].merge! static_signature_help unless dynamic_registration_for?('textDocument', 'signatureHelp')
33
- # result[:capabilities].merge! static_on_type_formatting unless dynamic_registration_for?('textDocument', 'onTypeFormatting')
34
- result[:capabilities].merge! static_hover unless dynamic_registration_for?('textDocument', 'hover')
35
- result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', 'formatting')
36
- result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', 'documentSymbol')
37
- result[:capabilities].merge! static_definitions unless dynamic_registration_for?('textDocument', 'definition')
38
- result[:capabilities].merge! static_rename unless dynamic_registration_for?('textDocument', 'rename')
39
- result[:capabilities].merge! static_references unless dynamic_registration_for?('textDocument', 'references')
40
- result[:capabilities].merge! static_workspace_symbols unless dynamic_registration_for?('workspace', 'symbol')
41
- result[:capabilities].merge! static_folding_range unless dynamic_registration_for?('textDocument', 'foldingRange')
42
- # @todo Temporarily disabled
43
- # result[:capabilities].merge! static_code_action unless dynamic_registration_for?('textDocument', 'codeAction')
44
- set_result result
45
27
  }
46
- Solargraph.logger.unknown "Solargraph initialized (#{bm.real} seconds)"
28
+ result[:capabilities].merge! static_completion unless dynamic_registration_for?('textDocument', 'completion')
29
+ result[:capabilities].merge! static_signature_help unless dynamic_registration_for?('textDocument', 'signatureHelp')
30
+ # result[:capabilities].merge! static_on_type_formatting unless dynamic_registration_for?('textDocument', 'onTypeFormatting')
31
+ result[:capabilities].merge! static_hover unless dynamic_registration_for?('textDocument', 'hover')
32
+ result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', 'formatting')
33
+ result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', 'documentSymbol')
34
+ result[:capabilities].merge! static_definitions unless dynamic_registration_for?('textDocument', 'definition')
35
+ result[:capabilities].merge! static_rename unless dynamic_registration_for?('textDocument', 'rename')
36
+ result[:capabilities].merge! static_references unless dynamic_registration_for?('textDocument', 'references')
37
+ result[:capabilities].merge! static_workspace_symbols unless dynamic_registration_for?('workspace', 'symbol')
38
+ result[:capabilities].merge! static_folding_range unless dynamic_registration_for?('textDocument', 'foldingRange')
39
+ # @todo Temporarily disabled
40
+ # result[:capabilities].merge! static_code_action unless dynamic_registration_for?('textDocument', 'codeAction')
41
+ set_result result
47
42
  end
48
43
 
49
44
  private
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
3
  require 'securerandom'
5
4
  require 'tmpdir'
6
5
 
@@ -12,28 +11,79 @@ module Solargraph
12
11
  include Solargraph::Diagnostics::RubocopHelpers
13
12
 
14
13
  def process
15
- filename = uri_to_file(params['textDocument']['uri'])
16
- Dir.mktmpdir do |tempdir|
17
- tempfile = File.join(tempdir, File.basename(filename))
18
- rubocop_file = Diagnostics::RubocopHelpers.find_rubocop_file(filename)
19
- original = host.read_text(params['textDocument']['uri'])
20
- File.write tempfile, original
21
- begin
22
- args = ['-a', '-f', 'fi', tempfile]
23
- args.unshift('-c', fix_drive_letter(rubocop_file)) unless rubocop_file.nil?
24
- options, paths = RuboCop::Options.new.parse(args)
25
- store = RuboCop::ConfigStore.new
26
- redirect_stdout { RuboCop::Runner.new(options, store).run(paths) }
27
- result = File.read(tempfile)
28
- format original, result
29
- rescue RuboCop::ValidationError, RuboCop::ConfigNotFoundError => e
30
- set_error(Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}")
31
- end
14
+ file_uri = params['textDocument']['uri']
15
+ config = config_for(file_uri)
16
+ original = host.read_text(file_uri)
17
+ args = cli_args(file_uri, config)
18
+
19
+ require_rubocop(config['version'])
20
+ options, paths = RuboCop::Options.new.parse(args)
21
+ options[:stdin] = original
22
+ corrections = redirect_stdout do
23
+ RuboCop::Runner.new(options, RuboCop::ConfigStore.new).run(paths)
32
24
  end
25
+ result = options[:stdin]
26
+
27
+ log_corrections(corrections)
28
+
29
+ format original, result
30
+ rescue RuboCop::ValidationError, RuboCop::ConfigNotFoundError => e
31
+ set_error(Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}")
33
32
  end
34
33
 
35
34
  private
36
35
 
36
+ def log_corrections(corrections)
37
+ corrections = corrections&.strip
38
+ return if corrections&.empty?
39
+
40
+ Solargraph.logger.info('Formatting result:')
41
+ corrections.each_line do |line|
42
+ next if line.strip.empty?
43
+ Solargraph.logger.info(line.strip)
44
+ end
45
+ end
46
+
47
+ def config_for(file_uri)
48
+ conf = host.formatter_config(file_uri)
49
+ return {} unless conf.is_a?(Hash)
50
+
51
+ conf['rubocop'] || {}
52
+ end
53
+
54
+ def cli_args file_uri, config
55
+ file = UriHelpers.uri_to_file(file_uri)
56
+ args = [
57
+ config['cops'] == 'all' ? '--auto-correct-all' : '--auto-correct',
58
+ '--cache', 'false',
59
+ '--format', formatter_class(config).name,
60
+ ]
61
+
62
+ ['except', 'only'].each do |arg|
63
+ cops = cop_list(config[arg])
64
+ args += ["--#{arg}", cops] if cops
65
+ end
66
+
67
+ args += config['extra_args'] if config['extra_args']
68
+ args + [file]
69
+ end
70
+
71
+ def formatter_class(config)
72
+ if self.class.const_defined?('BlankRubocopFormatter')
73
+ BlankRubocopFormatter
74
+ else
75
+ require_rubocop(config['version'])
76
+ klass = Class.new(::RuboCop::Formatter::BaseFormatter)
77
+ self.class.const_set 'BlankRubocopFormatter', klass
78
+ end
79
+ end
80
+
81
+ def cop_list(value)
82
+ value = value.join(',') if value.respond_to?(:join)
83
+ return nil if value == '' || !value.is_a?(String)
84
+ value
85
+ end
86
+
37
87
  # @param original [String]
38
88
  # @param result [String]
39
89
  # @return [void]
@@ -17,7 +17,7 @@ module Solargraph
17
17
  if !this_link.nil? && this_link != last_link
18
18
  parts.push this_link
19
19
  end
20
- parts.push pin.detail.gsub(':', '\\:') unless pin.is_a?(Pin::Namespace) || pin.detail.nil?
20
+ parts.push "`" + pin.detail.gsub(':', '\\:') + "`" unless pin.is_a?(Pin::Namespace) || pin.detail.nil?
21
21
  parts.push pin.documentation unless pin.documentation.nil? || pin.documentation.empty?
22
22
  unless parts.empty?
23
23
  data = parts.join("\n\n")
@@ -20,9 +20,7 @@ module Solargraph
20
20
  def initialize workspace = Solargraph::Workspace.new, name = nil
21
21
  @workspace = workspace
22
22
  @name = name
23
- api_map.catalog bench
24
- @synchronized = true
25
- @catalog_mutex = Mutex.new
23
+ @synchronized = false
26
24
  end
27
25
 
28
26
  def inspect
@@ -48,9 +46,13 @@ module Solargraph
48
46
  # @return [void]
49
47
  def attach source
50
48
  mutex.synchronize do
51
- @synchronized = (@current == source) if synchronized?
49
+ if @current && @current.filename != source.filename && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename)
50
+ source_map_hash.delete @current.filename
51
+ @synchronized = false
52
+ end
52
53
  @current = source
53
- catalog
54
+ maybe_map @current
55
+ api_map.catalog bench unless synchronized?
54
56
  end
55
57
  end
56
58
 
@@ -110,9 +112,9 @@ module Solargraph
110
112
  mutex.synchronize do
111
113
  next if File.directory?(filename) || !File.exist?(filename)
112
114
  next unless contain?(filename) || open?(filename) || workspace.would_merge?(filename)
113
- @synchronized = false
114
115
  source = Solargraph::Source.load_string(File.read(filename), filename)
115
116
  workspace.merge(source)
117
+ maybe_map source
116
118
  result = true
117
119
  end
118
120
  result
@@ -158,6 +160,8 @@ module Solargraph
158
160
  position = Position.new(line, column)
159
161
  cursor = Source::Cursor.new(read(filename), position)
160
162
  api_map.clip(cursor).complete
163
+ rescue FileNotFoundError => e
164
+ handle_file_not_found filename, e
161
165
  end
162
166
 
163
167
  # Get definition suggestions for the expression at the specified file and
@@ -186,6 +190,8 @@ module Solargraph
186
190
  else
187
191
  api_map.clip(cursor).define.map { |pin| pin.realize(api_map) }
188
192
  end
193
+ rescue FileNotFoundError => e
194
+ handle_file_not_found(filename, e)
189
195
  end
190
196
 
191
197
  # Get signature suggestions for the method at the specified file and
@@ -260,14 +266,12 @@ module Solargraph
260
266
  # @param query [String]
261
267
  # @return [Array<YARD::CodeObjects::Base>]
262
268
  def document query
263
- catalog
264
269
  api_map.document query
265
270
  end
266
271
 
267
272
  # @param query [String]
268
273
  # @return [Array<String>]
269
274
  def search query
270
- catalog
271
275
  api_map.search query
272
276
  end
273
277
 
@@ -276,7 +280,6 @@ module Solargraph
276
280
  # @param query [String]
277
281
  # @return [Array<Pin::Base>]
278
282
  def query_symbols query
279
- catalog
280
283
  api_map.query_symbols query
281
284
  end
282
285
 
@@ -295,10 +298,13 @@ module Solargraph
295
298
  # @param path [String]
296
299
  # @return [Array<Solargraph::Pin::Base>]
297
300
  def path_pins path
298
- catalog
299
301
  api_map.get_path_suggestions(path)
300
302
  end
301
303
 
304
+ def source_maps
305
+ source_map_hash.values
306
+ end
307
+
302
308
  # Get the current text of a file in the library.
303
309
  #
304
310
  # @param filename [String]
@@ -318,7 +324,6 @@ module Solargraph
318
324
  # be an option to do so.
319
325
  #
320
326
  return [] unless open?(filename)
321
- catalog
322
327
  result = []
323
328
  source = read(filename)
324
329
  repargs = {}
@@ -346,7 +351,7 @@ module Solargraph
346
351
  #
347
352
  # @return [void]
348
353
  def catalog
349
- @catalog_mutex.synchronize do
354
+ mutex.synchronize do
350
355
  break if synchronized?
351
356
  logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
352
357
  api_map.catalog bench
@@ -355,6 +360,17 @@ module Solargraph
355
360
  end
356
361
  end
357
362
 
363
+ def bench
364
+ source_maps = @current ? [@current] : []
365
+ source_maps.concat source_map_hash.values
366
+ Bench.new(
367
+ source_maps: source_maps,
368
+ load_paths: workspace.require_paths,
369
+ gemnames: workspace.gemnames,
370
+ directory: workspace.directory
371
+ )
372
+ end
373
+
358
374
  # Get an array of foldable ranges for the specified file.
359
375
  #
360
376
  # @deprecated The library should not need to handle folding ranges. The
@@ -381,14 +397,49 @@ module Solargraph
381
397
  # @param source [Source]
382
398
  # @return [Boolean] True if the source was merged into the workspace.
383
399
  def merge source
400
+ Logging.logger.debug "Merging source: #{source.filename}"
384
401
  result = false
385
402
  mutex.synchronize do
386
403
  result = workspace.merge(source)
387
- @synchronized = !result if synchronized?
404
+ maybe_map source
388
405
  end
406
+ # catalog
389
407
  result
390
408
  end
391
409
 
410
+ def source_map_hash
411
+ @source_map_hash ||= {}
412
+ end
413
+
414
+ def mapped?
415
+ (workspace.filenames - source_map_hash.keys).empty?
416
+ end
417
+
418
+ def next_map
419
+ return false if mapped?
420
+ mutex.synchronize do
421
+ @synchronized = false
422
+ src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
423
+ if src
424
+ Logging.logger.debug "Mapping #{src.filename}"
425
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
426
+ else
427
+ false
428
+ end
429
+ end
430
+ end
431
+
432
+ def map!
433
+ workspace.sources.each do |src|
434
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
435
+ end
436
+ self
437
+ end
438
+
439
+ def pins
440
+ @pins ||= []
441
+ end
442
+
392
443
  private
393
444
 
394
445
  # @return [Mutex]
@@ -401,14 +452,6 @@ module Solargraph
401
452
  @api_map ||= Solargraph::ApiMap.new
402
453
  end
403
454
 
404
- # @return [Bench]
405
- def bench
406
- Bench.new(
407
- workspace: workspace,
408
- opened: @current ? [@current] : []
409
- )
410
- end
411
-
412
455
  # Get the source for an open file or create a new source if the file
413
456
  # exists on disk. Sources created from disk are not added to the open
414
457
  # workspace files, i.e., the version on disk remains the authoritative
@@ -422,5 +465,35 @@ module Solargraph
422
465
  raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename)
423
466
  workspace.source(filename)
424
467
  end
468
+
469
+ def handle_file_not_found filename, error
470
+ if workspace.source(filename)
471
+ Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap"
472
+ nil
473
+ else
474
+ raise error
475
+ end
476
+ end
477
+
478
+ def maybe_map source
479
+ if source_map_hash.key?(source.filename)
480
+ return if source_map_hash[source.filename].code == source.code &&
481
+ source_map_hash[source.filename].source.synchronized? &&
482
+ source.synchronized?
483
+ if source.synchronized?
484
+ new_map = Solargraph::SourceMap.map(source)
485
+ unless source_map_hash[source.filename].try_merge!(new_map)
486
+ source_map_hash[source.filename] = new_map
487
+ @synchronized = false
488
+ end
489
+ else
490
+ # @todo Smelly instance variable access
491
+ source_map_hash[source.filename].instance_variable_set(:@source, source)
492
+ end
493
+ else
494
+ source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
495
+ @synchronized = false
496
+ end
497
+ end
425
498
  end
426
499
  end