solargraph 0.25.1 → 0.26.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/solargraph.rb +18 -16
  3. data/lib/solargraph/api_map.rb +100 -161
  4. data/lib/solargraph/api_map/source_to_yard.rb +9 -9
  5. data/lib/solargraph/api_map/store.rb +50 -13
  6. data/lib/solargraph/basic_type.rb +33 -0
  7. data/lib/solargraph/basic_type_methods.rb +111 -0
  8. data/lib/solargraph/complex_type.rb +51 -89
  9. data/lib/solargraph/core_fills.rb +12 -8
  10. data/lib/solargraph/diagnostics/type_not_defined.rb +2 -2
  11. data/lib/solargraph/language_server.rb +3 -0
  12. data/lib/solargraph/language_server/completion_item_kinds.rb +2 -0
  13. data/lib/solargraph/language_server/error_codes.rb +2 -0
  14. data/lib/solargraph/language_server/host.rb +53 -6
  15. data/lib/solargraph/language_server/message.rb +13 -0
  16. data/lib/solargraph/language_server/message/text_document/definition.rb +4 -6
  17. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +2 -1
  18. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +2 -1
  19. data/lib/solargraph/language_server/message_types.rb +2 -0
  20. data/lib/solargraph/language_server/request.rb +4 -0
  21. data/lib/solargraph/language_server/symbol_kinds.rb +28 -26
  22. data/lib/solargraph/language_server/transport.rb +3 -0
  23. data/lib/solargraph/language_server/uri_helpers.rb +2 -0
  24. data/lib/solargraph/library.rb +12 -7
  25. data/lib/solargraph/pin.rb +1 -1
  26. data/lib/solargraph/pin/attribute.rb +5 -5
  27. data/lib/solargraph/pin/base.rb +51 -16
  28. data/lib/solargraph/pin/base_variable.rb +25 -7
  29. data/lib/solargraph/pin/block.rb +18 -1
  30. data/lib/solargraph/pin/block_parameter.rb +42 -5
  31. data/lib/solargraph/pin/conversions.rb +4 -2
  32. data/lib/solargraph/pin/method.rb +6 -6
  33. data/lib/solargraph/pin/method_parameter.rb +6 -6
  34. data/lib/solargraph/pin/namespace.rb +7 -2
  35. data/lib/solargraph/pin/proxy_type.rb +39 -0
  36. data/lib/solargraph/pin/symbol.rb +20 -12
  37. data/lib/solargraph/pin/yard_pin/method.rb +2 -2
  38. data/lib/solargraph/source.rb +89 -38
  39. data/lib/solargraph/source/call_chainer.rb +273 -0
  40. data/lib/solargraph/source/chain.rb +104 -0
  41. data/lib/solargraph/source/chain/call.rb +72 -0
  42. data/lib/solargraph/source/chain/class_variable.rb +11 -0
  43. data/lib/solargraph/source/chain/constant.rb +17 -0
  44. data/lib/solargraph/source/chain/definition.rb +16 -0
  45. data/lib/solargraph/source/chain/global_variable.rb +11 -0
  46. data/lib/solargraph/source/chain/head.rb +20 -0
  47. data/lib/solargraph/source/chain/instance_variable.rb +11 -0
  48. data/lib/solargraph/source/chain/link.rb +33 -0
  49. data/lib/solargraph/source/chain/literal.rb +21 -0
  50. data/lib/solargraph/source/chain/variable.rb +11 -0
  51. data/lib/solargraph/source/change.rb +3 -1
  52. data/lib/solargraph/{api_map → source}/completion.rb +3 -1
  53. data/lib/solargraph/source/encoding_fixes.rb +21 -0
  54. data/lib/solargraph/source/fragment.rb +139 -284
  55. data/lib/solargraph/source/mapper.rb +27 -16
  56. data/lib/solargraph/source/node_chainer.rb +94 -0
  57. data/lib/solargraph/source/node_methods.rb +2 -2
  58. data/lib/solargraph/source/position.rb +4 -0
  59. data/lib/solargraph/source/range.rb +10 -2
  60. data/lib/solargraph/version.rb +1 -1
  61. data/lib/solargraph/yard_map.rb +13 -2
  62. metadata +20 -6
  63. data/lib/solargraph/api_map/probe.rb +0 -251
  64. data/lib/solargraph/api_map/type_methods.rb +0 -40
  65. data/lib/solargraph/pin/proxy_method.rb +0 -30
