theme-check 1.7.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +49 -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 +3 -1
  9. data/data/shopify_liquid/tags.yml +9 -9
  10. data/docs/checks/TEMPLATE.md.erb +24 -19
  11. data/docs/checks/schema_json_format.md +76 -0
  12. data/docs/language_server/code-action-command-palette.png +0 -0
  13. data/docs/language_server/code-action-flow.png +0 -0
  14. data/docs/language_server/code-action-keyboard.png +0 -0
  15. data/docs/language_server/code-action-light-bulb.png +0 -0
  16. data/docs/language_server/code-action-problem.png +0 -0
  17. data/docs/language_server/code-action-quickfix.png +0 -0
  18. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  19. data/exe/theme-check-language-server +0 -4
  20. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  21. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  22. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  23. data/lib/theme_check/checks/default_locale.rb +1 -1
  24. data/lib/theme_check/checks/deprecated_filter.rb +79 -4
  25. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  26. data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
  27. data/lib/theme_check/checks/matching_translations.rb +1 -0
  28. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  29. data/lib/theme_check/checks/missing_template.rb +1 -1
  30. data/lib/theme_check/checks/pagination_size.rb +2 -3
  31. data/lib/theme_check/checks/remote_asset.rb +5 -0
  32. data/lib/theme_check/checks/required_directories.rb +1 -1
  33. data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
  34. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  35. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  36. data/lib/theme_check/checks/translation_key_exists.rb +33 -25
  37. data/lib/theme_check/checks/unused_assign.rb +3 -2
  38. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  39. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  40. data/lib/theme_check/checks/valid_schema.rb +2 -2
  41. data/lib/theme_check/corrector.rb +34 -23
  42. data/lib/theme_check/exceptions.rb +1 -0
  43. data/lib/theme_check/file_system_storage.rb +8 -3
  44. data/lib/theme_check/html_node.rb +99 -6
  45. data/lib/theme_check/html_visitor.rb +1 -32
  46. data/lib/theme_check/in_memory_storage.rb +9 -0
  47. data/lib/theme_check/json_helpers.rb +14 -0
  48. data/lib/theme_check/language_server/bridge.rb +142 -0
  49. data/lib/theme_check/language_server/channel.rb +69 -0
  50. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  51. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  52. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  53. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  54. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  55. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
  56. data/lib/theme_check/language_server/configuration.rb +69 -0
  57. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  58. data/lib/theme_check/language_server/diagnostics_engine.rb +80 -0
  59. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  60. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  61. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  62. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  63. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  64. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  65. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  66. data/lib/theme_check/language_server/handler.rb +92 -217
  67. data/lib/theme_check/language_server/io_messenger.rb +112 -0
  68. data/lib/theme_check/language_server/messenger.rb +12 -42
  69. data/lib/theme_check/language_server/protocol.rb +4 -0
  70. data/lib/theme_check/language_server/server.rb +54 -110
  71. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  72. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  73. data/lib/theme_check/language_server.rb +28 -6
  74. data/lib/theme_check/liquid_node.rb +255 -12
  75. data/lib/theme_check/locale_diff.rb +48 -10
  76. data/lib/theme_check/node.rb +16 -0
  77. data/lib/theme_check/offense.rb +27 -23
  78. data/lib/theme_check/position.rb +4 -4
  79. data/lib/theme_check/regex_helpers.rb +1 -1
  80. data/lib/theme_check/schema_helper.rb +70 -0
  81. data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
  82. data/lib/theme_check/shopify_liquid/tag.rb +19 -1
  83. data/lib/theme_check/shopify_liquid.rb +1 -0
  84. data/lib/theme_check/storage.rb +4 -0
  85. data/lib/theme_check/tags.rb +0 -1
  86. data/lib/theme_check/theme.rb +1 -1
  87. data/lib/theme_check/theme_file.rb +8 -1
  88. data/lib/theme_check/theme_file_rewriter.rb +28 -6
  89. data/lib/theme_check/version.rb +1 -1
  90. data/lib/theme_check.rb +11 -2
  91. metadata +31 -3
  92. data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
3
  class ValidSchema < LiquidCheck
4
- severity :suggestion
4
+ severity :error
5
5
  category :json
6
6
  doc docs_url(__FILE__)
7
7
 
8
8
  def on_schema(node)
9
- JSON.parse(node.value.nodelist.join)
9
+ JSON.parse(node.inner_markup)
10
10
  rescue JSON::ParserError => e
11
11
  add_offense(format_json_parse_error(e), node: node)
12
12
  end
@@ -2,52 +2,63 @@
2
2
 
3
3
  module ThemeCheck
4
4
  class Corrector
5
+ include JsonHelpers
6
+
5
7
  def initialize(theme_file:)
6
8
  @theme_file = theme_file
7
9
  end
8
10
 
