theme-check 1.6.2 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/data/shopify_liquid/filters.yml +1 -0
  4. data/data/shopify_liquid/tags.yml +9 -9
  5. data/docs/checks/TEMPLATE.md.erb +24 -19
  6. data/exe/theme-check-language-server +0 -4
  7. data/lib/theme_check/analyzer.rb +29 -5
  8. data/lib/theme_check/checks/matching_schema_translations.rb +12 -5
  9. data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
  10. data/lib/theme_check/checks/translation_key_exists.rb +1 -13
  11. data/lib/theme_check/checks/unused_assign.rb +3 -2
  12. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  13. data/lib/theme_check/corrector.rb +40 -3
  14. data/lib/theme_check/exceptions.rb +1 -0
  15. data/lib/theme_check/file_system_storage.rb +4 -0
  16. data/lib/theme_check/language_server/bridge.rb +142 -0
  17. data/lib/theme_check/language_server/channel.rb +69 -0
  18. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
  19. data/lib/theme_check/language_server/diagnostics_engine.rb +125 -0
  20. data/lib/theme_check/language_server/handler.rb +24 -118
  21. data/lib/theme_check/language_server/io_messenger.rb +104 -0
  22. data/lib/theme_check/language_server/messenger.rb +27 -0
  23. data/lib/theme_check/language_server/protocol.rb +4 -0
  24. data/lib/theme_check/language_server/server.rb +111 -103
  25. data/lib/theme_check/language_server.rb +6 -1
  26. data/lib/theme_check/liquid_node.rb +33 -0
  27. data/lib/theme_check/locale_diff.rb +36 -10
  28. data/lib/theme_check/position.rb +4 -4
  29. data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
  30. data/lib/theme_check/shopify_liquid/tag.rb +19 -1
  31. data/lib/theme_check/shopify_liquid.rb +1 -0
  32. data/lib/theme_check/tags.rb +0 -1
  33. data/lib/theme_check/theme_file_rewriter.rb +13 -0
  34. data/lib/theme_check/version.rb +1 -1
  35. data/lib/theme_check.rb +4 -0
  36. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e992338f154cdf5da965bae74af6907af89156170b5bdcf599f8f15b9708bb87
4
- data.tar.gz: 27071bf8d98359410d2486a62502f2e7453465771b932a77a6c27785ba00abbb
3
+ metadata.gz: e8f59cffd194662dc9f66474d70310caeec0a713a3029c92efaefbf605ee69bc
4
+ data.tar.gz: 7bc65dd34a4b387c13cb0395d3d1cec4d60095a7ba2c3e6fa8fc5fa50bea2df8
5
5
  SHA512:
6
- metadata.gz: 33969efb3cc9bd670ce7a15a419e7419824e8e48ab5951e1f527f116e3aec42ca01c214eb50cd0eb1058fd6c5975c353f3b84024b990dcbad3a45f27cbb9380f
7
- data.tar.gz: 410dd3f410c5dd0fe23533eae5716d8bfefec6fd4bc57e68c77dfe14832e7c498854d2dc6728b25467188d887860d97d92747cf915ab1c669f40226b46a371ad
6
+ metadata.gz: 2ed214b8d5abb83dd3b9ede347fc52f6ed4cce4ddc42b2e0879c24e54b21c0ff30055b5ac8b3e45c3fa5da9efc5da5343c168c6cd818af863d5a73e4841b796b
7
+ data.tar.gz: 8777e953d57ae33ce11c8bd527e061567ddc179dad28c612d1c7e6f80dd8a5d2534e786b86d5f55d944695c10c08f34b3aeb26057700b50b7b6642fbf8007290
data/CHANGELOG.md CHANGED
@@ -1,4 +1,41 @@
1
1
 
