solargraph 0.40.3 → 0.42.0

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