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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +10 -0
  5. data/RELEASING.md +13 -0
  6. data/config/default.yml +5 -0
  7. data/data/shopify_liquid/deprecated_filters.yml +4 -0
  8. data/data/shopify_liquid/filters.yml +2 -1
  9. data/docs/checks/schema_json_format.md +76 -0
  10. data/docs/language_server/code-action-command-palette.png +0 -0
  11. data/docs/language_server/code-action-flow.png +0 -0
  12. data/docs/language_server/code-action-keyboard.png +0 -0
  13. data/docs/language_server/code-action-light-bulb.png +0 -0
  14. data/docs/language_server/code-action-problem.png +0 -0
  15. data/docs/language_server/code-action-quickfix.png +0 -0
  16. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  17. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  18. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  19. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  20. data/lib/theme_check/checks/default_locale.rb +1 -1
  21. data/lib/theme_check/checks/deprecated_filter.rb +79 -4
  22. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  23. data/lib/theme_check/checks/matching_schema_translations.rb +4 -6
  24. data/lib/theme_check/checks/matching_translations.rb +1 -0
  25. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  26. data/lib/theme_check/checks/missing_template.rb +1 -1
  27. data/lib/theme_check/checks/pagination_size.rb +2 -3
  28. data/lib/theme_check/checks/remote_asset.rb +5 -0
  29. data/lib/theme_check/checks/required_directories.rb +1 -1
  30. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  31. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  32. data/lib/theme_check/checks/translation_key_exists.rb +33 -13
  33. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  34. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  35. data/lib/theme_check/checks/valid_schema.rb +2 -2
  36. data/lib/theme_check/corrector.rb +28 -54
  37. data/lib/theme_check/file_system_storage.rb +4 -3
  38. data/lib/theme_check/html_node.rb +99 -6
  39. data/lib/theme_check/html_visitor.rb +1 -32
  40. data/lib/theme_check/in_memory_storage.rb +9 -0
  41. data/lib/theme_check/json_helpers.rb +14 -0
  42. data/lib/theme_check/language_server/bridge.rb +1 -1
  43. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  44. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  45. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  46. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  47. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  48. data/lib/theme_check/language_server/configuration.rb +69 -0
  49. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  50. data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
  51. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  52. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  53. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  54. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  55. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  56. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  57. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  58. data/lib/theme_check/language_server/handler.rb +79 -28
  59. data/lib/theme_check/language_server/io_messenger.rb +9 -1
  60. data/lib/theme_check/language_server/server.rb +8 -7
  61. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  62. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  63. data/lib/theme_check/language_server.rb +23 -5
  64. data/lib/theme_check/liquid_node.rb +249 -39
  65. data/lib/theme_check/locale_diff.rb +16 -4
  66. data/lib/theme_check/node.rb +16 -0
  67. data/lib/theme_check/offense.rb +27 -23
  68. data/lib/theme_check/regex_helpers.rb +1 -1
  69. data/lib/theme_check/schema_helper.rb +70 -0
  70. data/lib/theme_check/storage.rb +4 -0
  71. data/lib/theme_check/theme.rb +1 -1
  72. data/lib/theme_check/theme_file.rb +8 -1
  73. data/lib/theme_check/theme_file_rewriter.rb +18 -9
  74. data/lib/theme_check/version.rb +1 -1
  75. data/lib/theme_check.rb +7 -2
  76. metadata +26 -3
  77. data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -5,31 +5,30 @@ module ThemeCheck
5
5
  class DiagnosticsEngine
6
6
  include URIHelper
7
7
 
8
- def initialize(bridge)
8
+ attr_reader :storage
9
+
10
+ def initialize(storage, bridge, diagnostics_manager = DiagnosticsManager.new)
9
11
  @diagnostics_lock = Mutex.new
10
- @diagnostics_tracker = DiagnosticsTracker.new
12
+ @diagnostics_manager = diagnostics_manager
13
+ @storage = storage
11
14
  @bridge = bridge
12
15
  @token = 0
13
16
  end
14
17
 
15
18
  def first_run?
16
- @diagnostics_tracker.first_run?
19
+ @diagnostics_manager.first_run?
17
20
  end