@@ -0,0 +1,104 @@
1
+ # HACK Fix autoload issue
2
+ require 'solargraph/source/chain/link'
3
+
4
+ module Solargraph
5
+ class Source
6
+ class Chain
7
+ autoload :Link, 'solargraph/source/chain/link'
8
+ autoload :Call, 'solargraph/source/chain/call'
9
+ autoload :Variable, 'solargraph/source/chain/variable'
10
+ autoload :ClassVariable, 'solargraph/source/chain/class_variable'
11
+ autoload :Constant, 'solargraph/source/chain/constant'
12
+ autoload :InstanceVariable, 'solargraph/source/chain/instance_variable'
13
+ autoload :GlobalVariable, 'solargraph/source/chain/global_variable'
14
+ autoload :Literal, 'solargraph/source/chain/literal'
15
+ autoload :Definition, 'solargraph/source/chain/definition'
16
+ autoload :Head, 'solargraph/source/chain/head'
17
+
18
+ UNDEFINED_CALL = Source::Chain::Call.new('<undefined>')
19
+ UNDEFINED_CONSTANT = Source::Chain::Constant.new('<undefined>')
20
+
21
+ # @return [Array<Source::Chain::Link>]
22
+ attr_reader :links
23
+
24
+ # @param filename [String]
25
+ # @param links [Array<Chain::Link>]
26
+ def initialize filename, links
27
+ @filename = filename
28
+ @links = links
29
+ @links.push UNDEFINED_CALL if @links.empty?
30
+ end
31
+
32
+ # @return [Array<Source::Chain::Link>]
33
+ def base
34
+ # @todo It might make sense for the chain links to always have a root.
35
+ @base ||= links[0..-2]
36
+ end
37
+
38
+ # @return [Source::Chain::Link]
39
+ def tail
40
+ @tail ||= links.last
41
+ end
42
+
43
+ def literal?
44
+ tail.is_a?(Literal)
45
+ end
46
+
47
+ # @param api_map [ApiMap]
48
+ # @param context [Pin::Base]
49
+ # @param locals [Array<Pin::Base>]
50
+ # @return [Array<Pin::Base>]
51
+ def define_with api_map, context, locals
52
+ inner_define_with links, api_map, context, locals
53
+ end
54
+
55
+ def define_base_with api_map, context, locals
56
+ inner_define_with links[0..-2], api_map, context, locals
57
+ end
58
+
59
+ # @param api_map [ApiMap]
60
+ # @param context [Pin::Base]
61
+ # @param locals [Array<Pin::Base>]
62
+ # @return [ComplexType]
63
+ def infer_type_with api_map, context, locals
64
+ # @todo Perform link inference
65
+ inner_infer_type_with(links, api_map, context, locals)
66
+ end
67
+
68
+ def infer_base_type_with api_map, context, locals
69
+ inner_infer_type_with(links[0..-2], api_map, context, locals)
70
+ end
71
+
72
+ private
73
+
74
+ def inner_infer_type_with array, api_map, context, locals
75
+ type = ComplexType::UNDEFINED
76
+ pins = inner_define_with(array, api_map, context, locals)
77
+ pins.each do |pin|
78
+ type = pin.infer(api_map)
79
+ break unless type.undefined?
80
+ end
81
+ type
82
+ end
83
+
84
+ def inner_define_with array, api_map, context, locals
85
+ return [] if array.empty?
86
+ type = ComplexType::UNDEFINED
87
+ head = true
88
+ # @param link [Chain::Link]
89
+ array[0..-2].each do |link|
90
+ pins = link.resolve_pins(api_map, context, head ? locals : [])
91
+ head = false
92
+ return [] if pins.empty?
93
+ pins.each do |pin|
94
+ type = pin.infer(api_map)
95
+ break unless type.undefined?
96
+ end
97
+ return [] if type.undefined?
98
+ context = Pin::ProxyType.anonymous(type)
99
+ end
100
+ array.last.resolve_pins(api_map, context, head ? locals: [])
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,72 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Call < Link
5
+ # @return [String]
6
+ attr_reader :word
7
+
8
+ # @return [Array<Chain>]
9
+ attr_reader :arguments
10
+
11
+ def initialize word, arguments = []
12
+ @word = word
13
+ @arguments = arguments
14
+ end
15
+
16
+ def resolve_pins api_map, context, locals
17
+ found = locals.select{|p| p.name == word}
18
+ return inferred_pins(found, api_map, context) unless found.empty?
19
+ pins = api_map.get_method_stack(namespace_from_context(context), word, scope: context.scope)
20
+ return [] if pins.empty?
21
+ pins[0] = virtual_new_pin(pins.first, context) if pins.first.path == 'Class#new'
22
+ inferred_pins(pins, api_map, context)
23
+ end
24
+
25
+ private
26
+
27
+ # @param pin [Pin::Base]
28
+ # @return [String]
29
+ def namespace_from_context pin
30
+ return pin.namespace if pin.kind == Pin::ATTRIBUTE or pin.kind == Pin::METHOD
31
+ pin.return_complex_type.namespace
32
+ end
33
+
34
+ # Create a `new` pin to facilitate type inference. This is necessary for
35
+ # classes from YARD and classes in the namespace that do not have an
36
+ # `initialize` method.
37
+ #
38
+ # @param new_pin [Solargraph::Pin::Base]
39
+ # @param context_pin [Solargraph::Pin::Base]
40
+ # @return [Pin::Method]
41
+ def virtual_new_pin new_pin, context_pin
42
+ pin = Pin::Method.new(new_pin.location, context_pin.path, new_pin.name, '', :class, new_pin.visibility, new_pin.parameters)
43
+ # @todo Smelly instance variable access.
44
+ pin.instance_variable_set(:@return_complex_type, ComplexType.parse(context_pin.path))
45
+ pin
46
+ end
47
+
48
+ def self_pin(api_map, context)
49
+ return Pin::ProxyType.anonymous(ComplexType.parse(context.namespace)) if context.scope == :instance
50
+ # return api_map.get_path_suggestions(context.namespace)
51
+ context
52
+ end
53
+
54
+ def inferred_pins pins, api_map, context_pin
55
+ result = pins.uniq(&:location).map do |p|
56
+ if CoreFills::METHODS_RETURNING_SELF.include?(p.path)
57
+ next Solargraph::Pin::Method.new(p.location, p.namespace, p.name, "@return [#{context_pin.return_complex_type.tag}]", p.scope, p.visibility, p.parameters)
58
+ end
59
+ if CoreFills::METHODS_RETURNING_SUBTYPES.include?(p.path) and !context_pin.return_complex_type.subtypes.empty?
60
+ next Solargraph::Pin::Method.new(p.location, p.namespace, p.name, "@return [#{context_pin.return_complex_type.subtypes.first.tag}]", p.scope, p.visibility, p.parameters)
61
+ end
62
+ next p if p.kind == Pin::METHOD or p.kind == Pin::ATTRIBUTE or p.kind == Pin::NAMESPACE
63
+ type = p.infer(api_map)
64
+ next p if p.return_complex_type == type
65
+ Pin::ProxyType.new(p.location, nil, p.name, type)
66
+ end
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,11 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class ClassVariable < Link
5
+ def resolve_pins api_map, context, locals
6
+ api_map.get_class_variable_pins(context.namespace).select{|p| p.name == word}
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Constant < Link
5
+ def initialize word
6
+ @word = word
7
+ end
8
+
9
+ def resolve_pins api_map, context, locals
10
+ ns = api_map.qualify(word, context.named_context)
11
+ return [] if ns.nil?
12
+ api_map.get_path_suggestions(ns)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Definition < Link
5
+ # @param location [Solargraph::Source::Location]
6
+ def initialize location
7
+ @location = location
8
+ end
9
+
10
+ def resolve_pins api_map, context, locals
11
+ api_map.locate_pin(@location)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class GlobalVariable < Link
5
+ def resolve_pins api_map, context, locals
6
+ api_map.get_global_variable_pins.select{|p| p.name == word}
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ # Chain::Head is a link for ambiguous words, e.g.; `String` can refer to
5
+ # either a class (`String`) or a function (`Kernel#String`).
6
+ #
7
+ class Head < Call
8
+ def resolve_pins api_map, context, locals
9
+ return [self_pin(api_map, context)] if word == 'self'
10
+ base = super
11
+ return base if locals.map(&:name).include?(word)
12
+ here = []
13
+ ns = api_map.qualify(word, context.named_context)
14
+ here.concat api_map.get_path_suggestions(ns) unless ns.nil?
15
+ here + base
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class InstanceVariable < Link
5
+ def resolve_pins api_map, context, locals
6
+ api_map.get_instance_variable_pins(context.namespace, context.scope).select{|p| p.name == word}
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Link
5
+ attr_reader :word
6
+
7
+ def initialize word = '<undefined>'
8
+ @word = word
9
+ end
10
+
11
+ def undefined?
12
+ word == '<undefined>'
13
+ end
14
+
15
+ def constant?
16
+ is_a?(Chain::Constant)
17
+ end
18
+
19
+ # @param api_map [ApiMap]
20
+ # @param context [ComplexType]
21
+ # @param locals [Array<Solargraph::Pin::Base>]
22
+ # @return [Array<Solargraph::Pin::Base>]
23
+ def resolve_pins api_map, context, locals
24
+ []
25
+ end
26
+
27
+ def == other
28
+ self.class == other.class and word == other.word
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Literal < Link
5
+ def word
6
+ @word ||= "<#{@type}>"
7
+ end
8
+
9
+ # @param type [String]
10
+ def initialize type
11
+ @type = type
12
+ @complex_type = ComplexType.parse(type).first
13
+ end
14
+
15
+ def resolve_pins api_map, context, locals
16
+ [Pin::ProxyType.anonymous(@complex_type)]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Solargraph
2
+ class Source
3
+ class Chain
4
+ class Variable < Link
5
+ def resolve_pins api_map, context, locals
6
+ api_map.get_instance_variable_pins(context.namespace, context.scope).select{|p| p.name == word}
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,6 +3,8 @@ module Solargraph
3
3
  # A change to be applied to text.
