theme-check 1.6.2 → 1.8.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 (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