18
21
 
19
- def analyze_and_send_offenses(absolute_path, config)
22
+ def analyze_and_send_offenses(absolute_path, config, force: false)
20
23
  return unless @diagnostics_lock.try_lock
21
24
  @token += 1
22
25
  @bridge.send_create_work_done_progress_request(@token)
23
- storage = ThemeCheck::FileSystemStorage.new(
24
- config.root,
25
- ignored_patterns: config.ignored_patterns
26
- )
27
26
  theme = ThemeCheck::Theme.new(storage)
28
27
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
29
28
 
30
- if @diagnostics_tracker.first_run?
29
+ if @diagnostics_manager.first_run? || force
31
30
  @bridge.send_work_done_progress_begin(@token, "Full theme check")
32
- @bridge.log("Checking #{config.root}")
31
+ @bridge.log("Checking #{storage.root}")
33
32
  offenses = nil
34
33
  time = Benchmark.measure do
35
34
  offenses = analyzer.analyze_theme do |path, i, total|
@@ -55,7 +54,7 @@ module ThemeCheck
55
54
  end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
56
55
  @bridge.send_work_done_progress_end(@token, end_message)
57
56
  @bridge.log(end_message)
58
- send_diagnostics(offenses, [absolute_path])
57
+ send_diagnostics(offenses, [relative_path])
59
58
  end
60
59
  end
61
60
  @diagnostics_lock.unlock
@@ -64,62 +63,18 @@ module ThemeCheck
64
63
  private
65
64
 
66
65
  def send_diagnostics(offenses, analyzed_files = nil)
67
- @diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
68
- send_diagnostic(path, diagnostic_offenses)
66
+ @diagnostics_manager.build_diagnostics(offenses, analyzed_files: analyzed_files).each do |relative_path, diagnostics|
67
+ send_diagnostic(relative_path, diagnostics)
69
68
  end
70
69
  end
71
70
 
72
- def send_diagnostic(path, offenses)
71
+ def send_diagnostic(relative_path, diagnostics)
73
72
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
74
73
  @bridge.send_notification('textDocument/publishDiagnostics', {
75
- uri: file_uri(path),
76
- diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
74
+ uri: file_uri(storage.path(relative_path)),
75
+ diagnostics: diagnostics.map(&:to_h),
77
76
  })
78
77
  end
79
-
80
- def offense_to_diagnostic(offense)
81
- diagnostic = {
82
- code: offense.code_name,
83
- message: offense.message,
84
- range: range(offense),
85
- severity: severity(offense),
86
- source: "theme-check",
87
- }
88
- diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
89
- diagnostic
90
- end
91
-
92
- def code_description(offense)
93
- {
94
- href: offense.doc,
95
- }
96
- end
97
-
98
- def severity(offense)
99
- case offense.severity
100
- when :error
101
- 1
102
- when :suggestion
103
- 2
104
- when :style
105
- 3
106
- else
107
- 4
108
- end
109
- end
110
-
111
- def range(offense)
112
- {
113
- start: {
114
- line: offense.start_line,
115
- character: offense.start_column,
116
- },
117
- end: {
118
- line: offense.end_line,
119
- character: offense.end_column,
120
- },
121
- }
122
- end
123
78
  end
124
79
  end
