theme-check 1.7.2 → 1.9.2
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/.gitignore +1 -0
- data/CHANGELOG.md +47 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +3 -1
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +81 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -13
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +34 -23
- data/lib/theme_check/file_system_storage.rb +4 -3
- data/lib/theme_check/html_node.rb +122 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -0
- data/lib/theme_check/language_server/bridge.rb +19 -5
- data/lib/theme_check/language_server/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +83 -29
- data/lib/theme_check/language_server/io_messenger.rb +11 -1
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +29 -11
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +23 -5
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +39 -8
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- data/lib/theme_check/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +26 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module ThemeCheck
|
|
3
3
|
class ValidSchema < LiquidCheck
|
|
4
|
-
severity :
|
|
4
|
+
severity :error
|
|
5
5
|
category :json
|
|
6
6
|
doc docs_url(__FILE__)
|
|
7
7
|
|
|
8
8
|
def on_schema(node)
|
|
9
|
-
JSON.parse(node.
|
|
9
|
+
JSON.parse(node.inner_markup)
|
|
10
10
|
rescue JSON::ParserError => e
|
|
11
11
|
add_offense(format_json_parse_error(e), node: node)
|
|
12
12
|
end
|
|
@@ -2,52 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
module ThemeCheck
|
|
4
4
|
class Corrector
|
|
5
|
+
include JsonHelpers
|
|
6
|
+
|
|
5
7
|
def initialize(theme_file:)
|
|
6
8
|
@theme_file = theme_file
|
|
7
9
|
end
|
|
8
10
|
|
|
9
|
-
def insert_after(node, content)
|
|
10
|
-
@theme_file.rewriter.insert_after(node, content)
|
|
11
|
+
def insert_after(node, content, character_range = nil)
|
|
12
|
+
@theme_file.rewriter.insert_after(node, content, character_range)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def insert_before(node, content, character_range = nil)
|
|
16
|
+
@theme_file.rewriter.insert_before(node, content, character_range)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
|
-
def
|
|
14
|
-
@theme_file.rewriter.
|
|
19
|
+
def remove(node)
|
|
20
|
+
@theme_file.rewriter.remove(node)
|
|
15
21
|
end
|
|
16
22
|
|
|
17
|
-
def replace(node, content)
|
|
18
|
-
@theme_file.rewriter.replace(node, content)
|
|
23
|
+
def replace(node, content, character_range = nil)
|
|
24
|
+
@theme_file.rewriter.replace(node, content, character_range)
|
|
19
25
|
node.markup = content
|
|
20
26
|
end
|
|
21
27
|
|
|
28
|
+
def replace_inner_markup(node, content)
|
|
29
|
+
@theme_file.rewriter.replace_inner_markup(node, content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def replace_inner_json(node, json, **pretty_json_opts)
|
|
33
|
+
replace_inner_markup(node, pretty_json(json, **pretty_json_opts))
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
def wrap(node, insert_before, insert_after)
|
|
23
37
|
@theme_file.rewriter.wrap(node, insert_before, insert_after)
|
|
24
38
|
end
|
|
25
39
|
|
|
26
|
-
def
|
|
27
|
-
|
|
40
|
+
def create_file(storage, relative_path, content)
|
|
41
|
+
storage.write(relative_path, content)
|
|
28
42
|
end
|
|
29
43
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
theme.default_locale_json.update_contents({})
|
|
44
|
+
def remove_file(storage, relative_path)
|
|
45
|
+
storage.remove(relative_path)
|
|
33
46
|
end
|
|
34
47
|
|
|
35
|
-
def
|
|
36
|
-
|
|
48
|
+
def mkdir(storage, relative_path)
|
|
49
|
+
storage.mkdir(relative_path)
|
|
37
50
|
end
|
|
38
51
|
|
|
39
|
-
def
|
|
40
|
-
|
|
52
|
+
def add_translation(json_file, path, value)
|
|
53
|
+
hash = json_file.content
|
|
54
|
+
SchemaHelper.set(hash, path, value)
|
|
55
|
+
json_file.update_contents(hash)
|
|
41
56
|
end
|
|
42
57
|
|
|
43
|
-
def
|
|
44
|
-
hash =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
pointer[token] = {} unless pointer.key?(token)
|
|
48
|
-
pointer[token]
|
|
49
|
-
end
|
|
50
|
-
file.update_contents(hash)
|
|
58
|
+
def remove_translation(json_file, path)
|
|
59
|
+
hash = json_file.content
|
|
60
|
+
SchemaHelper.delete(hash, path)
|
|
61
|
+
json_file.update_contents(hash)
|
|
51
62
|
end
|
|
52
63
|
end
|
|
53
64
|
end
|
|
@@ -36,13 +36,14 @@ module ThemeCheck
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def mkdir(relative_path)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
file(relative_path).mkpath
|
|
39
|
+
return if file_exists?(relative_path)
|
|
40
|
+
reset_memoizers
|
|
41
|
+
file(relative_path).mkpath
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def files
|
|
45
45
|
@file_array ||= glob("**/*")
|
|
46
|
+
.reject { |path| File.directory?(path) }
|
|
46
47
|
.map { |path| path.relative_path_from(@root).to_s }
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -5,12 +5,75 @@ module ThemeCheck
|
|
|
5
5
|
class HtmlNode < Node
|
|
6
6
|
extend Forwardable
|
|
7
7
|
include RegexHelpers
|
|
8
|
+
include PositionHelper
|
|
8
9
|
attr_reader :theme_file, :parent
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
class << self
|
|
12
|
+
include RegexHelpers
|
|
13
|
+
|
|
14
|
+
def parse(liquid_file)
|
|
15
|
+
placeholder_values = []
|
|
16
|
+
parseable_source = +liquid_file.source.clone
|
|
17
|
+
|
|
18
|
+
# Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
|
|
19
|
+
# parser from freaking out. We transparently replace those placeholders in
|
|
20
|
+
# HtmlNode.
|
|
21
|
+
#
|
|
22
|
+
# We're using base36 to prevent index bleeding on 36^3 tags.
|
|
23
|
+
# `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
|
|
24
|
+
# Should be enough.
|
|
25
|
+
#
|
|
26
|
+
# The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
|
|
27
|
+
#
|
|
28
|
+
# Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
|
|
29
|
+
#
|
|
30
|
+
# (Note, we're also maintaining newline characters in there so
|
|
31
|
+
# that line numbers match the source...)
|
|
32
|
+
matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
|
|
33
|
+
value = m[0]
|
|
34
|
+
next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
|
|
35
|
+
placeholder_values.push(value)
|
|
36
|
+
key = (placeholder_values.size - 1).to_s(36)
|
|
37
|
+
|
|
38
|
+
# Doing shenanigans so that line numbers match... Ugh.
|
|
39
|
+
keyed_placeholder = parseable_source[m.begin(0)...m.end(0)]
|
|
40
|
+
|
|
41
|
+
# First and last chars are ≬
|
|
42
|
+
keyed_placeholder[0] = "≬"
|
|
43
|
+
keyed_placeholder[-1] = "≬"
|
|
44
|
+
|
|
45
|
+
# Non newline characters are #
|
|
46
|
+
keyed_placeholder.gsub!(/[^\n≬]/, '#')
|
|
47
|
+
|
|
48
|
+
# First few # are replaced by the base10 ID of the tag
|
|
49
|
+
i = -1
|
|
50
|
+
keyed_placeholder.gsub!('#') do
|
|
51
|
+
i += 1
|
|
52
|
+
if i > key.size - 1
|
|
53
|
+
'#'
|
|
54
|
+
else
|
|
55
|
+
key[i]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Replace source by placeholder
|
|
60
|
+
parseable_source[m.begin(0)...m.end(0)] = keyed_placeholder
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
new(
|
|
64
|
+
Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
|
|
65
|
+
liquid_file,
|
|
66
|
+
placeholder_values,
|
|
67
|
+
parseable_source
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize(value, theme_file, placeholder_values, parseable_source, parent = nil)
|
|
11
73
|
@value = value
|
|
12
74
|
@theme_file = theme_file
|
|
13
75
|
@placeholder_values = placeholder_values
|
|
76
|
+
@parseable_source = parseable_source
|
|
14
77
|
@parent = parent
|
|
15
78
|
end
|
|
16
79
|
|
|
@@ -27,11 +90,11 @@ module ThemeCheck
|
|
|
27
90
|
def children
|
|
28
91
|
@children ||= @value
|
|
29
92
|
.children
|
|
30
|
-
.map { |child| HtmlNode.new(child, theme_file, @placeholder_values, self) }
|
|
93
|
+
.map { |child| HtmlNode.new(child, theme_file, @placeholder_values, @parseable_source, self) }
|
|
31
94
|
end
|
|
32
95
|
|
|
33
96
|
def markup
|
|
34
|
-
@markup ||= replace_placeholders(
|
|
97
|
+
@markup ||= replace_placeholders(parseable_markup)
|
|
35
98
|
end
|
|
36
99
|
|
|
37
100
|
def line_number
|
|
@@ -39,11 +102,27 @@ module ThemeCheck
|
|
|
39
102
|
end
|
|
40
103
|
|
|
41
104
|
def start_index
|
|
42
|
-
|
|
105
|
+
position.start_index
|
|
43
106
|
end
|
|
44
107
|
|
|
45
108
|
def end_index
|
|
46
|
-
|
|
109
|
+
position.end_index
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def start_row
|
|
113
|
+
position.start_row
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def start_column
|
|
117
|
+
position.start_column
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def end_row
|
|
121
|
+
position.end_row
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def end_column
|
|
125
|
+
position.end_column
|
|
47
126
|
end
|
|
48
127
|
|
|
49
128
|
def literal?
|
|
@@ -60,6 +139,35 @@ module ThemeCheck
|
|
|
60
139
|
.to_h
|
|
61
140
|
end
|
|
62
141
|
|
|
142
|
+
def parseable_markup
|
|
143
|
+
return @parseable_source if @value.name == "#document-fragment"
|
|
144
|
+
|
|
145
|
+
start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
|
|
146
|
+
@parseable_source
|
|
147
|
+
.match(/<\s*#{name}[^>]*>/im, start_index)[0]
|
|
148
|
+
rescue NoMethodError
|
|
149
|
+
# Don't know what's up with the following issue. Don't think
|
|
150
|
+
# null check is correct approach. This should give us more info.
|
|
151
|
+
# https://github.com/Shopify/theme-check/issues/528
|
|
152
|
+
ThemeCheck.bug(<<~MSG)
|
|
153
|
+
Can't find a parseable tag of name #{name} inside the parseable HTML.
|
|
154
|
+
|
|
155
|
+
Tag name:
|
|
156
|
+
#{@value.name.inspect}
|
|
157
|
+
|
|
158
|
+
File:
|
|
159
|
+
#{@theme_file.relative_path}
|
|
160
|
+
|
|
161
|
+
Line number:
|
|
162
|
+
#{line_number}
|
|
163
|
+
|
|
164
|
+
Excerpt:
|
|
165
|
+
```
|
|
166
|
+
#{@parseable_source.lines[line_number - 1...line_number + 5]}
|
|
167
|
+
```
|
|
168
|
+
MSG
|
|
169
|
+
end
|
|
170
|
+
|
|
63
171
|
def content
|
|
64
172
|
@content ||= replace_placeholders(@value.content)
|
|
65
173
|
end
|
|
@@ -74,10 +182,18 @@ module ThemeCheck
|
|
|
74
182
|
|
|
75
183
|
private
|
|
76
184
|
|
|
185
|
+
def position
|
|
186
|
+
@position ||= Position.new(
|
|
187
|
+
markup,
|
|
188
|
+
theme_file.source,
|
|
189
|
+
line_number_1_indexed: line_number,
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
77
193
|
def replace_placeholders(string)
|
|
78
194
|
# Replace all ≬{i}####≬ with the actual content.
|
|
79
195
|
string.gsub(HTML_LIQUID_PLACEHOLDER) do |match|
|
|
80
|
-
key = /[0-9a-z]+/.match(match)[0]
|
|
196
|
+
key = /[0-9a-z]+/.match(match.gsub("\n", ''))[0]
|
|
81
197
|
@placeholder_values[key.to_i(36)]
|
|
82
198
|
end
|
|
83
199
|
end
|
|
@@ -4,7 +4,6 @@ require "forwardable"
|
|
|
4
4
|
|
|
5
5
|
module ThemeCheck
|
|
6
6
|
class HtmlVisitor
|
|
7
|
-
include RegexHelpers
|
|
8
7
|
attr_reader :checks
|
|
9
8
|
|
|
10
9
|
def initialize(checks)
|
|
@@ -12,43 +11,13 @@ module ThemeCheck
|
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def visit_liquid_file(liquid_file)
|
|
15
|
-
|
|
16
|
-
visit(HtmlNode.new(doc, liquid_file, placeholder_values))
|
|
14
|
+
visit(HtmlNode.parse(liquid_file))
|
|
17
15
|
rescue ArgumentError => e
|
|
18
16
|
call_checks(:on_parse_error, e, liquid_file)
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
private
|
|
22
20
|
|
|
23
|
-
def parse(liquid_file)
|
|
24
|
-
placeholder_values = []
|
|
25
|
-
parseable_source = +liquid_file.source.clone
|
|
26
|
-
|
|
27
|
-
# Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
|
|
28
|
-
# parser from freaking out. We transparently replace those placeholders in
|
|
29
|
-
# HtmlNode.
|
|
30
|
-
#
|
|
31
|
-
# We're using base36 to prevent index bleeding on 36^3 tags.
|
|
32
|
-
# `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
|
|
33
|
-
# Should be enough.
|
|
34
|
-
#
|
|
35
|
-
# The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
|
|
36
|
-
#
|
|
37
|
-
# Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
|
|
38
|
-
matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
|
|
39
|
-
value = m[0]
|
|
40
|
-
next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
|
|
41
|
-
placeholder_values.push(value)
|
|
42
|
-
key = (placeholder_values.size - 1).to_s(36)
|
|
43
|
-
parseable_source[m.begin(0)...m.end(0)] = "≬#{key.ljust(m.end(0) - m.begin(0) - 2, '#')}≬"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
[
|
|
47
|
-
Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
|
|
48
|
-
placeholder_values,
|
|
49
|
-
]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
21
|
def visit(node)
|
|
53
22
|
call_checks(:on_element, node) if node.element?
|
|
54
23
|
call_checks(:"on_#{node.name}", node)
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# as a big hash already, leave it like that and save yourself some IO.
|
|
7
7
|
module ThemeCheck
|
|
8
8
|
class InMemoryStorage < Storage
|
|
9
|
+
attr_reader :root
|
|
10
|
+
|
|
9
11
|
def initialize(files = {}, root = "/dev/null")
|
|
10
12
|
@files = files
|
|
11
13
|
@root = Pathname.new(root)
|
|
@@ -29,6 +31,7 @@ module ThemeCheck
|
|
|
29
31
|
|
|
30
32
|
def mkdir(relative_path)
|
|
31
33
|
@files[relative_path] = nil
|
|
34
|
+
reset_memoizers
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def files
|
|
@@ -46,5 +49,11 @@ module ThemeCheck
|
|
|
46
49
|
def relative_path(absolute_path)
|
|
47
50
|
Pathname.new(absolute_path).relative_path_from(@root).to_s
|
|
48
51
|
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def reset_memoizers
|
|
56
|
+
@directories = nil
|
|
57
|
+
end
|
|
49
58
|
end
|
|
50
59
|
end
|
|
@@ -5,5 +5,19 @@ module ThemeCheck
|
|
|
5
5
|
message = error.message[/\d+: (.+)$/, 1] || 'Invalid syntax'
|
|
6
6
|
"#{message} in JSON"
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
def pretty_json(hash, start_level: 1, indent: " ")
|
|
10
|
+
start_indent = indent * start_level
|
|
11
|
+
|
|
12
|
+
<<~JSON
|
|
13
|
+
|
|
14
|
+
#{start_indent}#{JSON.pretty_generate(
|
|
15
|
+
hash,
|
|
16
|
+
indent: indent,
|
|
17
|
+
array_nl: "\n#{start_indent}",
|
|
18
|
+
object_nl: "\n#{start_indent}",
|
|
19
|
+
)}
|
|
20
|
+
JSON
|
|
21
|
+
end
|
|
8
22
|
end
|
|
9
23
|
end
|
|
@@ -37,15 +37,15 @@ module ThemeCheck
|
|
|
37
37
|
|
|
38
38
|
def read_message
|
|
39
39
|
message_body = @messenger.read_message
|
|
40
|
-
message_json = JSON.parse(message_body)
|
|
41
|
-
@messenger.log(JSON.pretty_generate(message_json)) if
|
|
40
|
+
message_json = JSON.parse(message_body, symbolize_names: true)
|
|
41
|
+
@messenger.log(JSON.pretty_generate(message_json)) if ThemeCheck.debug?
|
|
42
42
|
message_json
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def send_message(message_hash)
|
|
46
46
|
message_hash[:jsonrpc] = '2.0'
|
|
47
47
|
message_body = JSON.dump(message_hash)
|
|
48
|
-
@messenger.log(JSON.pretty_generate(message_hash)) if
|
|
48
|
+
@messenger.log(JSON.pretty_generate(message_hash)) if ThemeCheck.debug?
|
|
49
49
|
@messenger.send_message(message_body)
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -68,11 +68,25 @@ module ThemeCheck
|
|
|
68
68
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
|
|
69
69
|
def send_response(id, result = nil, error = nil)
|
|
70
70
|
message = { id: id }
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if error
|
|
72
|
+
message[:error] = error
|
|
73
|
+
else
|
|
74
|
+
message[:result] = result
|
|
75
|
+
end
|
|
73
76
|
send_message(message)
|
|
74
77
|
end
|
|
75
78
|
|
|
79
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
|
|
80
|
+
def send_internal_error(id, e)
|
|
81
|
+
send_response(id, nil, {
|
|
82
|
+
code: ErrorCodes::INTERNAL_ERROR,
|
|
83
|
+
message: <<~EOS,
|
|
84
|
+
#{e.class}: #{e.message}
|
|
85
|
+
#{e.backtrace.join("\n ")}
|
|
86
|
+
EOS
|
|
87
|
+
})
|
|
88
|
+
end
|
|
89
|
+
|
|
76
90
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
|
77
91
|
def send_notification(method, params)
|
|
78
92
|
message = { method: method }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class ClientCapabilities
|
|
6
|
+
def initialize(capabilities)
|
|
7
|
+
@capabilities = capabilities
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def supports_work_done_progress?
|
|
11
|
+
@capabilities.dig(:window, :workDoneProgress) || false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def supports_workspace_configuration?
|
|
15
|
+
@capabilities.dig(:workspace, :configuration) || false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def supports_workspace_did_change_configuration_dynamic_registration?
|
|
19
|
+
@capabilities.dig(:workspace, :didChangeConfiguration, :dynamicRegistration) || false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialization_option(key)
|
|
23
|
+
@capabilities.dig(:initializationOptions, key)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class CodeActionEngine
|
|
6
|
+
include PositionHelper
|
|
7
|
+
|
|
8
|
+
def initialize(storage, diagnostics_manager)
|
|
9
|
+
@storage = storage
|
|
10
|
+
@providers = CodeActionProvider.all.map { |c| c.new(storage, diagnostics_manager) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def code_actions(absolute_path, start_position, end_position, only_kinds = [])
|
|
14
|
+
relative_path = @storage.relative_path(absolute_path)
|
|
15
|
+
buffer = @storage.read(relative_path)
|
|
16
|
+
start_index = from_row_column_to_index(buffer, start_position[0], start_position[1])
|
|
17
|
+
end_index = from_row_column_to_index(buffer, end_position[0], end_position[1])
|
|
18
|
+
range = (start_index...end_index)
|
|
19
|
+
|
|
20
|
+
@providers
|
|
21
|
+
.filter do |provider|
|
|
22
|
+
only_kinds.empty? ||
|
|
23
|
+
only_kinds.include?(provider.kind) ||
|
|
24
|
+
only_kinds.include?(provider.base_kind)
|
|
25
|
+
end
|
|
26
|
+
.flat_map do |provider|
|
|
27
|
+
provider.code_actions(relative_path, range)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class CodeActionProvider
|
|
6
|
+
class << self
|
|
7
|
+
def all
|
|
8
|
+
@all ||= []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def inherited(subclass)
|
|
12
|
+
all << subclass
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def kind(k = nil)
|
|
16
|
+
@kind = k unless k.nil?
|
|
17
|
+
@kind
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :storage
|
|
22
|
+
attr_reader :diagnostics_manager
|
|
23
|
+
|
|
24
|
+
def initialize(storage, diagnostics_manager)
|
|
25
|
+
@storage = storage
|
|
26
|
+
@diagnostics_manager = diagnostics_manager
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def kind
|
|
30
|
+
self.class.kind
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def base_kind
|
|
34
|
+
kind.split('.')[0]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def code_actions(relative_path, range)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class QuickfixCodeActionProvider < CodeActionProvider
|
|
6
|
+
kind "quickfix"
|
|
7
|
+
|
|
8
|
+
def code_actions(relative_path, range)
|
|
9
|
+
correctable_diagnostics = diagnostics_manager
|
|
10
|
+
.diagnostics(relative_path)
|
|
11
|
+
.filter(&:correctable?)
|
|
12
|
+
.reject do |diagnostic|
|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
diagnostics_under_cursor = correctable_diagnostics
|
|
19
|
+
.filter { |diagnostic| diagnostic.offense.in_range?(range) }
|
|
20
|
+
|
|
21
|
+
return [] if diagnostics_under_cursor.empty?
|
|
22
|
+
|
|
23
|
+
(
|
|
24
|
+
quickfix_cursor_code_actions(diagnostics_under_cursor) +
|
|
25
|
+
quickfix_all_of_type_code_actions(diagnostics_under_cursor, correctable_diagnostics) +
|
|
26
|
+
quickfix_all_code_action(correctable_diagnostics)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def quickfix_cursor_code_actions(diagnostics)
|
|
33
|
+
diagnostics.map do |diagnostic|
|
|
34
|
+
{
|
|
35
|
+
title: "Fix this #{diagnostic.code} problem: #{diagnostic.message}",
|
|
36
|
+
kind: kind,
|
|
37
|
+
diagnostics: [diagnostic.to_h],
|
|
38
|
+
isPreferred: true,
|
|
39
|
+
command: {
|
|
40
|
+
title: 'quickfix',
|
|
41
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
42
|
+
arguments: [diagnostic.to_h],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def quickfix_all_of_type_code_actions(cursor_diagnostics, correctable_diagnostics)
|
|
49
|
+
codes = Set.new(cursor_diagnostics.map(&:code))
|
|
50
|
+
correctable_diagnostics_by_code = correctable_diagnostics.group_by(&:code)
|
|
51
|
+
codes.flat_map do |code|
|
|
52
|
+
diagnostics = correctable_diagnostics_by_code[code].map(&:to_h)
|
|
53
|
+
return [] unless diagnostics.size > 1
|
|
54
|
+
{
|
|
55
|
+
title: "Fix all #{code} problems",
|
|
56
|
+
kind: kind,
|
|
57
|
+
diagnostics: diagnostics,
|
|
58
|
+
command: {
|
|
59
|
+
title: 'quickfix',
|
|
60
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
61
|
+
arguments: diagnostics,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def quickfix_all_code_action(diagnostics)
|
|
68
|
+
return [] unless diagnostics.size > 1
|
|
69
|
+
diagnostics = diagnostics.map(&:to_h)
|
|
70
|
+
[{
|
|
71
|
+
title: "Fix all auto-fixable problems",
|
|
72
|
+
kind: kind,
|
|
73
|
+
diagnostics: diagnostics,
|
|
74
|
+
command: {
|
|
75
|
+
title: 'quickfix',
|
|
76
|
+
command: CorrectionExecuteCommandProvider.command,
|
|
77
|
+
arguments: diagnostics,
|
|
78
|
+
},
|
|
79
|
+
}]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class SourceFixAllCodeActionProvider < CodeActionProvider
|
|
6
|
+
kind "source.fixAll"
|
|
7
|
+
|
|
8
|
+
def code_actions(relative_path, _)
|
|
9
|
+
diagnostics = diagnostics_manager
|
|
10
|
+
.diagnostics(relative_path)
|
|
11
|
+
.filter(&:correctable?)
|
|
12
|
+
.reject do |diagnostic|
|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
|
16
|
+
end
|
|
17
|
+
.map(&:to_h)
|
|
18
|
+
diagnostics_to_code_action(diagnostics)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def diagnostics_to_code_action(diagnostics)
|
|
24
|
+
return [] if diagnostics.empty?
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
title: "Fix all Theme Check auto-fixable problems",
|
|
28
|
+
kind: kind,
|
|
29
|
+
diagnostics: diagnostics,
|
|
30
|
+
command: {
|
|
31
|
+
title: 'fixAll.file',
|
|
32
|
+
command: LanguageServer::CorrectionExecuteCommandProvider.command,
|
|
33
|
+
arguments: diagnostics,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|