4
4
  #
5
5
  class Change
6
+ include EncodingFixes
7
+
6
8
  # @return [Range]
7
9
  attr_reader :range
8
10
 
@@ -68,7 +70,7 @@ module Solargraph
68
70
  def commit text, insert
69
71
  start_offset = Position.to_offset(text, range.start)
70
72
  end_offset = Position.to_offset(text, range.ending)
71
- (start_offset == 0 ? '' : text[0..start_offset-1].to_s) + insert.force_encoding('utf-8') + text[end_offset..-1].to_s
73
+ (start_offset == 0 ? '' : text[0..start_offset-1].to_s) + normalize(insert) + text[end_offset..-1].to_s
72
74
  end
73
75
  end
74
76
  end
@@ -1,5 +1,5 @@
1
1
  module Solargraph
2
- class ApiMap
2
+ class Source
3
3
  # The result of a completion request containing the pins that describe
4
4
  # completion options and the range to be replaced.
5
5
  #
@@ -10,6 +10,8 @@ module Solargraph
10
10
  # @return [Solargraph::Source::Range]
11
11
  attr_reader :range
12
12
 
13
+ # @param pins [Array<Solargraph::Pin::Base>]
14
+ # @param range [Solargraph::Source::Range]
13
15
  def initialize pins, range
