theme-check 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +21 -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 +2 -1
- 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/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 +79 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +4 -6
- 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/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_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 +28 -54
- data/lib/theme_check/file_system_storage.rb +4 -3
- data/lib/theme_check/html_node.rb +99 -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 +1 -1
- 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 +79 -28
- data/lib/theme_check/language_server/io_messenger.rb +9 -1
- data/lib/theme_check/language_server/server.rb +8 -7
- 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 +249 -39
- data/lib/theme_check/locale_diff.rb +16 -4
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- 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/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +18 -9
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +7 -2
- metadata +26 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -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,12 @@ module ThemeCheck
|
|
60
139
|
.to_h
|
61
140
|
end
|
62
141
|
|
142
|
+
def parseable_markup
|
143
|
+
start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
|
144
|
+
@parseable_source
|
145
|
+
.match(/<\s*#{@value.name}[^>]*>/im, start_index)[0]
|
146
|
+
end
|
147
|
+
|
63
148
|
def content
|
64
149
|
@content ||= replace_placeholders(@value.content)
|
65
150
|
end
|
@@ -74,10 +159,18 @@ module ThemeCheck
|
|
74
159
|
|
75
160
|
private
|
76
161
|
|
162
|
+
def position
|
163
|
+
@position ||= Position.new(
|
164
|
+
markup,
|
165
|
+
theme_file.source,
|
166
|
+
line_number_1_indexed: line_number,
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
77
170
|
def replace_placeholders(string)
|
78
171
|
# Replace all ≬{i}####≬ with the actual content.
|
79
172
|
string.gsub(HTML_LIQUID_PLACEHOLDER) do |match|
|
80
|
-
key = /[0-9a-z]+/.match(match)[0]
|
173
|
+
key = /[0-9a-z]+/.match(match.gsub("\n", ''))[0]
|
81
174
|
@placeholder_values[key.to_i(36)]
|
82
175
|
end
|
83
176
|
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,7 +37,7 @@ module ThemeCheck
|
|
37
37
|
|
38
38
|
def read_message
|
39
39
|
message_body = @messenger.read_message
|
40
|
-
message_json = JSON.parse(message_body)
|
40
|
+
message_json = JSON.parse(message_body, symbolize_names: true)
|
41
41
|
@messenger.log(JSON.pretty_generate(message_json)) if ThemeCheck.debug?
|
42
42
|
message_json
|
43
43
|
end
|
@@ -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
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class Configuration
|
6
|
+
CHECK_ON_OPEN = :"themeCheck.checkOnOpen"
|
7
|
+
CHECK_ON_SAVE = :"themeCheck.checkOnSave"
|
8
|
+
CHECK_ON_CHANGE = :"themeCheck.checkOnChange"
|
9
|
+
|
10
|
+
def initialize(bridge, capabilities)
|
11
|
+
@bridge = bridge
|
12
|
+
@capabilities = capabilities
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@initialized = false
|
15
|
+
@config = {
|
16
|
+
CHECK_ON_OPEN => @capabilities.initialization_option(CHECK_ON_OPEN) || true,
|
17
|
+
CHECK_ON_SAVE => @capabilities.initialization_option(CHECK_ON_SAVE) || true,
|
18
|
+
CHECK_ON_CHANGE => @capabilities.initialization_option(CHECK_ON_CHANGE) || true,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(force: nil)
|
23
|
+
@mutex.synchronize do
|
24
|
+
return unless @capabilities.supports_workspace_configuration?
|
25
|
+
return if initialized? && !force
|
26
|
+
check_on_open, check_on_save, check_on_change = @bridge.send_request(
|
27
|
+
"workspace/configuration",
|
28
|
+
items: [
|
29
|
+
{ section: CHECK_ON_OPEN },
|
30
|
+
{ section: CHECK_ON_SAVE },
|
31
|
+
{ section: CHECK_ON_CHANGE },
|
32
|
+
],
|
33
|
+
)
|
34
|
+
@config[CHECK_ON_OPEN] = check_on_open unless check_on_open.nil?
|
35
|
+
@config[CHECK_ON_CHANGE] = check_on_change unless check_on_change.nil?
|
36
|
+
@config[CHECK_ON_SAVE] = check_on_save unless check_on_save.nil?
|
37
|
+
@initialized = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def register_did_change_capability
|
42
|
+
return unless @capabilities.supports_workspace_did_change_configuration_dynamic_registration?
|
43
|
+
@bridge.send_request('client/registerCapability', registrations: [{
|
44
|
+
id: "workspace/didChangeConfiguration",
|
45
|
+
method: "workspace/didChangeConfiguration",
|
46
|
+
}])
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialized?
|
50
|
+
@initialized
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_on_open?
|
54
|
+
fetch # making sure we have an initialized value
|
55
|
+
@config[CHECK_ON_OPEN]
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_on_save?
|
59
|
+
fetch # making sure we have for an initialized value
|
60
|
+
@config[CHECK_ON_SAVE]
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_on_change?
|
64
|
+
fetch # making sure we have for an initialized value
|
65
|
+
@config[CHECK_ON_CHANGE]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class Diagnostic
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
attr_reader :offense
|
9
|
+
|
10
|
+
def initialize(offense)
|
11
|
+
@offense = offense
|
12
|
+
@diagnostic = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
case other
|
17
|
+
when Hash, Diagnostic
|
18
|
+
to_h == other.to_h
|
19
|
+
else
|
20
|
+
raise ArgumentError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
return @diagnostic unless @diagnostic.nil?
|
26
|
+
@diagnostic = {
|
27
|
+
source: "theme-check",
|
28
|
+
code: code,
|
29
|
+
message: message,
|
30
|
+
range: range,
|
31
|
+
severity: severity,
|
32
|
+
data: data,
|
33
|
+
}
|
34
|
+
@diagnostic[:codeDescription] = code_description unless offense.doc.nil?
|
35
|
+
@diagnostic
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
to_h.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def single_file?
|
43
|
+
offense.single_file?
|
44
|
+
end
|
45
|
+
|
46
|
+
def correctable?
|
47
|
+
offense.correctable?
|
48
|
+
end
|
49
|
+
|
50
|
+
def code
|
51
|
+
offense.code_name
|
52
|
+
end
|
53
|
+
|
54
|
+
def message
|
55
|
+
offense.message
|
56
|
+
end
|
57
|
+
|
58
|
+
def code_description
|
59
|
+
{
|
60
|
+
href: offense.doc,
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def severity
|
65
|
+
case offense.severity
|
66
|
+
when :error
|
67
|
+
1
|
68
|
+
when :suggestion
|
69
|
+
2
|
70
|
+
when :style
|
71
|
+
3
|
72
|
+
else
|
73
|
+
4
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def range
|
78
|
+
{
|
79
|
+
start: {
|
80
|
+
line: offense.start_row,
|
81
|
+
character: offense.start_column,
|
82
|
+
},
|
83
|
+
end: {
|
84
|
+
line: offense.end_row,
|
85
|
+
character: offense.end_column,
|
86
|
+
},
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def start_index
|
91
|
+
offense.start_index
|
92
|
+
end
|
93
|
+
|
94
|
+
def end_index
|
95
|
+
offense.end_index
|
96
|
+
end
|
97
|
+
|
98
|
+
def absolute_path
|
99
|
+
@absolute_path ||= offense&.theme_file&.path
|
100
|
+
end
|
101
|
+
|
102
|
+
def relative_path
|
103
|
+
@relative_path ||= offense&.theme_file&.relative_path
|
104
|
+
end
|
105
|
+
|
106
|
+
def uri
|
107
|
+
@uri ||= absolute_path && file_uri(absolute_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
def file_version
|
111
|
+
@version ||= offense&.version
|
112
|
+
end
|
113
|
+
|
114
|
+
def data
|
115
|
+
{
|
116
|
+
absolute_path: absolute_path.to_s,
|
117
|
+
relative_path: relative_path.to_s,
|
118
|
+
uri: uri,
|
119
|
+
version: file_version,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|