theme-check 1.7.2 → 1.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|