14
16
  @pins = pins
15
17
  @range = range
@@ -0,0 +1,21 @@
1
+ module Solargraph
2
+ class Source
3
+ module EncodingFixes
4
+ module_function
5
+
6
+ # Convert strings to normalized UTF-8.
7
+ #
8
+ # @param string [String]
9
+ # @return [String]
10
+ def normalize string
11
+ begin
12
+ string.force_encoding('UTF-8')
13
+ rescue ::Encoding::CompatibilityError, ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError => e
14
+ # @todo Improve error handling
15
+ STDERR.puts "Normalize error: #{e.message}"
16
+ string
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -79,114 +79,63 @@ module Solargraph
79
79
  @scope
80
80
  end
81
81
 
82
- # Get the signature up to the current offset. Given the text `foo.bar`,
83
- # the signature at offset 5 is `foo.b`.
84
- #
85
- # @return [String]
86
- def signature
87
- @signature ||= signature_data[1].to_s
88
- end
89
-
90
- def valid?
91
- @source.parsed?
92
- end
93
-
94
- def broken?
95
- !valid?
96
- end
97
-
98
82
  # Get the signature before the current word. Given the signature
99
83
  # `String.new.split`, the base is `String.new`.
100
84
  #
101
85
  # @return [String]
102
86
  def base
103
- if @base.nil?
104
- if signature.include?('.')
105
- if signature.end_with?('.')
106
- @base = signature[0..-2]
107
- else
108
- @base = signature.split('.')[0..-2].join('.')
109
- end
110
- elsif signature.include?('::')
111
- if signature.end_with?('::')
112
- @base = signature[0..-3]
113
- else
114
- @base = signature.split('::')[0..-2].join('::')
115
- end
116
- else
117
- # @base = signature
118
- @base = ''
119
- end
120
- end
121
- @base
122
- end
123
-
124
- # @return [String]
125
- def root
126
- @root ||= signature.split('.').first
87
+ chain.links[0..-2].map(&:word).join('.')
127
88
  end
