solargraph 0.40.3 → 0.42.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -5
  3. data/CHANGELOG.md +34 -0
  4. data/README.md +15 -0
  5. data/SPONSORS.md +1 -0
  6. data/lib/.rubocop.yml +4 -3
  7. data/lib/solargraph.rb +8 -7
  8. data/lib/solargraph/api_map.rb +40 -111
  9. data/lib/solargraph/api_map/store.rb +5 -0
  10. data/lib/solargraph/bench.rb +13 -16
  11. data/lib/solargraph/compat.rb +15 -1
  12. data/lib/solargraph/diagnostics/rubocop.rb +10 -2
  13. data/lib/solargraph/diagnostics/rubocop_helpers.rb +18 -0
  14. data/lib/solargraph/diagnostics/type_check.rb +1 -1
  15. data/lib/solargraph/language_server/host.rb +108 -7
  16. data/lib/solargraph/language_server/host/diagnoser.rb +9 -1
  17. data/lib/solargraph/language_server/host/sources.rb +1 -1
  18. data/lib/solargraph/language_server/message/completion_item/resolve.rb +1 -0
  19. data/lib/solargraph/language_server/message/extended/environment.rb +3 -3
  20. data/lib/solargraph/language_server/message/initialize.rb +37 -35
  21. data/lib/solargraph/language_server/message/text_document/formatting.rb +28 -7
  22. data/lib/solargraph/language_server/message/text_document/hover.rb +1 -1
  23. data/lib/solargraph/library.rb +132 -22
  24. data/lib/solargraph/parser/rubyvm/node_chainer.rb +0 -1
  25. data/lib/solargraph/parser/rubyvm/node_processors/args_node.rb +11 -12
  26. data/lib/solargraph/parser/rubyvm/node_processors/opt_arg_node.rb +1 -6
  27. data/lib/solargraph/shell.rb +5 -1
  28. data/lib/solargraph/source.rb +1 -1
  29. data/lib/solargraph/source/chain/head.rb +0 -16
  30. data/lib/solargraph/source/source_chainer.rb +1 -0
  31. data/lib/solargraph/source_map/mapper.rb +0 -5
  32. data/lib/solargraph/type_checker.rb +2 -2
  33. data/lib/solargraph/type_checker/checks.rb +4 -4
  34. data/lib/solargraph/version.rb +1 -1
  35. data/lib/solargraph/workspace.rb +1 -0
  36. data/lib/solargraph/workspace/config.rb +4 -3
  37. data/lib/solargraph/yard_map.rb +41 -39
  38. data/lib/solargraph/yard_map/core_fills.rb +1 -0
  39. data/solargraph.gemspec +1 -0
  40. metadata +16 -2
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
3
  require 'stringio'
5
4
 
6
5
  module Solargraph
@@ -23,6 +22,7 @@ module Solargraph
23
22
  # @param _api_map [Solargraph::ApiMap]
24
23
  # @return [Array<Hash>]
25
24
  def diagnose source, _api_map
25
+ require_rubocop(rubocop_version)
26
26
  options, paths = generate_options(source.filename, source.code)
27
27
  store = RuboCop::ConfigStore.new
28
28
  runner = RuboCop::Runner.new(options, store)
@@ -36,6 +36,13 @@ module Solargraph
36
36
 
37
37
  private
38
38
 
39
+ # Extracts the rubocop version from _args_
40
+ #
41
+ # @return [String]
42
+ def rubocop_version
43
+ args.find { |a| a =~ /version=/ }.to_s.split('=').last
44
+ end
45
+
39
46
  # @param resp [Hash]
40
47
  # @return [Array<Hash>]
41
48
  def make_array resp
@@ -57,7 +64,8 @@ module Solargraph
57
64
  range: offense_range(off).to_hash,
58
65
  # 1 = Error, 2 = Warning, 3 = Information, 4 = Hint
59
66
  severity: SEVERITIES[off['severity']],
60
- source: off['cop_name'],
67
+ source: 'rubocop',
68
+ code: off['cop_name'],
61
69
  message: off['message'].gsub(/^#{off['cop_name']}\:/, '')
62
70
  }
63
71
  end
