theme-check 1.10.3 → 1.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/theme-check.yml +1 -1
- data/.gitignore +3 -0
- data/CHANGELOG.md +48 -0
- data/CONTRIBUTING.md +82 -0
- data/README.md +11 -8
- data/Rakefile +7 -0
- data/TROUBLESHOOTING.md +65 -0
- data/config/default.yml +4 -0
- data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
- data/data/shopify_liquid/documentation/filters.json +5528 -0
- data/data/shopify_liquid/documentation/latest.json +1 -0
- data/data/shopify_liquid/documentation/objects.json +19272 -0
- data/data/shopify_liquid/documentation/tags.json +1252 -0
- data/data/shopify_liquid/filters.yml +18 -0
- data/dev.yml +1 -1
- data/docs/checks/asset_preload.md +60 -0
- data/docs/checks/asset_size_javascript.md +2 -2
- data/docs/checks/missing_enable_comment.md +3 -3
- data/docs/checks/nested_snippet.md +8 -8
- data/docs/checks/translation_key_exists.md +4 -4
- data/docs/checks/valid_html_translation.md +1 -1
- data/lib/theme_check/analyzer.rb +18 -3
- data/lib/theme_check/check.rb +6 -1
- data/lib/theme_check/checks/asset_preload.rb +20 -0
- data/lib/theme_check/checks/deprecated_filter.rb +29 -5
- data/lib/theme_check/checks/missing_enable_comment.rb +4 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +5 -1
- data/lib/theme_check/checks/missing_template.rb +5 -1
- data/lib/theme_check/checks/undefined_object.rb +4 -0
- data/lib/theme_check/checks/unused_assign.rb +6 -1
- data/lib/theme_check/checks/unused_snippet.rb +50 -2
- data/lib/theme_check/config.rb +2 -2
- data/lib/theme_check/disabled_checks.rb +11 -4
- data/lib/theme_check/file_system_storage.rb +2 -0
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/language_server/bridge.rb +31 -6
- data/lib/theme_check/language_server/completion_context.rb +52 -0
- data/lib/theme_check/language_server/completion_engine.rb +15 -21
- data/lib/theme_check/language_server/completion_provider.rb +16 -1
- data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
- data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
- data/lib/theme_check/language_server/diagnostics_engine.rb +80 -34
- data/lib/theme_check/language_server/diagnostics_manager.rb +27 -6
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +7 -6
- data/lib/theme_check/language_server/handler.rb +93 -9
- data/lib/theme_check/language_server/protocol.rb +9 -0
- data/lib/theme_check/language_server/server.rb +42 -14
- data/lib/theme_check/language_server/type_helper.rb +22 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
- data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
- data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
- data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
- data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
- data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
- data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +17 -2
- data/lib/theme_check/language_server.rb +12 -0
- data/lib/theme_check/liquid_file.rb +22 -1
- data/lib/theme_check/liquid_node.rb +33 -1
- data/lib/theme_check/liquid_visitor.rb +1 -1
- data/lib/theme_check/remote_asset_file.rb +13 -7
- data/lib/theme_check/schema_helper.rb +1 -1
- data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
- data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
- data/lib/theme_check/shopify_liquid/filter.rb +4 -0
- data/lib/theme_check/shopify_liquid/object.rb +4 -0
- data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
- data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
- data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
- data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
- data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
- data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
- data/lib/theme_check/shopify_liquid/tag.rb +4 -0
- data/lib/theme_check/shopify_liquid.rb +17 -1
- data/lib/theme_check/tags.rb +2 -1
- data/lib/theme_check/version.rb +1 -1
- data/shipit.rubygems.yml +3 -0
- data/theme-check.gemspec +5 -3
- metadata +45 -6
- data/.github/probots.yml +0 -3
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "timeout"
|
4
|
+
|
3
5
|
# This class exists as a bridge (or boundary) between our handlers and the outside world.
|
4
6
|
#
|
5
7
|
# It is concerned with all the Language Server Protocol constructs. i.e.
|
@@ -29,6 +31,9 @@ module ThemeCheck
|
|
29
31
|
|
30
32
|
# Whether the client supports work done progress notifications
|
31
33
|
@supports_work_done_progress = false
|
34
|
+
|
35
|
+
@work_done_progress_mutex = Mutex.new
|
36
|
+
@work_done_progress_token = 1
|
32
37
|
end
|
33
38
|
|
34
39
|
def log(message)
|
@@ -78,12 +83,17 @@ module ThemeCheck
|
|
78
83
|
|
79
84
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
|
80
85
|
def send_internal_error(id, e)
|
86
|
+
# For a reason I can't comprehend, sometimes
|
87
|
+
# e.full_message _hangs_ and brings your CPU to 100%.
|
88
|
+
# It's wrapped in here because it prints anyway...
|
89
|
+
# This shit is weird, yo.
|
90
|
+
Timeout.timeout(1) do
|
91
|
+
$stderr.puts e.full_message
|
92
|
+
end
|
93
|
+
ensure
|
81
94
|
send_response(id, nil, {
|
82
95
|
code: ErrorCodes::INTERNAL_ERROR,
|
83
|
-
message:
|
84
|
-
#{e.class}: #{e.message}
|
85
|
-
#{e.backtrace.join("\n ")}
|
86
|
-
EOS
|
96
|
+
message: "A theme-check-language-server has occured, inspect OUTPUT logs for details.",
|
87
97
|
})
|
88
98
|
end
|
89
99
|
|
@@ -103,15 +113,28 @@ module ThemeCheck
|
|
103
113
|
@supports_work_done_progress
|
104
114
|
end
|
105
115
|
|
106
|
-
def send_create_work_done_progress_request
|
107
|
-
|
116
|
+
def send_create_work_done_progress_request
|
117
|
+
# This isn't necessary, but it kind of is to make it obvious
|
118
|
+
# that this variable is not thread safe. Don't try to refactor
|
119
|
+
# this with @work_done_progress_token because you're going to
|
120
|
+
# have a hard time.
|
121
|
+
token = @work_done_progress_mutex.synchronize do
|
122
|
+
@work_done_progress_token += 1
|
123
|
+
end
|
124
|
+
|
125
|
+
return token unless supports_work_done_progress?
|
126
|
+
|
127
|
+
# We're going to wait for a response here...
|
108
128
|
send_request("window/workDoneProgress/create", {
|
109
129
|
token: token,
|
110
130
|
})
|
131
|
+
|
132
|
+
token
|
111
133
|
end
|
112
134
|
|
113
135
|
def send_work_done_progress_begin(token, title)
|
114
136
|
return unless supports_work_done_progress?
|
137
|
+
|
115
138
|
send_progress(token, {
|
116
139
|
kind: 'begin',
|
117
140
|
title: title,
|
@@ -122,6 +145,7 @@ module ThemeCheck
|
|
122
145
|
|
123
146
|
def send_work_done_progress_report(token, message, percentage)
|
124
147
|
return unless supports_work_done_progress?
|
148
|
+
|
125
149
|
send_progress(token, {
|
126
150
|
kind: 'report',
|
127
151
|
message: message,
|
@@ -132,6 +156,7 @@ module ThemeCheck
|
|
132
156
|
|
133
157
|
def send_work_done_progress_end(token, message)
|
134
158
|
return unless supports_work_done_progress?
|
159
|
+
|
135
160
|
send_progress(token, {
|
136
161
|
kind: 'end',
|
137
162
|
message: message,
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CompletionContext
|
6
|
+
include PositionHelper
|
7
|
+
|
8
|
+
attr_reader :storage, :relative_path, :line, :col
|
9
|
+
|
10
|
+
def initialize(storage, relative_path, line, col)
|
11
|
+
@storage = storage
|
12
|
+
@relative_path = relative_path
|
13
|
+
@line = line
|
14
|
+
@col = col
|
15
|
+
end
|
16
|
+
|
17
|
+
def buffer
|
18
|
+
@buffer ||= storage.read(relative_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def buffer_until_previous_row
|
22
|
+
@buffer_without_current_row ||= buffer[0..absolute_cursor].lines[0...-1].join
|
23
|
+
end
|
24
|
+
|
25
|
+
def absolute_cursor
|
26
|
+
@absolute_cursor ||= from_row_column_to_index(buffer, line, col)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cursor
|
30
|
+
@cursor ||= absolute_cursor - token&.start || 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def content
|
34
|
+
@content ||= token&.content
|
35
|
+
end
|
36
|
+
|
37
|
+
def token
|
38
|
+
@token ||= Tokens.new(buffer).find do |t|
|
39
|
+
# Here we include the next character and exclude the first
|
40
|
+
# one becase when we want to autocomplete inside a token
|
41
|
+
# and at most 1 outside it since the cursor could be placed
|
42
|
+
# at the end of the token.
|
43
|
+
t.start < absolute_cursor && absolute_cursor <= t.end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def clone_and_overwrite(col:)
|
48
|
+
CompletionContext.new(storage, relative_path, line, col)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -3,35 +3,29 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
module LanguageServer
|
5
5
|
class CompletionEngine
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(storage)
|
6
|
+
def initialize(storage, bridge = nil)
|
9
7
|
@storage = storage
|
8
|
+
@bridge = bridge
|
10
9
|
@providers = CompletionProvider.all.map { |x| x.new(storage) }
|
11
10
|
end
|
12
11
|
|
13
12
|
def completions(relative_path, line, col)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
context = context(relative_path, line, col)
|
14
|
+
|
15
|
+
@providers
|
16
|
+
.flat_map { |provider| provider.completions(context) }
|
17
|
+
.uniq { |completion_item| completion_item[:label] }
|
18
|
+
rescue StandardError => error
|
19
|
+
@bridge || raise(error)
|
20
|
+
|
21
|
+
message = error.message
|
22
|
+
backtrace = error.backtrace.join("\n")
|
18
23
|
|
19
|
-
@
|
20
|
-
p.completions(
|
21
|
-
token.content,
|
22
|
-
cursor - token.start
|
23
|
-
)
|
24
|
-
end
|
24
|
+
@bridge.log("[completion error] error: #{message}\n#{backtrace}")
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
|
29
|
-
# Here we include the next character and exclude the first
|
30
|
-
# one becase when we want to autocomplete inside a token
|
31
|
-
# and at most 1 outside it since the cursor could be placed
|
32
|
-
# at the end of the token.
|
33
|
-
token.start < cursor && cursor <= token.end
|
34
|
-
end
|
27
|
+
def context(relative_path, line, col)
|
28
|
+
CompletionContext.new(@storage, relative_path, line, col)
|
35
29
|
end
|
36
30
|
end
|
37
31
|
end
|
@@ -6,6 +6,10 @@ module ThemeCheck
|
|
6
6
|
include CompletionHelper
|
7
7
|
include RegexHelpers
|
8
8
|
|
9
|
+
attr_reader :storage
|
10
|
+
|
11
|
+
CurrentToken = Struct.new(:content, :cursor, :absolute_cursor, :buffer)
|
12
|
+
|
9
13
|
class << self
|
10
14
|
def all
|
11
15
|
@all ||= []
|
@@ -20,9 +24,20 @@ module ThemeCheck
|
|
20
24
|
@storage = storage
|
21
25
|
end
|
22
26
|
|
23
|
-
def completions(
|
27
|
+
def completions(relative_path, line, col)
|
24
28
|
raise NotImplementedError
|
25
29
|
end
|
30
|
+
|
31
|
+
def doc_hash(content)
|
32
|
+
return {} if content.nil? || content.empty?
|
33
|
+
|
34
|
+
{
|
35
|
+
documentation: {
|
36
|
+
kind: MarkupKinds::MARKDOWN,
|
37
|
+
value: content,
|
38
|
+
},
|
39
|
+
}
|
40
|
+
end
|
26
41
|
end
|
27
42
|
end
|
28
43
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class AssignmentsCompletionProvider < CompletionProvider
|
6
|
+
def completions(context)
|
7
|
+
content = context.buffer_until_previous_row
|
8
|
+
|
9
|
+
return [] if content.nil?
|
10
|
+
return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
|
11
|
+
return [] unless variable_lookup.lookups.empty?
|
12
|
+
return [] if context.content[context.cursor - 1] == "."
|
13
|
+
|
14
|
+
finder = VariableLookupFinder::AssignmentsFinder.new(content)
|
15
|
+
finder.find!
|
16
|
+
|
17
|
+
finder.assignments.map do |label, potential_lookup|
|
18
|
+
object, _property = VariableLookupTraverser.lookup_object_and_property(potential_lookup)
|
19
|
+
object_to_completion(label, object.name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def object_to_completion(label, object)
|
26
|
+
content = ShopifyLiquid::Documentation.object_doc(object)
|
27
|
+
|
28
|
+
{
|
29
|
+
label: label,
|
30
|
+
kind: CompletionItemKinds::VARIABLE,
|
31
|
+
**doc_hash(content),
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -4,11 +4,19 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
class FilterCompletionProvider < CompletionProvider
|
6
6
|
NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o
|
7
|
+
FILTER_SEPARATOR_INCLUDING_SPACES = /\s*#{Liquid::FilterSeparator}/
|
8
|
+
INPUT_TYPE_VARIABLE = 'variable'
|
7
9
|
|
8
|
-
def completions(
|
10
|
+
def completions(context)
|
11
|
+
content = context.content
|
12
|
+
cursor = context.cursor
|
13
|
+
|
14
|
+
return [] if content.nil?
|
9
15
|
return [] unless can_complete?(content, cursor)
|
10
|
-
|
11
|
-
|
16
|
+
|
17
|
+
context = context_with_cursor_before_potential_filter_separator(context)
|
18
|
+
available_filters_for(determine_input_type(context))
|
19
|
+
.select { |w| w.name.start_with?(partial(content, cursor)) }
|
12
20
|
.map { |filter| filter_to_completion(filter) }
|
13
21
|
end
|
14
22
|
|
@@ -21,12 +29,41 @@ module ThemeCheck
|
|
21
29
|
|
22
30
|
private
|
23
31
|
|
24
|
-
def
|
25
|
-
|
32
|
+
def context_with_cursor_before_potential_filter_separator(context)
|
33
|
+
content = context.content
|
34
|
+
diff = content.index(FILTER_SEPARATOR_INCLUDING_SPACES) - context.cursor
|
35
|
+
|
36
|
+
return context unless content.scan(FILTER_SEPARATOR_INCLUDING_SPACES).size == 1
|
37
|
+
|
38
|
+
context.clone_and_overwrite(col: context.col + diff)
|
39
|
+
end
|
40
|
+
|
41
|
+
def determine_input_type(context)
|
42
|
+
variable_lookup = VariableLookupFinder.lookup(context)
|
43
|
+
|
44
|
+
if variable_lookup
|
45
|
+
object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
|
46
|
+
return property.return_type if property
|
47
|
+
return object.name if object
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def available_filters_for(input_type)
|
52
|
+
filters = ShopifyLiquid::SourceIndex.filters
|
53
|
+
.select { |filter| input_type.nil? || filter.input_type == input_type }
|
54
|
+
return all_labels if filters.empty?
|
55
|
+
return filters if input_type == INPUT_TYPE_VARIABLE
|
56
|
+
|
57
|
+
filters + available_filters_for(INPUT_TYPE_VARIABLE)
|
58
|
+
end
|
59
|
+
|
60
|
+
def all_labels
|
61
|
+
available_filters_for(nil)
|
26
62
|
end
|
27
63
|
|
28
64
|
def cursor_on_filter?(content, cursor)
|
29
65
|
return false unless content.match?(NAMED_FILTER)
|
66
|
+
|
30
67
|
matches(content, NAMED_FILTER).any? do |match|
|
31
68
|
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
32
69
|
end
|
@@ -34,17 +71,23 @@ module ThemeCheck
|
|
34
71
|
|
35
72
|
def partial(content, cursor)
|
36
73
|
return '' unless content.match?(NAMED_FILTER)
|
74
|
+
|
37
75
|
partial_match = matches(content, NAMED_FILTER).find do |match|
|
38
76
|
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
39
77
|
end
|
40
78
|
return '' if partial_match.nil?
|
79
|
+
|
41
80
|
partial_match[1]
|
42
81
|
end
|
43
82
|
|
44
83
|
def filter_to_completion(filter)
|
84
|
+
content = ShopifyLiquid::Documentation.render_doc(filter)
|
85
|
+
|
45
86
|
{
|
46
|
-
label: filter,
|
87
|
+
label: filter.name,
|
47
88
|
kind: CompletionItemKinds::FUNCTION,
|
89
|
+
tags: filter.deprecated? ? [CompletionItemTag::DEPRECATED] : [],
|
90
|
+
**doc_hash(content),
|
48
91
|
}
|
49
92
|
end
|
50
93
|
end
|
data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class ObjectAttributeCompletionProvider < CompletionProvider
|
6
|
+
def completions(context)
|
7
|
+
content = context.content
|
8
|
+
cursor = context.cursor
|
9
|
+
|
10
|
+
return [] if content.nil?
|
11
|
+
return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
|
12
|
+
return [] if content[cursor - 1] == "." && content[cursor - 2] == "."
|
13
|
+
|
14
|
+
# Navigate through lookups until the last valid [object, property] level
|
15
|
+
object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
|
16
|
+
|
17
|
+
# If the last lookup level is incomplete/invalid, use the partial term
|
18
|
+
# to filter object properties.
|
19
|
+
partial = partial_property_name(property, variable_lookup)
|
20
|
+
|
21
|
+
return [] unless object
|
22
|
+
|
23
|
+
object
|
24
|
+
.properties
|
25
|
+
.select { |prop| partial.nil? || prop.name.start_with?(partial) }
|
26
|
+
.map { |prop| property_to_completion(prop) }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def partial_property_name(property, variable_lookup)
|
32
|
+
last_property = variable_lookup.lookups.last
|
33
|
+
last_property if last_property != property&.name
|
34
|
+
end
|
35
|
+
|
36
|
+
def property_to_completion(prop)
|
37
|
+
content = ShopifyLiquid::Documentation.render_doc(prop)
|
38
|
+
|
39
|
+
{
|
40
|
+
label: prop.name,
|
41
|
+
kind: CompletionItemKinds::PROPERTY,
|
42
|
+
**doc_hash(content),
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -3,19 +3,19 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
module LanguageServer
|
5
5
|
class ObjectCompletionProvider < CompletionProvider
|
6
|
-
def completions(
|
7
|
-
|
6
|
+
def completions(context)
|
7
|
+
content = context.content
|
8
|
+
|
9
|
+
return [] if content.nil?
|
10
|
+
return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
|
8
11
|
return [] unless variable_lookup.lookups.empty?
|
9
|
-
return [] if content[cursor - 1] == "."
|
12
|
+
return [] if content[context.cursor - 1] == "."
|
13
|
+
|
10
14
|
ShopifyLiquid::Object.labels
|
11
15
|
.select { |w| w.start_with?(partial(variable_lookup)) }
|
12
16
|
.map { |object| object_to_completion(object) }
|
13
17
|
end
|
14
18
|
|
15
|
-
def variable_lookup_at_cursor(content, cursor)
|
16
|
-
VariableLookupFinder.lookup(content, cursor)
|
17
|
-
end
|
18
|
-
|
19
19
|
def partial(variable_lookup)
|
20
20
|
variable_lookup.name || ''
|
21
21
|
end
|
@@ -23,9 +23,12 @@ module ThemeCheck
|
|
23
23
|
private
|
24
24
|
|
25
25
|
def object_to_completion(object)
|
26
|
+
content = ShopifyLiquid::Documentation.object_doc(object)
|
27
|
+
|
26
28
|
{
|
27
29
|
label: object,
|
28
30
|
kind: CompletionItemKinds::VARIABLE,
|
31
|
+
**doc_hash(content),
|
29
32
|
}
|
30
33
|
end
|
31
34
|
end
|
data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb
CHANGED
@@ -3,7 +3,11 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
module LanguageServer
|
5
5
|
class RenderSnippetCompletionProvider < CompletionProvider
|
6
|
-
def completions(
|
6
|
+
def completions(context)
|
7
|
+
content = context.content
|
8
|
+
cursor = context.cursor
|
9
|
+
|
10
|
+
return [] if content.nil?
|
7
11
|
return [] unless cursor_on_quoted_argument?(content, cursor)
|
8
12
|
partial = snippet(content) || ''
|
9
13
|
snippets
|
@@ -3,7 +3,11 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
module LanguageServer
|
5
5
|
class TagCompletionProvider < CompletionProvider
|
6
|
-
def completions(
|
6
|
+
def completions(context)
|
7
|
+
content = context.content
|
8
|
+
cursor = context.cursor
|
9
|
+
|
10
|
+
return [] if content.nil?
|
7
11
|
return [] unless can_complete?(content, cursor)
|
8
12
|
partial = first_word(content) || ''
|
9
13
|
labels = ShopifyLiquid::Tag.labels
|
@@ -23,9 +27,12 @@ module ThemeCheck
|
|
23
27
|
private
|
24
28
|
|
25
29
|
def tag_to_completion(tag)
|
30
|
+
content = ShopifyLiquid::Documentation.tag_doc(tag)
|
31
|
+
|
26
32
|
{
|
27
33
|
label: tag,
|
28
34
|
kind: CompletionItemKinds::KEYWORD,
|
35
|
+
**doc_hash(content),
|
29
36
|
}
|
30
37
|
end
|
31
38
|
end
|
@@ -12,57 +12,103 @@ module ThemeCheck
|
|
12
12
|
@diagnostics_manager = diagnostics_manager
|
13
13
|
@storage = storage
|
14
14
|
@bridge = bridge
|
15
|
-
@token = 0
|
16
15
|
end
|
17
16
|
|
18
17
|
def first_run?
|
19
18
|
@diagnostics_manager.first_run?
|
20
19
|
end
|
21
20
|
|
22
|
-
def analyze_and_send_offenses(
|
21
|
+
def analyze_and_send_offenses(absolute_path_or_paths, config, force: false, only_single_file: false)
|
23
22
|
return unless @diagnostics_lock.try_lock
|
24
|
-
|
25
|
-
@bridge.send_create_work_done_progress_request(@token)
|
23
|
+
|
26
24
|
theme = ThemeCheck::Theme.new(storage)
|
27
25
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
28
26
|
|
29
|
-
if
|
30
|
-
|
31
|
-
@bridge.log("Checking #{storage.root}")
|
32
|
-
offenses = nil
|
33
|
-
time = Benchmark.measure do
|
34
|
-
offenses = analyzer.analyze_theme do |path, i, total|
|
35
|
-
@bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
|
39
|
-
@bridge.send_work_done_progress_end(@token, end_message)
|
40
|
-
@bridge.log(end_message)
|
41
|
-
send_diagnostics(offenses)
|
27
|
+
if !only_single_file && (@diagnostics_manager.first_run? || force)
|
28
|
+
run_full_theme_check(analyzer)
|
42
29
|
else
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
56
|
-
@bridge.send_work_done_progress_end(@token, end_message)
|
57
|
-
@bridge.log(end_message)
|
58
|
-
send_diagnostics(offenses, [relative_path], only_single_file: only_single_file)
|
59
|
-
end
|
30
|
+
run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
@diagnostics_lock.unlock
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_diagnostics(relative_paths)
|
37
|
+
return unless @diagnostics_lock.try_lock
|
38
|
+
|
39
|
+
as_array(relative_paths).each do |relative_path|
|
40
|
+
send_clearing_diagnostics(relative_path)
|
60
41
|
end
|
42
|
+
|
61
43
|
@diagnostics_lock.unlock
|
62
44
|
end
|
63
45
|
|
64
46
|
private
|
65
47
|
|
48
|
+
def run_full_theme_check(analyzer)
|
49
|
+
raise 'Unsafe operation' unless @diagnostics_lock.owned?
|
50
|
+
|
51
|
+
token = @bridge.send_create_work_done_progress_request
|
52
|
+
@bridge.send_work_done_progress_begin(token, "Full theme check")
|
53
|
+
@bridge.log("Checking #{storage.root}")
|
54
|
+
offenses = nil
|
55
|
+
time = Benchmark.measure do
|
56
|
+
offenses = analyzer.analyze_theme do |path, i, total|
|
57
|
+
@bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
|
61
|
+
@bridge.send_work_done_progress_end(token, end_message)
|
62
|
+
@bridge.log(end_message)
|
63
|
+
send_diagnostics(offenses)
|
64
|
+
end
|
65
|
+
|
66
|
+
def run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
|
67
|
+
raise 'Unsafe operation' unless @diagnostics_lock.owned?
|
68
|
+
|
69
|
+
relative_paths = as_array(absolute_path_or_paths).map do |absolute_path|
|
70
|
+
Pathname.new(storage.relative_path(absolute_path))
|
71
|
+
end
|
72
|
+
|
73
|
+
theme_files = relative_paths
|
74
|
+
.map { |relative_path| theme[relative_path] }
|
75
|
+
.reject(&:nil?)
|
76
|
+
|
77
|
+
deleted_relative_paths = relative_paths - theme_files.map(&:relative_path)
|
78
|
+
deleted_relative_paths
|
79
|
+
.each { |p| send_clearing_diagnostics(p) }
|
80
|
+
|
81
|
+
token = @bridge.send_create_work_done_progress_request
|
82
|
+
@bridge.send_work_done_progress_begin(token, "Partial theme check")
|
83
|
+
offenses = nil
|
84
|
+
time = Benchmark.measure do
|
85
|
+
offenses = analyzer.analyze_files(theme_files, only_single_file: only_single_file) do |path, i, total|
|
86
|
+
@bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
90
|
+
@bridge.send_work_done_progress_end(token, end_message)
|
91
|
+
@bridge.log(end_message)
|
92
|
+
send_diagnostics(offenses, theme_files.map(&:relative_path), only_single_file: only_single_file)
|
93
|
+
end
|
94
|
+
|
95
|
+
def send_clearing_diagnostics(relative_path)
|
96
|
+
raise 'Unsafe operation' unless @diagnostics_lock.owned?
|
97
|
+
|
98
|
+
relative_path = Pathname.new(relative_path) unless relative_path.is_a?(Pathname)
|
99
|
+
@diagnostics_manager.clear_diagnostics(relative_path)
|
100
|
+
send_diagnostic(relative_path, DiagnosticsManager::NO_DIAGNOSTICS)
|
101
|
+
end
|
102
|
+
|
103
|
+
def as_array(maybe_array)
|
104
|
+
case maybe_array
|
105
|
+
when Array
|
106
|
+
maybe_array
|
107
|
+
else
|
108
|
+
[maybe_array]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
66
112
|
def send_diagnostics(offenses, analyzed_files = nil, only_single_file: false)
|
67
113
|
@diagnostics_manager.build_diagnostics(
|
68
114
|
offenses,
|