128
89
 
129
- # @return [String]
90
+ # @return [Source::Chain]
130
91
  def chain
131
- @chain ||= ( signature.empty? ? '' : signature.split('.')[1..-1].join('.') )
132
- end
133
-
134
- # @return [String]
135
- def base_chain
136
- @base_chain ||= signature.split('.')[1..-2].join('.')
92
+ @chain ||= generate_chain
137
93
  end
138
94
 
95
+ # Get the whole signature at the current offset, including the final
96
+ # word and its remainder.
97
+ #
139
98
  # @return [String]
140
- def whole_chain
141
- @whole_chain ||= whole_signature.split('.')[1..-1].join('.')
99
+ def whole_signature
100
+ chain.links.reject{|l| l.word == '<undefined>'}.map(&:word).join('.')
142
101
  end
143
102
 
144
- # Get the remainder of the word after the current offset. Given the text
145
- # `foobar` with an offset of 3, the remainder is `bar`.
103
+ # Get the word before the current offset. Given the text `foo.bar`, the
104
+ # word at offset 6 is `ba`.
146
105
  #
147
106
  # @return [String]
148
- def remainder
149
- @remainder ||= remainder_at(offset)
107
+ def start_of_word
108
+ @start_of_word ||= begin
109
+ match = @code[0..offset-1].to_s.match(start_word_pattern)
110
+ result = (match ? match[0] : '')
111
+ result = ":#{result}" if @code[0..offset-result.length].end_with?('::') and !@code[0..offset-result.length].end_with?('::')
112
+ result
113
+ end
150
114
  end
151
115
 
152
- # Get the whole word at the current offset, including the remainder.
153
- # Given the text `foobar.baz`, the whole word at any offset from 0 to 6
154
- # is `foobar`.
155
- #
156
- # @return [String]
157
- def whole_word
158
- @whole_word ||= word + remainder
116
+ def word
117
+ start_of_word
159
118
  end
160
119
 
161
- # Get the whole signature at the current offset, including the final
162
- # word and its remainder.
163
- #
164
- # @return [String]
165
- def whole_signature
166
- @whole_signature ||= signature + remainder
120
+ def end_of_word
121
+ @end_of_word ||= begin
122
+ match = @code[offset..-1].to_s.match(end_word_pattern)
123
+ match ? match[0] : ''
124
+ end
167
125
  end
168
126
 
169
- # Get the entire phrase up to the current offset. Given the text
170
- # `foo[bar].baz()`, the phrase at offset 10 is `foo[bar].b`.
171
- #
172
- # @return [String]
173
- def phrase
174
- @phrase ||= @code[signature_data[0]..offset]
127
+ def remainder
128
+ end_of_word
175
129
  end
176
130
 
177
- # Get the word before the current offset. Given the text `foo.bar`, the
178
- # word at offset 6 is `ba`.
179
- #
180
- # @return [String]
181
- def word
182
- @word ||= word_at(offset)
131
+ def whole_word
132
+ start_of_word + end_of_word
183
133
  end
184
134
 
185
135
  # True if the current offset is inside a string.
186
136
  #
187
137
  # @return [Boolean]
188
138
  def string?
189
- # @string ||= (node.type == :str or node.type == :dstr)
190
139
  @string ||= @source.string_at?(line, character)
191
140
  end
192
141
 
@@ -194,22 +143,15 @@ module Solargraph
194
143
  #
195
144
  # @return [Boolean]
196
145
  def comment?
197
- @comment ||= check_comment(line, column)
198
- end
199
-
200
- # Get the range of the word up to the current offset.
201
- #
202
- # @return [Range]
203
- def word_range
204
- @word_range ||= word_range_at(offset, false)
146
+ @comment ||= @source.comment_at?(line, column)
205
147
  end
206
148
 
207
149
  # Get the range of the whole word at the current offset, including its
208
150
  # remainder.
209
151
  #
210
152
  # @return [Range]
211
- def whole_word_range
212
- @whole_word_range ||= word_range_at(offset, true)
153
+ def word_range
154
+ @word_range ||= word_range_at(offset - start_of_word.length, offset + end_of_word.length)
213
155
  end
