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,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