solargraph 0.25.1 → 0.26.0

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