214
156
 
215
157
  # @return [Solargraph::Pin::Base]
@@ -222,30 +164,13 @@ module Solargraph
222
164
  @named_path ||= @source.locate_named_path_pin(line, character)
223
165
  end
224
166
 
167
+ # Get an array of all the locals that are visible from the fragment's
168
+ # position. Locals can be local variables, method parameters, or block
169
+ # parameters. The array starts with the nearest local pin.
170
+ #
225
171
  # @return [Array<Solargraph::Pin::Base>]
226
172
  def locals
227
- @locals ||= @source.locals.select{|pin| pin.visible_from?(block, position)}
228
- end
229
-
230
- # True if the fragment is a signature that stems from a literal value.
231
- #
232
- # @return [Boolean]
233
- def base_literal?
234
- !base_literal.nil?
235
- end
236
-
237
- # The type of literal value at the root of the signature (or nil).
238
- #
239
- # @return [String]
240
- def base_literal
241
- if @base_literal.nil? and !@calculated_literal
242
- @calculated_literal = true
243
- if signature.start_with?('.')
244
- pn = @source.node_at(line, column - 2)
245
- @base_literal = infer_literal_node_type(pn) unless pn.nil?
246
- end
247
- end
248
- @base_literal
173
+ @locals ||= @source.locals.select{|pin| pin.visible_from?(block, position)}.reverse
249
174
  end
250
175
 
251
176
  # True if the fragment is inside a literal value.
@@ -267,6 +192,70 @@ module Solargraph
267
192
  end
268
193
  end
269
194
 
195
+ # Get a set of available completions for the specified fragment. The
196
+ # resulting Completion object contains an array of pins and the range of
197
+ # text to replace in the source.
198
+ #
199
+ # @param api_map [ApiMap]
200
+ # @return [Completion]
201
+ def complete api_map
202
+ return Completion.new([], word_range) if chain.literal? or comment?
203
+ result = []
204
+ type = infer_base_type(api_map)
205
+ if chain.tail.constant?
206
+ result.concat api_map.get_constants(type.namespace, namespace)
207
+ else
208
+ result.concat api_map.get_complex_type_methods(type, namespace, chain.links.length == 1)
209
+ if chain.links.length == 1
210
+ if word.start_with?('@@')
211
+ return package_completions(api_map.get_class_variable_pins(namespace))
212
+ elsif word.start_with?('@')
213
+ return package_completions(api_map.get_instance_variable_pins(namespace, scope))
214
+ elsif word.start_with?('$')
215
+ return package_completions(api_map.get_global_variable_pins)
216
+ elsif word.start_with?(':') and !word.start_with?('::')
217
+ return package_completions(api_map.get_symbols)
218
+ end
219
+ result.concat api_map.get_constants('', namespace)
220
+ result.concat prefer_non_nil_variables(locals)
221
+ result.concat api_map.get_methods(namespace, scope: scope, visibility: [:public, :private, :protected])
222
+ result.concat api_map.get_methods('Kernel')
223
+ result.concat ApiMap.keywords
224
+ end
225
+ end
226
+ package_completions(result)
227
+ end
228
+
229
+ def define api_map
230
+ return [] if chain.literal?
231
+ return [] if string? or comment? or literal?
232
+ # HACK: Checking for self first because it's also a keyword
233
+ return [] if ApiMap::KEYWORDS.include?(chain.links.first.word) and chain.links.first.word != 'self'
234
+ chain.define_with(api_map, named_path, locals)
235
+ end
236
+
237
+ # Get an array of pins that describe the method being called by the
238
+ # argument list where the fragment is located. This is useful for queries
239
+ # that need to know what parameters the current method expects to receive.
240
+ #
241
+ # If the fragment is not inside an argument list, return an empty array.
242
+ #
243
+ # @param api_map [Solargraph::Source::Fragment]
244
+ # @return [Array<Solargraph::Pin::Base>]
245
+ def signify api_map
246
+ return [] unless argument?
247
+ return [] if recipient.whole_signature.nil? or recipient.whole_signature.empty?
248
+ result = []
249
+ result.concat recipient.define(api_map)
250
+ result.select{ |pin| pin.kind == Pin::METHOD }
251
+ end
252
+
253
+ # @param api_map [ApiMap]
254
+ # @return [ComplexType]
255
+ def infer_base_type api_map
256
+ chain.infer_base_type_with(api_map, named_path, locals)
257
+ end
258
+
270
259
  private