9
- def insert_after(node, content)
10
- @theme_file.rewriter.insert_after(node, content)
11
+ def insert_after(node, content, character_range = nil)
12
+ @theme_file.rewriter.insert_after(node, content, character_range)
13
+ end
14
+
15
+ def insert_before(node, content, character_range = nil)
16
+ @theme_file.rewriter.insert_before(node, content, character_range)
11
17
  end
12
18
 
13
- def insert_before(node, content)
14
- @theme_file.rewriter.insert_before(node, content)
19
+ def remove(node)
20
+ @theme_file.rewriter.remove(node)
15
21
  end
16
22
 
17
- def replace(node, content)
18
- @theme_file.rewriter.replace(node, content)
23
+ def replace(node, content, character_range = nil)
24
+ @theme_file.rewriter.replace(node, content, character_range)
19
25
  node.markup = content
20
26
  end
21
27
 
28
+ def replace_inner_markup(node, content)
29
+ @theme_file.rewriter.replace_inner_markup(node, content)
30
+ end
31
+
32
+ def replace_inner_json(node, json, **pretty_json_opts)
33
+ replace_inner_markup(node, pretty_json(json, **pretty_json_opts))
34
+ end
35
+
22
36
  def wrap(node, insert_before, insert_after)
23
37
  @theme_file.rewriter.wrap(node, insert_before, insert_after)
24
38
  end
25
39
 
26
- def create(theme, relative_path, content)
27
- theme.storage.write(relative_path, content)
40
+ def create_file(storage, relative_path, content)
41
+ storage.write(relative_path, content)
28
42
  end
29
43
 
30
- def create_default_locale_json(theme)
31
- theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
32
- theme.default_locale_json.update_contents({})
44
+ def remove_file(storage, relative_path)
45
+ storage.remove(relative_path)
33
46
  end
34
47
 
35
- def remove(theme, relative_path)
36
- theme.storage.remove(relative_path)
48
+ def mkdir(storage, relative_path)
49
+ storage.mkdir(relative_path)
37
50
  end
38
51
 
39
- def mkdir(theme, relative_path)
40
- theme.storage.mkdir(relative_path)
52
+ def add_translation(json_file, path, value)
53
+ hash = json_file.content
54
+ SchemaHelper.set(hash, path, value)
55
+ json_file.update_contents(hash)
41
56
  end
42
57
 
43
- def add_default_translation_key(file, key, value)
44
- hash = file.content
45
- key.reduce(hash) do |pointer, token|
46
- return pointer[token] = value if token == key.last
47
- pointer[token] = {} unless pointer.key?(token)
48
- pointer[token]
49
- end
50
- file.update_contents(hash)
58
+ def remove_translation(json_file, path)
59
+ hash = json_file.content
60
+ SchemaHelper.delete(hash, path)
61
+ json_file.update_contents(hash)
51
62
  end
52
63
  end
53
64
  end
@@ -22,6 +22,7 @@ module ThemeCheck
22
22
  Errno::EAGAIN,
23
23
  Errno::EHOSTUNREACH,
24
24
  Errno::ENETUNREACH,
25
+ Errno::EADDRNOTAVAIL,
25
26
  ]
26
27
 
