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.
- 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
|