271
260
 
272
261
  # @return [Integer]
@@ -283,175 +272,11 @@ module Solargraph
283
272
  [pos.line, pos.character]
284
273
  end
285
274
 
286
- def signature_data
287
- @signature_data ||= get_signature_data_at(offset)
288
- end
289
-
290
- def get_signature_data_at index
291
- brackets = 0
292
- squares = 0
293
- parens = 0
294
- signature = ''
295
- index -=1
296
- in_whitespace = false
297
- while index >= 0
298
- pos = Position.from_offset(@code, index)
299
- break if index > 0 and check_comment(pos.line, pos.character)
300
- unless !in_whitespace and string?
301
- break if brackets > 0 or parens > 0 or squares > 0
302
- char = @code[index, 1]
303
- break if char.nil? # @todo Is this the right way to handle this?
304
- if brackets.zero? and parens.zero? and squares.zero? and [' ', "\r", "\n", "\t"].include?(char)
305
- in_whitespace = true
306
- else
307
- if brackets.zero? and parens.zero? and squares.zero? and in_whitespace
308
- unless char == '.' or @code[index+1..-1].strip.start_with?('.')
309
- old = @code[index+1..-1]
310
- nxt = @code[index+1..-1].lstrip
311
- index += (@code[index+1..-1].length - @code[index+1..-1].lstrip.length)
312
- break
313
- end
314
- end
315
- if char == ')'
316
- parens -=1
317
- elsif char == ']'
318
- squares -=1
319
- elsif char == '}'
320
- brackets -= 1
321
- elsif char == '('
322
- parens += 1
323
- elsif char == '{'
324
- brackets += 1
325
- elsif char == '['
326
- squares += 1
327
- signature = ".[]#{signature}" if parens.zero? and brackets.zero? and squares.zero? and @code[index-2] != '%'
328
- end
329
- if brackets.zero? and parens.zero? and squares.zero?
330
- break if ['"', "'", ',', ';', '%'].include?(char)
331
- signature = char + signature if char.match(/[a-z0-9:\._@\$\?\!]/i) and @code[index - 1] != '%'
332
- break if char == '$'
333
- if char == '@'
334
- signature = "@#{signature}" if @code[index-1, 1] == '@'
335
- break
336
- end
337
- end
338
- in_whitespace = false
339
- end
340
- end
341
- index -= 1
342
- end
343
- # @todo Smelly exceptional case for integer literals
344
- match = signature.match(/^[0-9]+/)
345
- if match
346
- index += match[0].length
347
- signature = signature[match[0].length..-1].to_s
348
- @base_literal = 'Integer'
349
- # @todo Smelly exceptional case for array literals
350
- elsif signature.start_with?('.[]')
351
- index += 2
352
- signature = signature[3..-1].to_s
353
- @base_literal = 'Array'
354
- elsif signature.start_with?('.')
355
- pos = Position.from_offset(source.code, index)
356
- node = source.node_at(pos.line, pos.character)
357
- lit = infer_literal_node_type(node)
358
- unless lit.nil?
359
- signature = signature[1..-1].to_s
360
- index += 1
361
- @base_literal = lit
362
- end
363
- end
364
- [index + 1, signature]
365
- end
366
-
367
- # Determine if the specified location is inside a comment.
368
- #
369
- # @param lin [Integer]
370
- # @param col [Integer]
371
- # @return [Boolean]
372
- def check_comment(lin, col)
373
- index = Position.line_char_to_offset(source_from_parser, lin, col)
374
- @source.comments.each do |c|
375
- return true if index > c.location.expression.begin_pos and index <= c.location.expression.end_pos
376
- end
377
- false
378
- end
379
-
380
- # True if the line and column are inside the specified range.
381
- #
382
- # @param location [Parser::Source::Range]
383
- def compare_range line, column, location
384
- return true if line == location.first_line and line == location.last_line and column >= location.column and column < location.last_column
385
- return true if line > location.first_line and line < location.last_line
386
- return true if line == location.last_line and column >= location.last_column and column < location.last_column
387
- false
388
- end
389
-
390
- # Select the word that directly precedes the specified index.
391
- # A word can only consist of letters, numbers, and underscores.
392
- #
393
- # @param index [Integer]
394
- # @return [String]
395
- def word_at index
396
- @code[beginning_of_word_at(index)..index - 1].to_s
397
- end
398
-
399
- def beginning_of_word_at index
400
- cursor = index - 1
401
- # Words can end with ? or !
402
- if @code[cursor, 1] == '!' or @code[cursor, 1] == '?'
403
- cursor -= 1
404
- end
405
- while cursor > -1
406
- char = @code[cursor, 1]
407
- break if char.nil? or char.strip.empty?
408
- break unless char.match(/[a-z0-9_]/i)
409
- cursor -= 1
410
- end
411
- # Words can begin with @@, @, $, or :
412
- if cursor > -1
413
- if cursor > 0 and @code[cursor - 1, 2] == '@@'
414
- cursor -= 2
415
- elsif @code[cursor, 1] == '@' or @code[cursor, 1] == '$'
416
- cursor -= 1
417
- elsif @code[cursor, 1] == ':' and (cursor == 0 or @code[cursor - 1, 2] != '::')
418
- cursor -= 1
419
- end
420
- end
421
- cursor + 1
422
- end
423
-
424
275
  # @return Solargraph::Source::Range
