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,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
- def initialize(value, theme_file, placeholder_values = [], parent = nil)
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(@value.to_html)
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
- raise NotImplementedError
105
+ position.start_index
43
106
  end
44
107
 
45
108
  def end_index
46
- raise NotImplementedError
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
- doc, placeholder_values = parse(liquid_file)
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
@@ -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