@@ -7,6 +7,24 @@ module Solargraph
7
7
  module RubocopHelpers
8
8
  module_function
9
9
 
10
+ # Requires a specific version of rubocop, or the latest installed version
11
+ # if _version_ is `nil`.
12
+ #
13
+ # @param version [String]
14
+ # @raise [InvalidRubocopVersionError] if _version_ is not installed
15
+ def require_rubocop(version = nil)
16
+ begin
17
+ gem_path = Gem::Specification.find_by_name('rubocop', version).full_gem_path
18
+ gem_lib_path = File.join(gem_path, 'lib')
19
+ $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path)
20
+ rescue Gem::MissingSpecVersionError => e
21
+ raise InvalidRubocopVersionError,
22
+ "could not find '#{e.name}' (#{e.requirement}) - "\
23
+ "did find: [#{e.specs.map { |s| s.version.version }.join(', ')}]"
24
+ end
25
+ require 'rubocop'
26
+ end
27
+
10
28
  # Generate command-line options for the specified filename and code.
11
29
  #
12
30
  # @param filename [String]
@@ -7,7 +7,7 @@ module Solargraph
7
7
  #
8
8
  class TypeCheck < Base
9
9
  def diagnose source, api_map
10
- return [] unless args.include?('always') || api_map.workspaced?(source.filename)
10
+ # return [] unless args.include?('always') || api_map.workspaced?(source.filename)
11
11
  severity = Diagnostics::Severities::ERROR
12
12
  level = (args.reverse.find { |a| ['normal', 'typed', 'strict', 'strong'].include?(a) }) || :normal
13
13
  checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'diff/lcs'
3
4
  require 'observer'
5
+ require 'securerandom'
4
6
  require 'set'
5
7
 
6
8
  module Solargraph
@@ -106,9 +108,12 @@ module Solargraph
106
108
  end
107
109
  message
108
110
  elsif request['id']
109
- # @todo What if the id is invalid?
110
- requests[request['id']].process(request['result'])
111
- requests.delete request['id']
111
+ if requests[request['id']]
112
+ requests[request['id']].process(request['result'])
113
+ requests.delete request['id']
114
+ else
115
+ logger.warn "Discarding client response to unrecognized message #{request['id']}"
116
+ end
112
117
  else
113
118
  logger.warn "Invalid message received."
114
119
  logger.debug request
@@ -164,8 +169,6 @@ module Solargraph
164
169
  # @return [void]
165
170
  def open_from_disk uri
166
171
  sources.open_from_disk(uri)
167
- library = library_for(uri)
168
- # library.open_from_disk uri_to_file(uri)
169
172
  diagnoser.schedule uri
170
173
  end
171
174
 
@@ -192,7 +195,7 @@ module Solargraph
192
195
  def diagnose uri
193
196
  if sources.include?(uri)
194
197
  library = library_for(uri)
195
- if library.synchronized?
198
+ if library.mapped? && library.synchronized?
196
199
  logger.info "Diagnosing #{uri}"
197
200
  begin
198
201
  results = library.diagnose uri_to_file(uri)
@@ -277,6 +280,7 @@ module Solargraph
277
280
  begin
278
281
  lib = Solargraph::Library.load(path, name)
279
282
  libraries.push lib
283
+ async_library_map lib
280
284
  rescue WorkspaceTooLargeError => e