425
- def word_range_at index, whole
426
- cursor = beginning_of_word_at(index)
427
- start_offset = cursor
428
- start_offset -= 1 if (start_offset > 1 and @code[start_offset - 1] == ':') and (start_offset == 1 or @code[start_offset - 2] != ':')
429
- cursor = index
430
- if whole
431
- while cursor < @code.length
432
- char = @code[cursor, 1]
433
- break if char.nil? or char == ''
434
- break unless char.match(/[a-z0-9_\?\!]/i)
435
- cursor += 1
436
- end
437
- end
438
- end_offset = cursor
439
- end_offset = start_offset if end_offset < start_offset
440
- start_pos = get_position_at(start_offset)
441
- end_pos = get_position_at(end_offset)
442
- Solargraph::Source::Range.from_to(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
443
- end
444
-
445
- # @return [String]
446
- def remainder_at index
447
- cursor = index
448
- while cursor < @code.length
449
- char = @code[cursor, 1]
450
- break if char.nil? or char == ''
451
- break unless char.match(/[a-z0-9_\?\!]/i)
452
- cursor += 1
453
- end
454
- @code[index..cursor-1].to_s
276
+ def word_range_at first, last
277
+ s = Position.from_offset(@source.code, first)
278
+ e = Position.from_offset(@source.code, last)
279
+ Solargraph::Source::Range.new(s, e)
455
280
  end
456
281
 
457
282
  def signature_position
@@ -475,12 +300,42 @@ module Solargraph
475
300
  @signature_position
476
301
  end
477
302
 
478
- # Range tests that depend on positions identified from parsed code, such
479
- # as comment ranges, need to normalize EOLs to \n.
303
+ def generate_chain
304
+ CallChainer.chain(source, line, column)
305
+ end
306
+
307
+ def start_word_pattern
308
+ /(@{1,2}|\$)?([a-z0-9_]|[^\u0000-\u007F])*\z/i
309
+ end
310
+
311
+ def end_word_pattern
312
+ /^([a-z0-9_]|[^\u0000-\u007F])*[\?\!]?/i
313
+ end
314
+
315
+ # @param fragment [Source::Fragment]
316
+ # @param result [Array<Pin::Base>]
317
+ # @return [Completion]
318
+ def package_completions result
319
+ frag_start = word.to_s.downcase
320
+ filtered = result.uniq(&:identifier).select{|s| s.name.downcase.start_with?(frag_start) and (s.kind != Pin::METHOD or s.name.match(/^[a-z0-9_]+(\!|\?|=)?$/i))}.sort_by.with_index{ |x, idx| [x.name, idx] }
321
+ Completion.new(filtered, word_range)
322
+ end
323
+
324
+ # Sort an array of pins to put nil or undefined variables last.
480
325
  #
481
- # @return [String]
482
- def source_from_parser
483
- @source_from_parser ||= @source.code.gsub(/\r\n/, "\n")
326
+ # @param pins [Array<Solargraph::Pin::Base>]
327
+ # @return [Array<Solargraph::Pin::Base>]
328
+ def prefer_non_nil_variables pins
329
+ result = []
330
+ nil_pins = []
331
+ pins.each do |pin|
332
+ if pin.variable? and pin.nil_assignment?
333
+ nil_pins.push pin
334
+ else
335
+ result.push pin
336
+ end
337
+ end
338
+ result + nil_pins
484
339
  end
485
340
  end
486
341
  end