solargraph 0.25.1 → 0.26.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/solargraph.rb +18 -16
- data/lib/solargraph/api_map.rb +100 -161
- data/lib/solargraph/api_map/source_to_yard.rb +9 -9
- data/lib/solargraph/api_map/store.rb +50 -13
- data/lib/solargraph/basic_type.rb +33 -0
- data/lib/solargraph/basic_type_methods.rb +111 -0
- data/lib/solargraph/complex_type.rb +51 -89
- data/lib/solargraph/core_fills.rb +12 -8
- data/lib/solargraph/diagnostics/type_not_defined.rb +2 -2
- data/lib/solargraph/language_server.rb +3 -0
- data/lib/solargraph/language_server/completion_item_kinds.rb +2 -0
- data/lib/solargraph/language_server/error_codes.rb +2 -0
- data/lib/solargraph/language_server/host.rb +53 -6
- data/lib/solargraph/language_server/message.rb +13 -0
- data/lib/solargraph/language_server/message/text_document/definition.rb +4 -6
- data/lib/solargraph/language_server/message/text_document/document_symbol.rb +2 -1
- data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +2 -1
- data/lib/solargraph/language_server/message_types.rb +2 -0
- data/lib/solargraph/language_server/request.rb +4 -0
- data/lib/solargraph/language_server/symbol_kinds.rb +28 -26
- data/lib/solargraph/language_server/transport.rb +3 -0
- data/lib/solargraph/language_server/uri_helpers.rb +2 -0
- data/lib/solargraph/library.rb +12 -7
- data/lib/solargraph/pin.rb +1 -1
- data/lib/solargraph/pin/attribute.rb +5 -5
- data/lib/solargraph/pin/base.rb +51 -16
- data/lib/solargraph/pin/base_variable.rb +25 -7
- data/lib/solargraph/pin/block.rb +18 -1
- data/lib/solargraph/pin/block_parameter.rb +42 -5
- data/lib/solargraph/pin/conversions.rb +4 -2
- data/lib/solargraph/pin/method.rb +6 -6
- data/lib/solargraph/pin/method_parameter.rb +6 -6
- data/lib/solargraph/pin/namespace.rb +7 -2
- data/lib/solargraph/pin/proxy_type.rb +39 -0
- data/lib/solargraph/pin/symbol.rb +20 -12
- data/lib/solargraph/pin/yard_pin/method.rb +2 -2
- data/lib/solargraph/source.rb +89 -38
- data/lib/solargraph/source/call_chainer.rb +273 -0
- data/lib/solargraph/source/chain.rb +104 -0
- data/lib/solargraph/source/chain/call.rb +72 -0
- data/lib/solargraph/source/chain/class_variable.rb +11 -0
- data/lib/solargraph/source/chain/constant.rb +17 -0
- data/lib/solargraph/source/chain/definition.rb +16 -0
- data/lib/solargraph/source/chain/global_variable.rb +11 -0
- data/lib/solargraph/source/chain/head.rb +20 -0
- data/lib/solargraph/source/chain/instance_variable.rb +11 -0
- data/lib/solargraph/source/chain/link.rb +33 -0
- data/lib/solargraph/source/chain/literal.rb +21 -0
- data/lib/solargraph/source/chain/variable.rb +11 -0
- data/lib/solargraph/source/change.rb +3 -1
- data/lib/solargraph/{api_map → source}/completion.rb +3 -1
- data/lib/solargraph/source/encoding_fixes.rb +21 -0
- data/lib/solargraph/source/fragment.rb +139 -284
- data/lib/solargraph/source/mapper.rb +27 -16
- data/lib/solargraph/source/node_chainer.rb +94 -0
- data/lib/solargraph/source/node_methods.rb +2 -2
- data/lib/solargraph/source/position.rb +4 -0
- data/lib/solargraph/source/range.rb +10 -2
- data/lib/solargraph/version.rb +1 -1
- data/lib/solargraph/yard_map.rb +13 -2
- metadata +20 -6
- data/lib/solargraph/api_map/probe.rb +0 -251
- data/lib/solargraph/api_map/type_methods.rb +0 -40
- 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,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,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,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
|
@@ -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
|
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
|
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
|
-
|
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 [
|
90
|
+
# @return [Source::Chain]
|
130
91
|
def chain
|
131
|
-
@chain ||=
|
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
|
141
|
-
|
99
|
+
def whole_signature
|
100
|
+
chain.links.reject{|l| l.word == '<undefined>'}.map(&:word).join('.')
|
142
101
|
end
|
143
102
|
|
144
|
-
# Get the
|
145
|
-
#
|
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
|
149
|
-
@
|
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
|
-
|
153
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
170
|
-
|
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
|
-
|
178
|
-
|
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 ||=
|
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
|
212
|
-
@
|
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
|
426
|
-
|
427
|
-
|
428
|
-
|
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
|
-
|
479
|
-
|
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
|
-
# @
|
482
|
-
|
483
|
-
|
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
|