125
80
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ require "logger"
3
+
4
+ module ThemeCheck
5
+ module LanguageServer
6
+ class DiagnosticsManager
7
+ # This class exists to facilitate LanguageServer diagnostics tracking.
8
+ #
9
+ # Motivations:
10
+ # 1. The first time we lint, we want all the errors from all the files.
11
+ # 2. If we fix all the errors in a file, we have to send an empty array for that file.
12
+ # 3. If we do a partial check, we should consider the whole theme diagnostics as valid, and return cached results
13
+ # 4. We should be able to create WorkspaceEdits from diagnostics, so that the ExecuteCommandEngine can do its job
14
+ # 5. We should clean up diagnostics that were applied by the client
15
+ def initialize
16
+ @latest_diagnostics = {} # { [Pathname(relative_path)] => Diagnostic[] }
17
+ @mutex = Mutex.new
18
+ @first_run = true
19
+ end
20
+
21
+ def first_run?
22
+ @first_run
23
+ end
24
+
25
+ def diagnostics(relative_path)
26
+ relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
27
+ @mutex.synchronize { @latest_diagnostics[relative_path] || [] }
28
+ end
29
+
30
+ def build_diagnostics(offenses, analyzed_files: nil)
31
+ @mutex.synchronize do
32
+ full_check = analyzed_files.nil?
33
+ analyzed_paths = analyzed_files.map { |path| Pathname.new(path) } unless full_check
34
+
35
+ # When analyzed_files is nil, contains all offenses.
36
+ # When analyzed_files is !nil, contains all whole theme offenses and single file offenses of the analyzed_files.
37
+ current_diagnostics = offenses
38
+ .select(&:theme_file)
39
+ .group_by(&:theme_file)
40
+ .transform_keys { |theme_file| Pathname.new(theme_file.relative_path) }
41
+ .transform_values do |theme_file_offenses|
42
+ theme_file_offenses.map { |o| Diagnostic.new(o) }
43
+ end
44
+
45
+ previous_paths = paths(@latest_diagnostics)
46
+ current_paths = paths(current_diagnostics)
47
+
48
+ diagnostics_update = (current_paths + previous_paths).map do |path|
49
+ # When doing a full_check, we either send the current
50
+ # diagnostics or an empty array to clear the diagnostics
51
+ # for that file.
52
+ if full_check
53
+ [path, current_diagnostics[path] || []]
54
+
55
+ # When doing a partial check, the single file diagnostics
56
+ # from the previous runs should be sent. Otherwise the
57
+ # latest results are the good ones.
58
+ else
59
+ new_diagnostics = current_diagnostics[path] || []
60
+ should_use_cached_results = !analyzed_paths.include?(path)
61
+ old_diagnostics = should_use_cached_results ? single_file_diagnostics(path) : []
62
+ [path, new_diagnostics + old_diagnostics]
63
+ end
64
+ end.to_h
65
+
66
+ @latest_diagnostics = diagnostics_update.reject { |_, v| v.empty? }
67
+ @first_run = false
68
+ diagnostics_update
69
+ end
70
+ end
71
+
72
+ def workspace_edit(diagnostics)
73
+ diagnostics = sanitize(diagnostics)
74
+ .select(&:correctable?)
75
+
76
+ {
77
+ documentChanges: document_changes(diagnostics),
78
+ }
79
+ end
80
+
81
+ def delete_applied(diagnostics)
82
+ diagnostics = sanitize(diagnostics)
83
+ .select(&:correctable?)
84
+
85
+ previous_paths = paths(@latest_diagnostics)
86
+
87
+ diagnostics.each do |diagnostic|
88
+ delete(diagnostic.relative_path, diagnostic)
89
+ end
90
+
91
+ current_paths = paths(@latest_diagnostics)
92
+
93
+ (current_paths + previous_paths).map do |path|
94
+ [path, @latest_diagnostics[path] || []]
95
+ end.to_h
96
+ end
97
+
98
+ private
99
+
100
+ def sanitize(diagnostics)
101
+ diagnostics = diagnostics.map { |hash| find(hash) }.reject(&:nil?) if diagnostics[0]&.is_a?(Hash)
102
+ diagnostics
103
+ end
104
+
105
+ def delete(relative_path, diagnostic)
106
+ relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
107
+ @mutex.synchronize do
108
+ @latest_diagnostics[relative_path]&.delete(diagnostic)
109
+ @latest_diagnostics.delete(relative_path) if @latest_diagnostics[relative_path]&.empty?
110
+ end
111
+ end
112
+
113
+ def find(diagnostic_hash)
114
+ diagnostics(diagnostic_hash.dig(:data, :relative_path))
115
+ .find { |d| d == diagnostic_hash }
116
+ end
117
+
118
+ def document_changes(diagnostics)
119
+ corrector = DocumentChangeCorrector.new
120
+ diagnostics.each do |diagnostic|
121
+ offense = diagnostic.offense
122
+ offense.correct(corrector)
123
+ end
124
+ corrector.document_changes
125
+ end
126
+
127
+ def paths(diagnostics)
128
+ (diagnostics || {}).keys.map { |path| Pathname.new(path) }.to_set
129
+ end
130
+
131
+ def single_file_diagnostics(relative_path)
132
+ @latest_diagnostics[relative_path]&.select(&:single_file?) || []
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentChangeCorrector
6
+ include URIHelper
7
+ include JsonHelpers
8
+
9
+ def initialize
10
+ @json_edits = {}
11
+ @json_file_edits = {}
12
+ @text_document_edits = {}
13
+ @create_files = []
14
+ @rename_files = []
15
+ @delete_files = []
16
+ end
17
+
18
+ def document_changes
19
+ apply_json_edits
20
+ apply_json_file_edits
21
+ @create_files + @rename_files + @text_document_edits.values + @delete_files
22
+ end
23
+
24
+ # @param node [Node]
25
+ def insert_before(node, content, character_range = nil)
26
+ position = character_range_position(node, character_range) if character_range
27
+ edits(node) << {
28
+ range: {
29
+ start: start_location(position || node),
30
+ end: start_location(position || node),
31
+ },
32
+ newText: content,
33
+ }
34
+ end
35
+
36
+ # @param node [Node]
37
+ def insert_after(node, content, character_range = nil)
38
+ position = character_range_position(node, character_range) if character_range
39
+ edits(node) << {
40
+ range: {
41
+ start: end_location(position || node),
42
+ end: end_location(position || node),
43
+ },
44
+ newText: content,
45
+ }
46
+ end
47
+
48
+ def replace(node, content, character_range = nil)
49
+ position = character_range_position(node, character_range) if character_range
50
+ edits(node) << {
51
+ range: range(position || node),
52
+ newText: content,
53
+ }
54
+ end
55
+
56
+ # @param node [LiquidNode]
57
+ def remove(node)
58
+ edits(node) << {
59
+ range: {
60
+ start: { line: node.outer_markup_start_row, character: node.outer_markup_start_column },
61
+ end: { line: node.outer_markup_end_row, character: node.outer_markup_end_column },
62
+ },
63
+ newText: '',
64
+ }
65
+ end
66
+
67
+ def replace_inner_markup(node, content)
68
+ edits(node) << {
69
+ range: {
70
+ start: {
71
+ line: node.inner_markup_start_row,
72
+ character: node.inner_markup_start_column,
73
+ },
74
+ end: {
75
+ line: node.inner_markup_end_row,
76
+ character: node.inner_markup_end_column,
77
+ },
78
+ },
79
+ newText: content,
80
+ }
81
+ end
82
+
83
+ def replace_inner_json(node, json, **pretty_json_opts)
84
+ # Kind of brittle alert: We're assuming that modifications are
85
+ # made directly on the same json hash (e.g. schema). As such,
86
+ # if this assumption is true, then it follows that the
87
+ # "correct" JSON is the _last_ one that we defined.
88
+ #
89
+ # We're going to append those changes to the text edit when
90
+ # we're done.
91
+ #
92
+ # We're doing this because no language client will accept
93
+ # text modifications that occur on the same range. So we need
94
+ # to dedup our JSON edits for the client to accept our change.
95
+ #
96
+ # What we're doing here is overwriting the json edit for a
97
+ # node to the latest one that is called. If all the edits
98
+ # occur on the same hash, this final hash will have all the
99
+ # edits in it.
100
+ @json_edits[node] = [json, pretty_json_opts]
101
+ end
102
+
103
+ def wrap(node, insert_before, insert_after)
104
+ edits(node) << {
105
+ range: range(node),
106
+ newText: insert_before + node.markup + insert_after,
107
+ }
108
+ end
109
+
110
+ def create_file(storage, relative_path, contents = nil, overwrite: false)
111
+ uri = file_uri(storage.path(relative_path))
112
+ @create_files << create_file_change(uri, overwrite)
113
+ return if contents.nil?
114
+ text_document = { uri: uri, version: nil }
115
+ @text_document_edits[text_document] = {
116
+ textDocument: text_document,
117
+ edits: [{
118
+ range: {
119
+ start: { line: 0, character: 0 },
120
+ end: { line: 0, character: 0 },
121
+ },
122
+ newText: contents,
123
+ }],
124
+ }
125
+ end
126
+
127
+ def remove_file(storage, relative_path)
128
+ uri = file_uri(storage.path(relative_path))
129
+ @delete_files << delete_file_change(uri)
130
+ end
131
+
132
+ def mkdir(storage, relative_path)
133
+ path = Pathname.new(relative_path).join("tmp").to_s
134
+ # The LSP doesn't have a concept for directories, so what we
135
+ # do is create a file and then delete it.
136
+ #
137
+ # It does the job :upside_down_smile:.
138
+ create_file(storage, path)
139
+ remove_file(storage, path)
140
+ end
141
+
142
+ def add_translation(file, path, value)
143
+ raise ArgumentError unless file.is_a?(JsonFile)
144
+ hash = file.content
145
+ SchemaHelper.set(hash, path, value)
146
+ @json_file_edits[file] = hash
147
+ end
148
+
149
+ def remove_translation(file, path)
150
+ raise ArgumentError unless file.is_a?(JsonFile)
151
+ hash = file.content
152
+ SchemaHelper.delete(hash, path)
153
+ @json_file_edits[file] = hash
154
+ end
155
+
156
+ private
157
+
158
+ def apply_json_edits
159
+ @json_edits.each do |node, (json, pretty_json_opts)|
160
+ replace_inner_markup(node, pretty_json(json, **pretty_json_opts))
161
+ end
162
+ end
163
+
164
+ def apply_json_file_edits
165
+ @json_file_edits.each do |file, hash|
166
+ replace_entire_file(file, JSON.pretty_generate(hash))
167
+ end
168
+ end
169
+
170
+ def replace_entire_file(file, contents)
171
+ text_document = to_text_document(file)
172
+ position = ThemeCheck::StrictPosition.new(file.source, file.source, 0)
173
+ @text_document_edits[text_document] = {
174
+ textDocument: text_document,
175
+ edits: [{
176
+ range: {
177
+ start: { line: 0, character: 0 },
178
+ end: { line: position.end_row, character: position.end_column },
179
+ },
180
+ newText: contents,
181
+ }],
182
+ }
183
+ end
184
+
185
+ # @param node [Node]
186
+ def text_document_edit(node)
187
+ text_document = to_text_document(node)
188
+ @text_document_edits[text_document] ||= {
189
+ textDocument: text_document,
190
+ edits: [],
191
+ }
192
+ end
193
+
194
+ def create_file_change(uri, overwrite = false)
195
+ change = {}
196
+ change[:kind] = 'create'
197
+ change[:uri] = uri
198
+ change[:options] = { overwrite: overwrite } if overwrite
199
+ change
200
+ end
201
+
202
+ def delete_file_change(uri)
203
+ {
204
+ kind: 'delete',
205
+ uri: uri,
206
+ }
207
+ end
208
+
209
+ def edits(node)
210
+ text_document_edit(node)[:edits]
211
+ end
212
+
213
+ def to_text_document(thing)
214
+ case thing
215
+ when Node
216
+ {
217
+ uri: file_uri(thing.theme_file&.path),
218
+ version: thing.theme_file&.version,
219
+ }
220
+ when ThemeFile
221
+ {
222
+ uri: file_uri(thing.path),
223
+ version: thing.version,
224
+ }
225
+ else
226
+ raise ArgumentError
227
+ end
228
+ end
229
+
230
+ def absolute_path(node)
231
+ node.theme_file&.path
232
+ end
233
+
234
+ def character_range_position(node, character_range)
235
+ return unless character_range
236
+ source = node.theme_file.source
237
+ StrictPosition.new(
238
+ source[character_range],
239
+ source,
240
+ character_range.begin,
241
+ )
242
+ end
243
+
244
+ # @param node [ThemeCheck::Node]
245
+ def range(node)
246
+ {
247
+ start: start_location(node),
248
+ end: end_location(node),
249
+ }
250
+ end
251
+
252
+ def start_location(node)
253
+ {
254
+ line: node.start_row,
255
+ character: node.start_column,
256
+ }
257
+ end
258
+
259
+ def end_location(node)
260
+ {
261
+ line: node.end_row,
262
+ character: node.end_column,
263
+ }
264
+ end
265
+ end
266
+ end
267
+ end
@@ -37,12 +37,12 @@ module ThemeCheck
37
37
 