2
+ v1.8.0 / 2021-11-09
3
+ ===================
4
+
5
+ ## Features
6
+
7
+ **New corrections for the following checks:**
8
+
9
+ * `MissingRequiredTemplateFiles` ([#462](https://github.com/shopify/theme-check/issues/462))
10
+ * `RequiredLayoutThemeObject` ([#484](https://github.com/shopify/theme-check/issues/484))
11
+ * `UnusedAssign` ([#380](https://github.com/shopify/theme-check/issues/380))
12
+
13
+ ## Fixes
14
+
15
+ * Add support for `preload_tag` filter
16
+ * Minor Language Server improvements (close logs) ([#472](https://github.com/shopify/theme-check/issues/472))
17
+
18
+ v1.7.2 / 2021-09-24
19
+ ===================
20
+
21
+ * Fixup a multithreading problem with our IO Messenger (regression from 1.7.1) ([#468](https://github.com/shopify/theme-check/issues/468))
22
+
23
+ v1.7.1 / 2021-09-24
24
+ ===================
25
+
26
+ * Handle Errno::EADDRNOTAVAIL in RemoteAsset ([#465](https://github.com/shopify/theme-check/issues/465))
27
+ * Complete end tags ([#277](https://github.com/shopify/theme-check/issues/277))
28
+ * Do not flag shopify translations as missing or extra ([#407](https://github.com/shopify/theme-check/issues/407))
29
+
30
+ v1.7.0 / 2021-09-20
31
+ ===================
32
+
33
+ ### Features
34
+
35
+ * Handle LSP messages concurrently in the Language Server ([#459](https://github.com/shopify/theme-check/issues/459))
36
+ * Adds progress reporting while checking (:eyes: VS Code status bar)
37
+ * Makes completions work while checking (more noticeable on Windows since ruby is 3x slower on Windows)
38
+
2
39
  v1.6.2 / 2021-09-16
3
40
  ===================
4
41
 
@@ -81,6 +81,7 @@ UrlFilter:
81
81
  - product_img_url
82
82
  - collection_img_url
83
83
  - article_img_url
84
+ - preload_tag
84
85
  JsonFilter:
85
86
  - json
86
87
  ColorFilter:
@@ -2,28 +2,28 @@
2
2
  - assign
3
3
  - break
4
4
  - capture
5
- - case
6
- - comment
5
+ - case: endcase
6
+ - comment: endcomment
7
7
  - continue
8
8
  - cycle
9
9
  - decrement
10
10
  - echo
11
11
  - else
12
12
  - elsif
13
- - for
14
- - form
15
- - if
13
+ - for: endfor
14
+ - form: endform
15
+ - if: endif
16
16
  - ifchanged
17
17
  - increment
18
- - javascript
18
+ - javascript: endjavascript
19
19
  - layout
20
20
  - liquid
21
- - paginate
21
+ - paginate: endpaginate
22
22
  - raw
23
23
  - render
24
- - schema
24
+ - schema: endschema
25
25
  - section
26
- - style
26
+ - style: endstyle
27
27
  - stylesheet
28
28
  - tablerow
29
29
  - unless
@@ -1,47 +1,52 @@
1
1
  # Check Title (`<%= class_name %>`)
2
2
 
3
- A brief paragraph explaining why the check exists.
3
+ _Version THEME_CHECK_VERSION+_
4
4
 
5
- ## Check Details
5
+ A short description of what the check does.
6
6
 
7
- This check is aimed at eliminating ...
7
+ A brief paragraph explaining why the check exists (what best practice is it enforcing, and why is it important?).
8
8
 
9
- :-1: Examples of **incorrect** code for this check:
9
+ ## Examples
10
+
11
+ The following examples contain code snippets that either fail or pass this check.
12
+
13
+ ### &#x2717; Fail
10
14
 
11
15
  ```liquid
12
16
  ```
13
17
 
14
- :+1: Examples of **correct** code for this check:
18
+ ### &#x2713; Pass
15
19
 
16
20
  ```liquid
17
21
  ```
18
22
 
19
- ## Check Options
23
+ ## Options
20
24
 
21
- The default configuration for this check is the following:
25
+ The following example contains the default configuration for this check:
22
26
 
23
27
  ```yaml
24
28
  <%= class_name %>:
25
- enabled: true
26
- some_option: 10
29
+ enabled: false
30
+ severity: suggestion
31
+ other_option: 10_000
27
32
  ```
28
33
 
29
- ### `some_option`
30
-
31
- The `some_option` option (Default: `10`) determines ...
32
-
33
- ## When Not To Use It
34
+ | Parameter | Description |
35
+ | --- | --- |
36
+ | enabled | Whether the check is enabled. |
37
+ | severity | The [severity](https://shopify.developers/themes/tools/theme-check/configuration#check-severity) of the check. |
38
+ | other_option | A description of the option. |
34
39
 
35
- If you don't want to ..., then it's safe to disable this rule.
40
+ ## Disabling this check
36
41
 
37
- ## Version
42
+ [ This check is safe to disable. You might want to disable this check if ... | Disabling this check isn't recommended because ... ].
38
43
 
39
- This check has been introduced in Theme Check THEME_CHECK_VERSION.
44
+ [ This check is disabled by default when <condition>. ]
40
45
 
41
46
  ## Resources
42
47
 
43
- - [Rule Source][codesource]
44
- - [Documentation Source][docsource]
48
+ - [Rule source][codesource]
49
+ - [Documentation source][docsource]
45
50
 
46
51
  [codesource]: /<%= code_source %>
47
52
  [docsource]: /<%= doc_source %>
@@ -3,9 +3,5 @@
3
3
 
4
4
  require 'theme_check'
5
5
 
6
- if ENV["THEME_CHECK_DEBUG"] == "true"
7
- $DEBUG = true
8
- end
9
-
10
6
  status_code = ThemeCheck::LanguageServer.start
11
7
  exit! status_code
@@ -29,19 +29,36 @@ module ThemeCheck
29
29
  @html_checks.flat_map(&:offenses)
30
30
  end
31
31
 
32
+ def json_file_count
33
+ @json_file_count ||= @theme.json.size
34
+ end
35
+
36
+ def liquid_file_count
37
+ @liquid_file_count ||= @theme.liquid.size
38
+ end
39
+
40
+ def total_file_count
41
+ json_file_count + liquid_file_count
42
+ end
43
+
32
44
  def analyze_theme
33
45
  reset
34
46
 
35
47
  liquid_visitor = LiquidVisitor.new(@liquid_checks, @disabled_checks)
36
48
  html_visitor = HtmlVisitor.new(@html_checks)
49
+
37
50
  ThemeCheck.with_liquid_c_disabled do
38
- @theme.liquid.each do |liquid_file|
51
+ @theme.liquid.each_with_index do |liquid_file, i|
52
+ yield(liquid_file.relative_path.to_s, i, total_file_count) if block_given?
39
53
  liquid_visitor.visit_liquid_file(liquid_file)
40
54
  html_visitor.visit_liquid_file(liquid_file)
41
55
  end
42
56
  end
43
57
 
44
- @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
58
+ @theme.json.each_with_index do |json_file, i|
59
+ yield(json_file.relative_path.to_s, liquid_file_count + i, total_file_count) if block_given?
60
+ @json_checks.call(:on_file, json_file)
61
+ end
45
62
 
46
63
  finish
47
64
  end
@@ -53,16 +70,23 @@ module ThemeCheck
53
70
  # Call all checks that run on the whole theme
54
71
  liquid_visitor = LiquidVisitor.new(@liquid_checks.whole_theme, @disabled_checks)
55
72
  html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
56
- @theme.liquid.each do |liquid_file|
73
+ total = total_file_count + files.size
74
+ @theme.liquid.each_with_index do |liquid_file, i|
75
+ yield(liquid_file.relative_path.to_s, i, total) if block_given?
57
76
  liquid_visitor.visit_liquid_file(liquid_file)
58
77
  html_visitor.visit_liquid_file(liquid_file)
59
78
  end
60
- @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
79
+
80
+ @theme.json.each_with_index do |json_file, i|
81
+ yield(json_file.relative_path.to_s, liquid_file_count + i, total) if block_given?
82
+ @json_checks.whole_theme.call(:on_file, json_file)
83
+ end
61
84
 
62
85
  # Call checks that run on a single files, only on specified file
63
86
  liquid_visitor = LiquidVisitor.new(@liquid_checks.single_file, @disabled_checks)
64
87
  html_visitor = HtmlVisitor.new(@html_checks.single_file)
65
- files.each do |theme_file|
88
+ files.each_with_index do |theme_file, i|
89
+ yield(theme_file.relative_path.to_s, total_file_count + i, total) if block_given?
66
90
  if theme_file.liquid?
67
91
  liquid_visitor.visit_liquid_file(theme_file)
68
92
  html_visitor.visit_liquid_file(theme_file)
@@ -7,7 +7,6 @@ module ThemeCheck
7
7
 
8
8
  def on_schema(node)
9
9
  schema = JSON.parse(node.value.nodelist.join)
10
-
11
10
  # Get all locales used in the schema
12
11
  used_locales = Set.new([theme.default_locale])
13
12
  visit_object(schema) do |_, locales|
@@ -19,11 +18,17 @@ module ThemeCheck
19
18
  visit_object(schema) do |key, locales|
20
19
  missing = used_locales - locales
21
20
  if missing.any?
22
- add_offense("#{key} missing translations for #{missing.join(', ')}", node: node)
21
+ add_offense("#{key} missing translations for #{missing.join(', ')}", node: node) do |corrector|
22
+ key = key.split(".")
23
+ missing.each do |language|
24
+ corrector.schema_corrector(schema, key + [language], "TODO")
25
+ end
26
+ corrector.replace_block_body(node, schema)
27
+ end
23
28
  end
24
29
  end
25
30
 
26
- check_locales(schema["locales"], node: node)
31
+ check_locales(schema, node: node)
27
32
 
28
33
  rescue JSON::ParserError
29
34
  # Ignored, handled in ValidSchema.
@@ -31,14 +36,16 @@ module ThemeCheck
31
36
 
32
37
  private
33
38
 
34
- def check_locales(locales, node:)
39
+ def check_locales(schema, node:)
40
+ locales = schema["locales"]
35
41
  return unless locales.is_a?(Hash)
36
42
 
37
43
  default_locale = locales[theme.default_locale]
44
+
38
45
  if default_locale
39
46
  locales.each_pair do |name, content|
40
47
  diff = LocaleDiff.new(default_locale, content)
41
- diff.add_as_offenses(self, key_prefix: ["locales", name], node: node)
48
+ diff.add_as_offenses(self, key_prefix: ["locales", name], node: node, schema: schema)
42
49
  end
43
50
  else
44
51
  add_offense("Missing default locale in key: locales", node: node)
@@ -27,14 +27,19 @@ module ThemeCheck
27
27
  def after_document(node)
28
28
  return unless node.theme_file.name == LAYOUT_FILENAME
29
29
 
30
- add_missing_object_offense("content_for_layout") unless @content_for_layout_found
31
- add_missing_object_offense("content_for_header") unless @content_for_header_found
30
+ add_missing_object_offense("content_for_layout", "</body>") unless @content_for_layout_found
31
+ add_missing_object_offense("content_for_header", "</head>") unless @content_for_header_found
32
32
  end
33
33
 
34
34
  private
35
35
 
36
- def add_missing_object_offense(name)
37
- add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node)
36
+ def add_missing_object_offense(name, tag)
37
+ add_offense("#{LAYOUT_FILENAME} must include {{#{name}}}", node: @layout_theme_node) do
38
+ if @layout_theme_node.source.index(tag)
39
+ @layout_theme_node.source.insert(@layout_theme_node.source.index(tag), " {{ #{name} }}\n ")
40
+ @layout_theme_node.markup = @layout_theme_node.source
41
+ end
42
+ end
38
43
  end
39
44
  end
40
45
  end
@@ -1,17 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- module SystemTranslations
4
- extend self
5
-
6
- def translations
7
- @translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
8
- end
9
-
10
- def include?(key)
11
- translations.include?(key)
12
- end
13
- end
14
-
15
3
  class TranslationKeyExists < LiquidCheck
16
4
  severity :error
17
5
  category :translation
@@ -24,7 +12,7 @@ module ThemeCheck
24
12
  return unless (key_node = node.children.first)
25
13
  return unless key_node.value.is_a?(String)
26
14
 
27
- unless key_exists?(key_node.value) || SystemTranslations.include?(key_node.value)
15
+ unless key_exists?(key_node.value) || ShopifyLiquid::SystemTranslations.include?(key_node.value)
28
16
  add_offense(
29
17
  "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
30
18
  node: node,
@@ -46,8 +46,9 @@ module ThemeCheck
46
46
  @templates.each_pair do |_, info|
47
47
  used = info.collect_used_assigns(@templates)
48
48
  info.assign_nodes.each_pair do |name, node|
49
- unless used.include?(name)
50
- add_offense("`#{name}` is never used", node: node)
49
+ next if used.include?(name)
50
+ add_offense("`#{name}` is never used", node: node) do |corrector|
51
+ corrector.remove(node)
51
52
  end
52
53
  end
53
54
  end
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  def on_end
26
26
  missing_snippets.each do |theme_file|
27
27
  add_offense("This snippet is not used", theme_file: theme_file) do |corrector|
28
- corrector.remove(@theme, theme_file.relative_path.to_s)
28
+ corrector.remove_file(@theme, theme_file.relative_path.to_s)
29
29
  end
30
30
  end
31
31
  end
@@ -14,11 +14,20 @@ module ThemeCheck
14
14
  @theme_file.rewriter.insert_before(node, content)
15
15
  end
16
16
 
17
+ def remove(node)
18
+ @theme_file.rewriter.remove(node)
19
+ end
20
+
17
21
  def replace(node, content)
18
22
  @theme_file.rewriter.replace(node, content)
19
23
  node.markup = content
20
24
  end
21
25
 
26
+ def replace_block_body(node, content)
27
+ content = "\n #{JSON.pretty_generate(content, array_nl: "\n ", object_nl: "\n ")}\n" if content.is_a?(Hash)
28
+ @theme_file.rewriter.replace_body(node, content)
29
+ end
30
+
22
31
  def wrap(node, insert_before, insert_after)
23
32
  @theme_file.rewriter.wrap(node, insert_before, insert_after)
24
33
  end
@@ -28,11 +37,11 @@ module ThemeCheck
28
37
  end
29
38
 
30
39
  def create_default_locale_json(theme)
40
+ create(theme, "locales/#{theme.default_locale}.default.json", {})
31
41
  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
32
- theme.default_locale_json.update_contents({})
33
42
  end
34
43
 
35
- def remove(theme, relative_path)
44
+ def remove_file(theme, relative_path)
36
45
  theme.storage.remove(relative_path)
37
46
  end
38
47
 
@@ -42,12 +51,40 @@ module ThemeCheck
42
51
 
43
52
  def add_default_translation_key(file, key, value)
44
53
  hash = file.content
54
+ add_key(hash, key, value)
55
+ file.update_contents(hash)
56
+ end
57
+
58
+ def remove_key(hash, key)
59
+ key.reduce(hash) do |pointer, token|
60
+ return pointer.delete(token) if token == key.last
61
+ pointer[token]
62
+ end
63
+ end
64
+
65
+ def add_key(hash, key, value)
45
66
  key.reduce(hash) do |pointer, token|
46
67
  return pointer[token] = value if token == key.last
47
68
  pointer[token] = {} unless pointer.key?(token)
48
69
  pointer[token]
49
70
  end
50
- file.update_contents(hash)
71
+ end
72
+
73
+ def schema_corrector(schema, key, value)
74
+ return unless schema.is_a?(Hash)
75
+ key.reduce(schema) do |pointer, token|
76
+ case pointer
77
+ when Array
78
+ pointer.each do |item|
79
+ schema_corrector(item, key.drop(1), value)
80
+ end
81
+
82
+ when Hash
83
+ return pointer[token] = value if token == key.last
84
+ pointer[token] = {} unless pointer.key?(token) || pointer.key?("id")
85
+ pointer[token].nil? && pointer["id"] == token ? pointer : pointer[token]
86
+ end
87
+ end
51
88
  end
52
89
  end
53
90
  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
@@ -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)
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