281
285
  send_notification 'window/showMessage', {
282
286
  'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
@@ -631,6 +635,7 @@ module Solargraph
631
635
 
632
636
  # @return [void]
633
637
  def catalog
638
+ return unless libraries.all?(&:mapped?)
634
639
  libraries.each(&:catalog)
635
640
  end
636
641
 
@@ -669,7 +674,8 @@ module Solargraph
669
674
  # @return [Source::Updater]
670
675
  def generate_updater params
671
676
  changes = []
672
- params['contentChanges'].each do |chng|
677
+ params['contentChanges'].each do |recvd|
678
+ chng = check_diff(params['textDocument']['uri'], recvd)
673
679
  changes.push Solargraph::Source::Change.new(
674
680
  (chng['range'].nil? ?
675
681
  nil :
@@ -685,6 +691,36 @@ module Solargraph
685
691
  )
686
692
  end
687
693
 
694
+ # @param uri [String]
695
+ # @param change [Hash]
696
+ # @return [Hash]
697
+ def check_diff uri, change
698
+ return change if change['range']
699
+ source = sources.find(uri)
700
+ return change if source.code.length + 1 != change['text'].length
701
+ diffs = Diff::LCS.diff(source.code, change['text'])
702
+ return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
703
+ # @type [Diff::LCS::Change]
704
+ diff = diffs.first.first
705
+ return change unless diff.adding? && ['.', ':'].include?(diff.element)
706
+ position = Solargraph::Position.from_offset(source.code, diff.position)
707
+ {
708
+ 'range' => {
709
+ 'start' => {
710
+ 'line' => position.line,
711
+ 'character' => position.character
712
+ },
713
+ 'end' => {
714
+ 'line' => position.line,
715
+ 'character' => position.character
716
+ }
717
+ },
718
+ 'text' => diff.element
719
+ }
720
+ rescue Solargraph::FileNotFoundError
721
+ change
722
+ end
723
+
688
724
  # @return [Hash]
689
725
  def dynamic_capability_options
690
726
  @dynamic_capability_options ||= {
@@ -741,6 +777,71 @@ module Solargraph
741
777
  def prepare_rename?
742
778
  client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
743
779
  end
780
+
781
+ def client_supports_progress?
782
+ client_capabilities['window'] && client_capabilities['window']['workDoneProgress']
783
+ end
784
+
785
+ # @param library [Library]
786
+ # @return [void]
787
+ def async_library_map library
788
+ return if library.mapped?
789
+ Thread.new do
790
+ if client_supports_progress?
791
+ uuid = SecureRandom.uuid
792
+ send_request 'window/workDoneProgress/create', {
793
+ token: uuid
794
+ } do |response|
795
+ do_async_library_map library, response.nil? ? uuid : nil
796
+ end
797
+ else
798
+ do_async_library_map library
799
+ end
800
+ end
801
+ end
802
+
803
+ def do_async_library_map library, uuid = nil
804
+ total = library.workspace.sources.length
805
+ if uuid
806
+ send_notification '$/progress', {
807
+ token: uuid,
808
+ value: {
809
+ kind: 'begin',
810
+ title: "Mapping workspace",
811
+ message: "0/#{total} files",
812
+ cancellable: false,
813
+ percentage: 0
814
+ }
815
+ }
816
+ end
817
+ pct = 0
818
+ mod = 10
819
+ while library.next_map
820
+ next unless uuid
821
+ cur = ((library.source_map_hash.keys.length.to_f / total.to_f) * 100).to_i
822
+ if cur > pct && cur % mod == 0
823
+ pct = cur
824
+ send_notification '$/progress', {
825
+ token: uuid,
826
+ value: {
827
+ kind: 'report',
828
+ cancellable: false,
829
+ message: "#{library.source_map_hash.keys.length}/#{total} files",
830
+ percentage: pct
831
+ }
832
+ }
833
+ end
834
+ end
835
+ if uuid
836
+ send_notification '$/progress', {
837
+ token: uuid,
838
+ value: {
839
+ kind: 'end',
840
+ message: 'Mapping complete'
841
+ }
842
+ }
843
+ end
844
+ end
744
845
  end
745
846
  end
746
847
  end
@@ -62,7 +62,15 @@ module Solargraph
62
62
  end
63
63
  current = mutex.synchronize { queue.shift }
64
64
  return if queue.include?(current)
65
- host.diagnose current
65
+ begin
66
+ host.diagnose current
67
+ rescue InvalidOffsetError
68
+ # @todo This error can occur when the Source is out of sync with
69
+ # with the ApiMap. It's probably not the best way to handle it,
70
+ # but it's quick and easy.
71
+ Logging.logger.warn "Deferring diagnosis due to invalid offset: #{current}"
72
+ mutex.synchronize { queue.push current }
73
+ end
66
74
  end
67
75
 
68
76
  private
@@ -76,7 +76,7 @@ module Solargraph
76
76
  source = Solargraph::Source.load(UriHelpers.uri_to_file(uri))
77
77
  open_source_hash[uri] = source
78
78
  end
79
-
79
+
80
80
  # Update an existing source.
81
81
  #
82
82
  # @raise [FileNotFoundError] if the URI does not match an open source.
@@ -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
@@ -56,6 +51,7 @@ module Solargraph
56
51
  end
57
52
 
58
53
  def static_completion
54
+ return {} unless host.options['completion']
59
55
  {
60
56
  completionProvider: {
61
57
  resolveProvider: true,
@@ -89,18 +85,21 @@ module Solargraph
89
85
  end
90
86
 
91
87
  def static_hover
88
+ return {} unless host.options['hover']
92
89
  {
93
90
  hoverProvider: true
94
91
  }
95
92
  end
96
93
 
97
94
  def static_document_formatting
95
+ return {} unless host.options['formatting']
98
96
  {
99
97
  documentFormattingProvider: true
100
98
  }
101
99
  end
102
100
 
103
101
  def static_document_symbols
102
+ return {} unless host.options['symbols']
104
103
  {
105
104
  documentSymbolProvider: true
106
105
  }
@@ -113,6 +112,7 @@ module Solargraph
113
112
  end
114
113
 
115
114
  def static_definitions
115
+ return {} unless host.options['definitions']
116
116
  {
117
117
  definitionProvider: true
118
118
  }
@@ -125,12 +125,14 @@ module Solargraph
125
125
  end
126
126
 
127
127
  def static_references
128
+ return {} unless host.options['references']
128
129
  {
129
130
  referencesProvider: true
130
131
  }
131
132
  end
132
133
 
133
134
  def static_folding_range
135
+ return {} unless host.options['folding']
134
136
  {
135
137
  foldingRangeProvider: true
136
138
  }
@@ -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
 
@@ -11,21 +10,22 @@ module Solargraph
11
10
  class Formatting < Base
12
11
  include Solargraph::Diagnostics::RubocopHelpers
13
12
 
14
- class BlankRubocopFormatter < ::RuboCop::Formatter::BaseFormatter; end
15
-
16
13
  def process
17
14
  file_uri = params['textDocument']['uri']
18
15
  config = config_for(file_uri)
19
16
  original = host.read_text(file_uri)
20
17
  args = cli_args(file_uri, config)
21
18
 
19
+ require_rubocop(config['version'])
22
20
  options, paths = RuboCop::Options.new.parse(args)
23
21
  options[:stdin] = original
24
- redirect_stdout do
22
+ corrections = redirect_stdout do
25
23
  RuboCop::Runner.new(options, RuboCop::ConfigStore.new).run(paths)
26
24
  end
27
25
  result = options[:stdin]
28
26
 
27
+ log_corrections(corrections)
28
+
29
29
  format original, result
30
30
  rescue RuboCop::ValidationError, RuboCop::ConfigNotFoundError => e
31
31
  set_error(Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}")
@@ -33,6 +33,17 @@ module Solargraph
33
33
 
34
34
  private
35
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
+
36
47
  def config_for(file_uri)
37
48
  conf = host.formatter_config(file_uri)
38
49
  return {} unless conf.is_a?(Hash)
@@ -40,12 +51,12 @@ module Solargraph
40
51
  conf['rubocop'] || {}
41
52
  end
42
53
 
43
- def cli_args file, config
54
+ def cli_args file_uri, config
55
+ file = UriHelpers.uri_to_file(file_uri)
44
56
  args = [
45
57
  config['cops'] == 'all' ? '--auto-correct-all' : '--auto-correct',
46
58
  '--cache', 'false',
47
- '--format', 'Solargraph::LanguageServer::Message::' \
48
- 'TextDocument::Formatting::BlankRubocopFormatter',
59
+ '--format', formatter_class(config).name,
49
60
  ]
50
61
 
51
62
  ['except', 'only'].each do |arg|
@@ -57,6 +68,16 @@ module Solargraph
57
68
  args + [file]
58
69
  end
59
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
+
60
81
  def cop_list(value)
61
82
  value = value.join(',') if value.respond_to?(:join)
62
83
  return nil if value == '' || !value.is_a?(String)