38
38
  def document_links(buffer)
39
39
  matches(buffer, partial_regexp).map do |match|
40
- start_line, start_character = from_index_to_row_column(
40
+ start_row, start_column = from_index_to_row_column(
41
41
  buffer,
42
42
  match.begin(:partial),
43
43
  )
44
44
 
45
- end_line, end_character = from_index_to_row_column(
45
+ end_row, end_column = from_index_to_row_column(
46
46
  buffer,
47
47
  match.end(:partial)
48
48
  )
@@ -51,12 +51,12 @@ module ThemeCheck
51
51
  target: file_link(match[:partial]),
52
52
  range: {
53
53
  start: {
54
- line: start_line,
55
- character: start_character,
54
+ line: start_row,
55
+ character: start_column,
56
56
  },
57
57
  end: {
58
- line: end_line,
59
- character: end_character,
58
+ line: end_row,
59
+ character: end_column,
60
60
  },
61
61
  },
62
62
  }
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ExecuteCommandEngine
6
+ def initialize
7
+ @providers = {}
8
+ end
9
+
10
+ def <<(provider)
11
+ @providers[provider.command] = provider
12
+ end
13
+
14
+ def execute(command, arguments)
15
+ @providers[command].execute(arguments)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ExecuteCommandProvider
6
+ class << self
7
+ def all
8
+ @all ||= []
9
+ end
10
+
11
+ def inherited(subclass)
12
+ all << subclass
13
+ end
14
+
15
+ def command(cmd = nil)
16
+ @command = cmd unless cmd.nil?
17
+ @command
18
+ end
19
+ end
20
+
21
+ def execute(arguments)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def command
26
+ self.class.command
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class CorrectionExecuteCommandProvider < ExecuteCommandProvider
6
+ include URIHelper
7
+
8
+ command "correction"
9
+
10
+ attr_reader :storage, :bridge, :diagnostics_manager
11
+
12
+ def initialize(storage, bridge, diagnostics_manager)
13
+ @storage = storage
14
+ @bridge = bridge
15
+ @diagnostics_manager = diagnostics_manager
16
+ end
17
+
18
+ # The arguments passed to this method are the ones forwarded
19
+ # from the selected CodeAction by the client.
20
+ #
21
+ # @param diagnostic_hashes [Array] - of diagnostics
22
+ def execute(diagnostic_hashes)
23
+ # attempt to apply the document changes
24
+ workspace_edit = diagnostics_manager.workspace_edit(diagnostic_hashes)
25
+ result = bridge.send_request('workspace/applyEdit', {
26
+ label: 'Theme Check correction',
27
+ edit: workspace_edit,
28
+ })
29
+
30
+ # Bail if unable to apply changes
31
+ return unless result[:applied]
32
+
33
+ # Clean up internal representation of fixed diagnostics
34
+ diagnostics_update = diagnostics_manager.delete_applied(diagnostic_hashes)
35
+
36
+ # Send updated diagnostics to client
37
+ diagnostics_update
38
+ .map do |relative_path, diagnostics|
39
+ bridge.send_notification('textDocument/publishDiagnostics', {
40
+ uri: file_uri(storage.path(relative_path)),
41
+ diagnostics: diagnostics.map(&:to_h),
42
+ })
43
+ storage.path(relative_path)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class RunChecksExecuteCommandProvider < ExecuteCommandProvider
6
+ include URIHelper
7
+
8
+ command "runChecks"
9
+
10
+ def initialize(diagnostics_engine, root_path, root_config)
11
+ @diagnostics_engine = diagnostics_engine
12
+ @root_path = root_path
13
+ @root_config = root_config
14
+ end
15
+
16
+ def execute(_args)
17
+ @diagnostics_engine.analyze_and_send_offenses(@root_path, @root_config, force: true)
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end