theme-check 1.8.0 → 1.9.0

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