theme-check 0.7.2 → 0.8.3
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/.github/workflows/theme-check.yml +1 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +33 -0
- data/RELEASING.md +5 -3
- data/config/default.yml +1 -1
- data/data/shopify_liquid/tags.yml +3 -0
- data/data/shopify_translation_keys.yml +1 -0
- data/dev.yml +1 -1
- data/docs/checks/nested_snippet.md +1 -1
- data/docs/checks/space_inside_braces.md +28 -0
- data/exe/theme-check +1 -1
- data/lib/theme_check.rb +5 -0
- data/lib/theme_check/analyzer.rb +19 -9
- data/lib/theme_check/bug.rb +20 -0
- data/lib/theme_check/check.rb +5 -1
- data/lib/theme_check/checks.rb +39 -8
- data/lib/theme_check/checks/missing_enable_comment.rb +4 -4
- data/lib/theme_check/checks/nested_snippet.rb +1 -1
- data/lib/theme_check/checks/space_inside_braces.rb +8 -2
- data/lib/theme_check/cli.rb +99 -64
- data/lib/theme_check/config.rb +6 -2
- data/lib/theme_check/disabled_check.rb +39 -0
- data/lib/theme_check/disabled_checks.rb +20 -32
- data/lib/theme_check/exceptions.rb +32 -0
- data/lib/theme_check/json_file.rb +5 -1
- data/lib/theme_check/language_server.rb +1 -1
- data/lib/theme_check/language_server/completion_engine.rb +1 -1
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
- data/lib/theme_check/language_server/constants.rb +5 -1
- data/lib/theme_check/language_server/document_link_engine.rb +2 -2
- data/lib/theme_check/language_server/handler.rb +32 -24
- data/lib/theme_check/language_server/variable_lookup_finder.rb +295 -0
- data/lib/theme_check/node.rb +12 -0
- data/lib/theme_check/offense.rb +14 -48
- data/lib/theme_check/parsing_helpers.rb +1 -1
- data/lib/theme_check/position.rb +77 -0
- data/lib/theme_check/position_helper.rb +37 -0
- data/lib/theme_check/remote_asset_file.rb +3 -0
- data/lib/theme_check/shopify_liquid/tag.rb +13 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +9 -10
- data/theme-check.gemspec +2 -0
- metadata +10 -5
- data/lib/theme_check/language_server/position_helper.rb +0 -27
data/lib/theme_check/config.rb
CHANGED
@@ -91,7 +91,11 @@ module ThemeCheck
|
|
91
91
|
|
92
92
|
options_for_check = options.transform_keys(&:to_sym)
|
93
93
|
options_for_check.delete(:enabled)
|
94
|
-
check =
|
94
|
+
check = if options_for_check.empty?
|
95
|
+
check_class.new
|
96
|
+
else
|
97
|
+
check_class.new(**options_for_check)
|
98
|
+
end
|
95
99
|
check.options = options_for_check
|
96
100
|
check
|
97
101
|
end.compact
|
@@ -104,7 +108,7 @@ module ThemeCheck
|
|
104
108
|
private
|
105
109
|
|
106
110
|
def check_name?(name)
|
107
|
-
name.start_with?(/[A-Z]/)
|
111
|
+
name.to_s.start_with?(/[A-Z]/)
|
108
112
|
end
|
109
113
|
|
110
114
|
def validate_configuration(configuration, default_configuration = self.class.default, parent_keys = [])
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class keeps track of checks being turned on and off in ranges.
|
4
|
+
# We'll use the node position to figure out if the test is disabled or not.
|
5
|
+
module ThemeCheck
|
6
|
+
class DisabledCheck
|
7
|
+
attr_reader :name, :ranges
|
8
|
+
attr_accessor :first_line
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
@ranges = []
|
13
|
+
@first_line = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_index=(index)
|
17
|
+
return unless ranges.empty? || !last.end.nil?
|
18
|
+
@ranges << (index..)
|
19
|
+
end
|
20
|
+
|
21
|
+
def end_index=(index)
|
22
|
+
return if ranges.empty? || !last.end.nil?
|
23
|
+
@ranges << (@ranges.pop.begin..index)
|
24
|
+
end
|
25
|
+
|
26
|
+
def disabled?(index)
|
27
|
+
ranges.any? { |range| range.cover?(index) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def last
|
31
|
+
ranges.last
|
32
|
+
end
|
33
|
+
|
34
|
+
def missing_end_index?
|
35
|
+
return false if first_line && ranges.size == 1
|
36
|
+
last.end.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -8,50 +8,36 @@ module ThemeCheck
|
|
8
8
|
|
9
9
|
ACTION_DISABLE_CHECKS = :disable
|
10
10
|
ACTION_ENABLE_CHECKS = :enable
|
11
|
-
ACTION_UNRELATED_COMMENT = :unrelated
|
12
11
|
|
13
12
|
def initialize
|
14
|
-
@
|
15
|
-
@all_disabled = false
|
16
|
-
@full_document_disabled = false
|
13
|
+
@disabled_checks = {}
|
17
14
|
end
|
18
15
|
|
19
16
|
def update(node)
|
20
17
|
text = comment_text(node)
|
21
|
-
|
22
18
|
if start_disabling?(text)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@full_document_disabled = true
|
19
|
+
checks_from_text(text).each do |check_name|
|
20
|
+
@disabled_checks[check_name] ||= DisabledCheck.new(check_name)
|
21
|
+
@disabled_checks[check_name].start_index = node.start_index
|
22
|
+
@disabled_checks[check_name].first_line = true if node.line_number == 1
|
28
23
|
end
|
29
24
|
elsif stop_disabling?(text)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
checks_from_text(text).each do |check_name|
|
26
|
+
next unless @disabled_checks.key?(check_name)
|
27
|
+
@disabled_checks[check_name].end_index = node.end_index
|
28
|
+
end
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
# Whether all checks should be disabled
|
43
|
-
def all_disabled?
|
44
|
-
@all_disabled
|
45
|
-
end
|
46
|
-
|
47
|
-
# Get a list of all the individual disabled checks
|
48
|
-
def all
|
49
|
-
@disabled
|
32
|
+
def disabled?(key, index)
|
33
|
+
@disabled_checks[:all]&.disabled?(index) ||
|
34
|
+
@disabled_checks[key]&.disabled?(index)
|
50
35
|
end
|
51
36
|
|
52
|
-
|
53
|
-
|
54
|
-
|
37
|
+
def checks_missing_end_index
|
38
|
+
@disabled_checks.values
|
39
|
+
.select(&:missing_end_index?)
|
40
|
+
.map(&:name)
|
55
41
|
end
|
56
42
|
|
57
43
|
private
|
@@ -69,9 +55,11 @@ module ThemeCheck
|
|
69
55
|
end
|
70
56
|
|
71
57
|
# Return a list of checks from a theme-check-disable comment
|
72
|
-
# Returns [] if all checks are meant to be disabled
|
58
|
+
# Returns [:all] if all checks are meant to be disabled
|
73
59
|
def checks_from_text(text)
|
74
|
-
text.gsub(DISABLE_PREFIX_PATTERN, '').strip.split(',').map(&:strip)
|
60
|
+
checks = text.gsub(DISABLE_PREFIX_PATTERN, '').strip.split(',').map(&:strip)
|
61
|
+
return [:all] if checks.empty?
|
62
|
+
checks
|
75
63
|
end
|
76
64
|
end
|
77
65
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "net/http"
|
3
|
+
|
4
|
+
TIMEOUT_EXCEPTIONS = [
|
5
|
+
Net::ReadTimeout,
|
6
|
+
Net::OpenTimeout,
|
7
|
+
Net::WriteTimeout,
|
8
|
+
Errno::ETIMEDOUT,
|
9
|
+
Timeout::Error,
|
10
|
+
]
|
11
|
+
|
12
|
+
CONNECTION_EXCEPTIONS = [
|
13
|
+
IOError,
|
14
|
+
EOFError,
|
15
|
+
SocketError,
|
16
|
+
Errno::EINVAL,
|
17
|
+
Errno::ECONNRESET,
|
18
|
+
Errno::ECONNABORTED,
|
19
|
+
Errno::EPIPE,
|
20
|
+
Errno::ECONNREFUSED,
|
21
|
+
Errno::EAGAIN,
|
22
|
+
Errno::EHOSTUNREACH,
|
23
|
+
Errno::ENETUNREACH,
|
24
|
+
]
|
25
|
+
|
26
|
+
NET_HTTP_EXCEPTIONS = [
|
27
|
+
Net::HTTPBadResponse,
|
28
|
+
Net::HTTPHeaderSyntaxError,
|
29
|
+
Net::ProtocolError,
|
30
|
+
*TIMEOUT_EXCEPTIONS,
|
31
|
+
*CONNECTION_EXCEPTIONS,
|
32
|
+
]
|
@@ -20,6 +20,10 @@ module ThemeCheck
|
|
20
20
|
@relative_pathname ||= Pathname.new(@relative_path)
|
21
21
|
end
|
22
22
|
|
23
|
+
def source
|
24
|
+
@source ||= @storage.read(@relative_path)
|
25
|
+
end
|
26
|
+
|
23
27
|
def content
|
24
28
|
load!
|
25
29
|
@content
|
@@ -39,7 +43,7 @@ module ThemeCheck
|
|
39
43
|
def load!
|
40
44
|
return if @loaded
|
41
45
|
|
42
|
-
@content = JSON.parse(
|
46
|
+
@content = JSON.parse(source)
|
43
47
|
rescue JSON::ParserError => e
|
44
48
|
@parser_error = e
|
45
49
|
ensure
|
@@ -4,7 +4,7 @@ require_relative "language_server/constants"
|
|
4
4
|
require_relative "language_server/handler"
|
5
5
|
require_relative "language_server/server"
|
6
6
|
require_relative "language_server/tokens"
|
7
|
-
require_relative "language_server/
|
7
|
+
require_relative "language_server/variable_lookup_finder"
|
8
8
|
require_relative "language_server/completion_helper"
|
9
9
|
require_relative "language_server/completion_provider"
|
10
10
|
require_relative "language_server/completion_engine"
|
@@ -12,7 +12,7 @@ module ThemeCheck
|
|
12
12
|
|
13
13
|
def completions(relative_path, line, col)
|
14
14
|
buffer = @storage.read(relative_path)
|
15
|
-
cursor =
|
15
|
+
cursor = from_row_column_to_index(buffer, line, col)
|
16
16
|
token = find_token(buffer, cursor)
|
17
17
|
return [] if token.nil?
|
18
18
|
|
@@ -4,18 +4,20 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
class ObjectCompletionProvider < CompletionProvider
|
6
6
|
def completions(content, cursor)
|
7
|
-
return [] unless
|
8
|
-
|
7
|
+
return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
|
8
|
+
return [] unless variable_lookup.lookups.empty?
|
9
|
+
return [] if content[cursor - 1] == "."
|
9
10
|
ShopifyLiquid::Object.labels
|
10
|
-
.select { |w| w.start_with?(partial) }
|
11
|
+
.select { |w| w.start_with?(partial(variable_lookup)) }
|
11
12
|
.map { |object| object_to_completion(object) }
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def variable_lookup_at_cursor(content, cursor)
|
16
|
+
VariableLookupFinder.lookup(content, cursor)
|
17
|
+
end
|
18
|
+
|
19
|
+
def partial(variable_lookup)
|
20
|
+
variable_lookup.name || ''
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
@@ -4,7 +4,11 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
PARTIAL_RENDER = %r{
|
6
6
|
\{\%-?\s*render\s+'(?<partial>[^']*)'|
|
7
|
-
\{\%-?\s*render\s+"(?<partial>[^"]*)"
|
7
|
+
\{\%-?\s*render\s+"(?<partial>[^"]*)"|
|
8
|
+
|
9
|
+
# in liquid tags the whole line is white space until render
|
10
|
+
^\s*render\s+'(?<partial>[^']*)'|
|
11
|
+
^\s*render\s+"(?<partial>[^"]*)"
|
8
12
|
}mix
|
9
13
|
end
|
10
14
|
end
|
@@ -14,12 +14,12 @@ module ThemeCheck
|
|
14
14
|
buffer = @storage.read(relative_path)
|
15
15
|
return [] unless buffer
|
16
16
|
matches(buffer, PARTIAL_RENDER).map do |match|
|
17
|
-
start_line, start_character =
|
17
|
+
start_line, start_character = from_index_to_row_column(
|
18
18
|
buffer,
|
19
19
|
match.begin(:partial),
|
20
20
|
)
|
21
21
|
|
22
|
-
end_line, end_character =
|
22
|
+
end_line, end_character = from_index_to_row_column(
|
23
23
|
buffer,
|
24
24
|
match.end(:partial)
|
25
25
|
)
|
@@ -23,17 +23,17 @@ module ThemeCheck
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def on_initialize(id, params)
|
26
|
-
@root_path = params["rootPath"]
|
26
|
+
@root_path = path_from_uri(params["rootUri"]) || params["rootPath"]
|
27
|
+
|
28
|
+
# Tell the client we don't support anything if there's no rootPath
|
29
|
+
return send_response(id, { capabilities: {} }) if @root_path.nil?
|
27
30
|
@storage = in_memory_storage(@root_path)
|
28
31
|
@completion_engine = CompletionEngine.new(@storage)
|
29
32
|
@document_link_engine = DocumentLinkEngine.new(@storage)
|
30
33
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
|
31
|
-
send_response(
|
32
|
-
|
33
|
-
|
34
|
-
capabilities: CAPABILITIES,
|
35
|
-
}
|
36
|
-
)
|
34
|
+
send_response(id, {
|
35
|
+
capabilities: CAPABILITIES,
|
36
|
+
})
|
37
37
|
end
|
38
38
|
|
39
39
|
def on_exit(_id, _params)
|
@@ -63,20 +63,14 @@ module ThemeCheck
|
|
63
63
|
|
64
64
|
def on_text_document_document_link(id, params)
|
65
65
|
relative_path = relative_path_from_text_document_uri(params)
|
66
|
-
send_response(
|
67
|
-
id: id,
|
68
|
-
result: document_links(relative_path)
|
69
|
-
)
|
66
|
+
send_response(id, document_links(relative_path))
|
70
67
|
end
|
71
68
|
|
72
69
|
def on_text_document_completion(id, params)
|
73
70
|
relative_path = relative_path_from_text_document_uri(params)
|
74
71
|
line = params.dig('position', 'line')
|
75
72
|
col = params.dig('position', 'character')
|
76
|
-
send_response(
|
77
|
-
id: id,
|
78
|
-
result: completions(relative_path, line, col)
|
79
|
-
)
|
73
|
+
send_response(id, completions(relative_path, line, col))
|
80
74
|
end
|
81
75
|
|
82
76
|
private
|
@@ -99,7 +93,11 @@ module ThemeCheck
|
|
99
93
|
end
|
100
94
|
|
101
95
|
def text_document_uri(params)
|
102
|
-
params.dig('textDocument', 'uri')
|
96
|
+
path_from_uri(params.dig('textDocument', 'uri'))
|
97
|
+
end
|
98
|
+
|
99
|
+
def path_from_uri(uri)
|
100
|
+
uri&.sub('file://', '')
|
103
101
|
end
|
104
102
|
|
105
103
|
def relative_path_from_text_document_uri(params)
|
@@ -167,13 +165,10 @@ module ThemeCheck
|
|
167
165
|
|
168
166
|
def send_diagnostic(path, offenses)
|
169
167
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
175
|
-
},
|
176
|
-
)
|
168
|
+
send_notification('textDocument/publishDiagnostics', {
|
169
|
+
uri: "file://#{path}",
|
170
|
+
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
171
|
+
})
|
177
172
|
end
|
178
173
|
|
179
174
|
def offense_to_diagnostic(offense)
|
@@ -220,11 +215,24 @@ module ThemeCheck
|
|
220
215
|
}
|
221
216
|
end
|
222
217
|
|
223
|
-
def
|
218
|
+
def send_message(message)
|
224
219
|
message[:jsonrpc] = '2.0'
|
225
220
|
@server.send_response(message)
|
226
221
|
end
|
227
222
|
|
223
|
+
def send_response(id, result = nil, error = nil)
|
224
|
+
message = { id: id }
|
225
|
+
message[:result] = result if result
|
226
|
+
message[:error] = error if error
|
227
|
+
send_message(message)
|
228
|
+
end
|
229
|
+
|
230
|
+
def send_notification(method, params)
|
231
|
+
message = { method: method }
|
232
|
+
message[:params] = params
|
233
|
+
send_message(message)
|
234
|
+
end
|
235
|
+
|
228
236
|
def log(message)
|
229
237
|
@server.log(message)
|
230
238
|
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
module VariableLookupFinder
|
6
|
+
extend self
|
7
|
+
|
8
|
+
UNCLOSED_SQUARE_BRACKET = /\[[^\]]*\Z/
|
9
|
+
ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED = %r{
|
10
|
+
(
|
11
|
+
# quotes not preceded by a [
|
12
|
+
(?<!\[)['"]|
|
13
|
+
# closing ]
|
14
|
+
\]|
|
15
|
+
# opening [
|
16
|
+
\[
|
17
|
+
)$
|
18
|
+
}x
|
19
|
+
|
20
|
+
VARIABLE_LOOKUP_CHARACTERS = /[a-z0-9_.'"\]\[]/i
|
21
|
+
VARIABLE_LOOKUP = /#{VARIABLE_LOOKUP_CHARACTERS}+/o
|
22
|
+
SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS = %r{
|
23
|
+
(?:
|
24
|
+
\s(?:
|
25
|
+
if|elsif|unless|and|or|#{Liquid::Condition.operators.keys.join("|")}
|
26
|
+
|echo
|
27
|
+
|case|when
|
28
|
+
|cycle
|
29
|
+
|in
|
30
|
+
)
|
31
|
+
|[:,=]
|
32
|
+
)
|
33
|
+
\s+
|
34
|
+
}omix
|
35
|
+
ENDS_WITH_BLANK_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}$/oimx
|
36
|
+
ENDS_WITH_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}#{VARIABLE_LOOKUP}$/oimx
|
37
|
+
|
38
|
+
def lookup(content, cursor)
|
39
|
+
return if cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
|
40
|
+
potential_lookup = lookup_liquid_variable(content, cursor) || lookup_liquid_tag(content, cursor)
|
41
|
+
|
42
|
+
# And we only return it if it's parsed by Liquid as VariableLookup
|
43
|
+
return unless potential_lookup.is_a?(Liquid::VariableLookup)
|
44
|
+
potential_lookup
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
|
50
|
+
content[0..cursor - 1] =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED
|
51
|
+
end
|
52
|
+
|
53
|
+
def cursor_is_on_liquid_variable_lookup_position(content, cursor)
|
54
|
+
previous_char = content[cursor - 1]
|
55
|
+
is_liquid_variable = content =~ Liquid::VariableStart
|
56
|
+
is_in_variable_segment = previous_char =~ VARIABLE_LOOKUP_CHARACTERS
|
57
|
+
is_on_blank_variable_lookup_position = content[0..cursor - 1] =~ /[{:,-]\s+$/
|
58
|
+
(
|
59
|
+
is_liquid_variable && (
|
60
|
+
is_in_variable_segment ||
|
61
|
+
is_on_blank_variable_lookup_position
|
62
|
+
)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def lookup_liquid_variable(content, cursor)
|
67
|
+
return unless cursor_is_on_liquid_variable_lookup_position(content, cursor)
|
68
|
+
start_index = content.match(/#{Liquid::VariableStart}-?/o).end(0) + 1
|
69
|
+
end_index = cursor - 1
|
70
|
+
|
71
|
+
# We take the following content
|
72
|
+
# - start after the first two {{
|
73
|
+
# - end at cursor position
|
74
|
+
#
|
75
|
+
# That way, we'll have a partial liquid variable that
|
76
|
+
# can be parsed such that the "last" variable_lookup
|
77
|
+
# will be the one we're trying to complete.
|
78
|
+
markup = content[start_index..end_index]
|
79
|
+
|
80
|
+
# Early return for incomplete variables
|
81
|
+
return empty_lookup if markup =~ /\s+$/
|
82
|
+
|
83
|
+
# Now we go to hack city... The cursor might be in the middle
|
84
|
+
# of a string/square bracket lookup. We need to close those
|
85
|
+
# otherwise the variable parse won't work.
|
86
|
+
markup += "'" if markup.count("'").odd?
|
87
|
+
markup += '"' if markup.count('"').odd?
|
88
|
+
markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
|
89
|
+
|
90
|
+
variable = variable_from_markup(markup)
|
91
|
+
|
92
|
+
variable_lookup_for_liquid_variable(variable)
|
93
|
+
end
|
94
|
+
|
95
|
+
def cursor_is_on_liquid_tag_lookup_position(content, cursor)
|
96
|
+
markup = content[0..cursor - 1]
|
97
|
+
is_liquid_tag = content.match?(Liquid::TagStart)
|
98
|
+
is_in_variable_segment = markup =~ ENDS_WITH_POTENTIAL_LOOKUP
|
99
|
+
is_on_blank_variable_lookup_position = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
|
100
|
+
(
|
101
|
+
is_liquid_tag && (
|
102
|
+
is_in_variable_segment ||
|
103
|
+
is_on_blank_variable_lookup_position
|
104
|
+
)
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Context:
|
109
|
+
#
|
110
|
+
# We know full well that the code as it is being typed is probably not
|
111
|
+
# something that can be parsed by liquid.
|
112
|
+
#
|
113
|
+
# How this works:
|
114
|
+
#
|
115
|
+
# 1. Attempt to turn the code of the token until the cursor position into
|
116
|
+
# valid liquid code with some hacks.
|
117
|
+
# 2. If the code ends in space at a "potential lookup" spot
|
118
|
+
# a. Then return an empty variable lookup
|
119
|
+
# 3. Parse the valid liquid code
|
120
|
+
# 4. Attempt to extract a VariableLookup from Liquid::Template
|
121
|
+
def lookup_liquid_tag(content, cursor)
|
122
|
+
return unless cursor_is_on_liquid_tag_lookup_position(content, cursor)
|
123
|
+
|
124
|
+
markup = parseable_markup(content, cursor)
|
125
|
+
return empty_lookup if markup == :empty_lookup_markup
|
126
|
+
|
127
|
+
template = Liquid::Template.parse(markup)
|
128
|
+
current_tag = template.root.nodelist[0]
|
129
|
+
|
130
|
+
case current_tag.tag_name
|
131
|
+
when "if", "unless"
|
132
|
+
variable_lookup_for_if_tag(current_tag)
|
133
|
+
when "case"
|
134
|
+
variable_lookup_for_case_tag(current_tag)
|
135
|
+
when "cycle"
|
136
|
+
variable_lookup_for_cycle_tag(current_tag)
|
137
|
+
when "for"
|
138
|
+
variable_lookup_for_for_tag(current_tag)
|
139
|
+
when "tablerow"
|
140
|
+
variable_lookup_for_tablerow_tag(current_tag)
|
141
|
+
when "render"
|
142
|
+
variable_lookup_for_render_tag(current_tag)
|
143
|
+
when "assign"
|
144
|
+
variable_lookup_for_assign_tag(current_tag)
|
145
|
+
when "echo"
|
146
|
+
variable_lookup_for_echo_tag(current_tag)
|
147
|
+
end
|
148
|
+
|
149
|
+
# rubocop:disable Style/RedundantReturn
|
150
|
+
rescue Liquid::SyntaxError
|
151
|
+
# We don't complete variable for liquid syntax errors
|
152
|
+
return
|
153
|
+
end
|
154
|
+
# rubocop:enable Style/RedundantReturn
|
155
|
+
|
156
|
+
def parseable_markup(content, cursor)
|
157
|
+
start_index = 0
|
158
|
+
end_index = cursor - 1
|
159
|
+
markup = content[start_index..end_index]
|
160
|
+
|
161
|
+
# Welcome to Hackcity
|
162
|
+
markup += "'" if markup.count("'").odd?
|
163
|
+
markup += '"' if markup.count('"').odd?
|
164
|
+
markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
|
165
|
+
|
166
|
+
# Now check if it's a liquid tag
|
167
|
+
is_liquid_tag = markup =~ tag_regex('liquid')
|
168
|
+
ends_with_blank_potential_lookup = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
|
169
|
+
last_line = markup.rstrip.lines.last
|
170
|
+
markup = "{% #{last_line}" if is_liquid_tag
|
171
|
+
|
172
|
+
# Close the tag
|
173
|
+
markup += ' %}'
|
174
|
+
|
175
|
+
# if statements
|
176
|
+
is_if_tag = markup =~ tag_regex('if')
|
177
|
+
return :empty_lookup_markup if is_if_tag && ends_with_blank_potential_lookup
|
178
|
+
markup += '{% endif %}' if is_if_tag
|
179
|
+
|
180
|
+
# unless statements
|
181
|
+
is_unless_tag = markup =~ tag_regex('unless')
|
182
|
+
return :empty_lookup_markup if is_unless_tag && ends_with_blank_potential_lookup
|
183
|
+
markup += '{% endunless %}' if is_unless_tag
|
184
|
+
|
185
|
+
# elsif statements
|
186
|
+
is_elsif_tag = markup =~ tag_regex('elsif')
|
187
|
+
return :empty_lookup_markup if is_elsif_tag && ends_with_blank_potential_lookup
|
188
|
+
markup = '{% if x %}' + markup + '{% endif %}' if is_elsif_tag
|
189
|
+
|
190
|
+
# case statements
|
191
|
+
is_case_tag = markup =~ tag_regex('case')
|
192
|
+
return :empty_lookup_markup if is_case_tag && ends_with_blank_potential_lookup
|
193
|
+
markup += "{% endcase %}" if is_case_tag
|
194
|
+
|
195
|
+
# when
|
196
|
+
is_when_tag = markup =~ tag_regex('when')
|
197
|
+
return :empty_lookup_markup if is_when_tag && ends_with_blank_potential_lookup
|
198
|
+
markup = "{% case x %}" + markup + "{% endcase %}" if is_when_tag
|
199
|
+
|
200
|
+
# for statements
|
201
|
+
is_for_tag = markup =~ tag_regex('for')
|
202
|
+
return :empty_lookup_markup if is_for_tag && ends_with_blank_potential_lookup
|
203
|
+
markup += "{% endfor %}" if is_for_tag
|
204
|
+
|
205
|
+
# tablerow statements
|
206
|
+
is_tablerow_tag = markup =~ tag_regex('tablerow')
|
207
|
+
return :empty_lookup_markup if is_tablerow_tag && ends_with_blank_potential_lookup
|
208
|
+
markup += "{% endtablerow %}" if is_tablerow_tag
|
209
|
+
|
210
|
+
markup
|
211
|
+
end
|
212
|
+
|
213
|
+
def variable_lookup_for_if_tag(if_tag)
|
214
|
+
condition = if_tag.blocks.last
|
215
|
+
variable_lookup_for_condition(condition)
|
216
|
+
end
|
217
|
+
|
218
|
+
def variable_lookup_for_condition(condition)
|
219
|
+
return variable_lookup_for_condition(condition.child_condition) if condition.child_condition
|
220
|
+
return condition.right if condition.right
|
221
|
+
condition.left
|
222
|
+
end
|
223
|
+
|
224
|
+
def variable_lookup_for_case_tag(case_tag)
|
225
|
+
return variable_lookup_for_case_block(case_tag.blocks.last) unless case_tag.blocks.empty?
|
226
|
+
case_tag.left
|
227
|
+
end
|
228
|
+
|
229
|
+
def variable_lookup_for_case_block(condition)
|
230
|
+
condition.right
|
231
|
+
end
|
232
|
+
|
233
|
+
def variable_lookup_for_cycle_tag(cycle_tag)
|
234
|
+
cycle_tag.variables.last
|
235
|
+
end
|
236
|
+
|
237
|
+
def variable_lookup_for_for_tag(for_tag)
|
238
|
+
for_tag.collection_name
|
239
|
+
end
|
240
|
+
|
241
|
+
def variable_lookup_for_tablerow_tag(tablerow_tag)
|
242
|
+
tablerow_tag.collection_name
|
243
|
+
end
|
244
|
+
|
245
|
+
def variable_lookup_for_render_tag(render_tag)
|
246
|
+
return empty_lookup if render_tag.raw =~ /:\s*$/
|
247
|
+
render_tag.attributes.values.last
|
248
|
+
end
|
249
|
+
|
250
|
+
def variable_lookup_for_assign_tag(assign_tag)
|
251
|
+
variable_lookup_for_liquid_variable(assign_tag.from)
|
252
|
+
end
|
253
|
+
|
254
|
+
def variable_lookup_for_echo_tag(echo_tag)
|
255
|
+
variable_lookup_for_liquid_variable(echo_tag.variable)
|
256
|
+
end
|
257
|
+
|
258
|
+
def variable_lookup_for_liquid_variable(variable)
|
259
|
+
has_filters = !variable.filters.empty?
|
260
|
+
|
261
|
+
# Can complete after trailing comma or :
|
262
|
+
if has_filters && variable.raw =~ /[:,]\s*$/
|
263
|
+
empty_lookup
|
264
|
+
elsif has_filters
|
265
|
+
last_filter_argument(variable.filters)
|
266
|
+
elsif variable.name.nil?
|
267
|
+
empty_lookup
|
268
|
+
else
|
269
|
+
variable.name
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def empty_lookup
|
274
|
+
Liquid::VariableLookup.parse('')
|
275
|
+
end
|
276
|
+
|
277
|
+
# We want the last thing in variable.filters which is at most
|
278
|
+
# an array that looks like [name, positional_args, hash_arg]
|
279
|
+
def last_filter_argument(filters)
|
280
|
+
filter = filters.last
|
281
|
+
return filter[2].values.last if filter.size == 3
|
282
|
+
return filter[1].last if filter.size == 2
|
283
|
+
nil
|
284
|
+
end
|
285
|
+
|
286
|
+
def variable_from_markup(markup, parse_context = Liquid::ParseContext.new)
|
287
|
+
Liquid::Variable.new(markup, parse_context)
|
288
|
+
end
|
289
|
+
|
290
|
+
def tag_regex(tag)
|
291
|
+
ShopifyLiquid::Tag.tag_regex(tag)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|