27
28
  NET_HTTP_EXCEPTIONS = [
@@ -11,6 +11,10 @@ module ThemeCheck
11
11
  @files = {}
12
12
  end
13
13
 
14
+ def relative_path(absolute_path)
15
+ Pathname.new(absolute_path).relative_path_from(@root).to_s
16
+ end
17
+
14
18
  def path(relative_path)
15
19
  @root.join(relative_path)
16
20
  end
@@ -32,13 +36,14 @@ module ThemeCheck
32
36
  end
33
37
 
34
38
  def mkdir(relative_path)
35
- reset_memoizers unless file_exists?(relative_path)
36
-
37
- file(relative_path).mkpath unless file(relative_path).directory?
39
+ return if file_exists?(relative_path)
40
+ reset_memoizers
41
+ file(relative_path).mkpath
38
42
  end
39
43
 
40
44
  def files
41
45
  @file_array ||= glob("**/*")
46
+ .reject { |path| File.directory?(path) }
42
47
  .map { |path| path.relative_path_from(@root).to_s }
43
48
  end
44
49
 
@@ -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
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class exists as a bridge (or boundary) between our handlers and the outside world.
4
+ #
5
+ # It is concerned with all the Language Server Protocol constructs. i.e.
6
+ #
7
+ # - sending Hash messages as JSON
8
+ # - reading JSON messages as Hashes
9
+ # - preparing, sending and resolving requests
10
+ # - preparing and sending responses
11
+ # - preparing and sending notifications
12
+ # - preparing and sending progress notifications
13
+ #
14
+ # But it _not_ concerned by _how_ those messages are sent to the
15
+ # outside world. That's the job of the messenger.
16
+ #
17
+ # This enables us to have all the language server protocol logic
18
+ # in here living independently of how we communicate with the
19
+ # client (STDIO or websocket)
20
+ module ThemeCheck
21
+ module LanguageServer
22
+ class Bridge
23
+ attr_writer :supports_work_done_progress
24
+
25
+ def initialize(messenger)
26
+ # The messenger is responsible for IO.
27
+ # Could be STDIO or WebSockets or Mock.
28
+ @messenger = messenger
29
+
30
+ # Whether the client supports work done progress notifications
31
+ @supports_work_done_progress = false
32
+ end
33
+
34
+ def log(message)
35
+ @messenger.log(message)
36
+ end
37
+
38
+ def read_message
39
+ message_body = @messenger.read_message
40
+ message_json = JSON.parse(message_body, symbolize_names: true)
41
+ @messenger.log(JSON.pretty_generate(message_json)) if ThemeCheck.debug?
42
+ message_json
43
+ end
44
+
45
+ def send_message(message_hash)
46
+ message_hash[:jsonrpc] = '2.0'
47
+ message_body = JSON.dump(message_hash)
48
+ @messenger.log(JSON.pretty_generate(message_hash)) if ThemeCheck.debug?
49
+ @messenger.send_message(message_body)
50
+ end
51
+
52
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#requestMessage
53
+ def send_request(method, params = nil)
54
+ channel = Channel.create
55
+ message = { id: channel.id }
56
+ message[:method] = method
57
+ message[:params] = params if params
58
+ send_message(message)
59
+ channel.pop
60
+ ensure
61
+ channel.close
62
+ end
63
+
64
+ def receive_response(id, result)
65
+ Channel.by_id(id) << result
66
+ end
67
+
68
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
69
+ def send_response(id, result = nil, error = nil)
70
+ message = { id: id }
71
+ if error
72
+ message[:error] = error
73
+ else
74
+ message[:result] = result
75
+ end
76
+ send_message(message)
77
+ end
78
+
79
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
80
+ def send_internal_error(id, e)
81
+ send_response(id, nil, {
82
+ code: ErrorCodes::INTERNAL_ERROR,
83
+ message: <<~EOS,
84
+ #{e.class}: #{e.message}
85
+ #{e.backtrace.join("\n ")}
86
+ EOS
87
+ })
88
+ end
89
+
90
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
91
+ def send_notification(method, params)
92
+ message = { method: method }
93
+ message[:params] = params
94
+ send_message(message)
95
+ end
96
+
97
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
98
+ def send_progress(token, value)
99
+ send_notification("$/progress", token: token, value: value)
100
+ end
101
+
102
+ def supports_work_done_progress?
103
+ @supports_work_done_progress
104
+ end
105
+
106
+ def send_create_work_done_progress_request(token)
107
+ return unless supports_work_done_progress?
108
+ send_request("window/workDoneProgress/create", {
109
+ token: token,
110
+ })
111
+ end
112
+
113
+ def send_work_done_progress_begin(token, title)
114
+ return unless supports_work_done_progress?
115
+ send_progress(token, {
116
+ kind: 'begin',
117
+ title: title,
118
+ cancellable: false,
119
+ percentage: 0,
120
+ })
121
+ end
122
+
123
+ def send_work_done_progress_report(token, message, percentage)
124
+ return unless supports_work_done_progress?
125
+ send_progress(token, {
126
+ kind: 'report',
127
+ message: message,
128
+ cancellable: false,
129
+ percentage: percentage,
130
+ })
131
+ end
132
+
133
+ def send_work_done_progress_end(token, message)
134
+ return unless supports_work_done_progress?
135
+ send_progress(token, {
136
+ kind: 'end',
137
+ message: message,
138
+ })
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ # How you'd use this class:
6
+ #
7
+ # In thread #1:
8
+ # def foo
9
+ # chan = Channel.create
10
+ # send_request(chan.id, ...)
11
+ # result = chan.pop
12
+ # do_stuff_with_result(result)
13
+ # ensure
14
+ # chan.close
15
+ # end
16
+ #
17
+ # In thread #2:
18
+ # Channel.by_id(id) << result
19
+ class Channel
20
+ MUTEX = Mutex.new
21
+ CHANNELS = {}
22
+
23
+ class << self
24
+ def create
25
+ id = new_id
26
+ CHANNELS[id] = new(id)
27
+ CHANNELS[id]
28
+ end
29
+
30
+ def by_id(id)
31
+ CHANNELS[id]
32
+ end
33
+
34
+ def close(id)
35
+ CHANNELS.delete(id)
36
+ end
37
+
38
+ private
39
+
40
+ def new_id
41
+ MUTEX.synchronize do
42
+ @id ||= 0
43
+ @id += 1
44
+ end
45
+ end
46
+ end
47
+
48
+ attr_reader :id
49
+
50
+ def initialize(id)
51
+ @id = id
52
+ @response = SizedQueue.new(1)
53
+ end
54
+
55
+ def pop
56
+ @response.pop
57
+ end
58
+
59
+ def <<(value)
60
+ @response << value
61
+ end
62
+
63
+ def close
64
+ @response.close
65
+ Channel.close(id)
66
+ end
67
+ end
68
+ end
69
+ 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