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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/data/shopify_liquid/filters.yml +1 -0
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/analyzer.rb +29 -5
- data/lib/theme_check/checks/matching_schema_translations.rb +12 -5
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/translation_key_exists.rb +1 -13
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/corrector.rb +40 -3
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +4 -0
- data/lib/theme_check/language_server/bridge.rb +142 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/diagnostics_engine.rb +125 -0
- data/lib/theme_check/language_server/handler.rb +24 -118
- data/lib/theme_check/language_server/io_messenger.rb +104 -0
- data/lib/theme_check/language_server/messenger.rb +27 -0
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +111 -103
- data/lib/theme_check/language_server.rb +6 -1
- data/lib/theme_check/liquid_node.rb +33 -0
- data/lib/theme_check/locale_diff.rb +36 -10
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
- data/lib/theme_check/shopify_liquid/tag.rb +19 -1
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme_file_rewriter.rb +13 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +4 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8f59cffd194662dc9f66474d70310caeec0a713a3029c92efaefbf605ee69bc
|
4
|
+
data.tar.gz: 7bc65dd34a4b387c13cb0395d3d1cec4d60095a7ba2c3e6fa8fc5fa50bea2df8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
@@ -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
|
data/docs/checks/TEMPLATE.md.erb
CHANGED
@@ -1,47 +1,52 @@
|
|
1
1
|
# Check Title (`<%= class_name %>`)
|
2
2
|
|
3
|
-
|
3
|
+
_Version THEME_CHECK_VERSION+_
|
4
4
|
|
5
|
-
|
5
|
+
A short description of what the check does.
|
6
6
|
|
7
|
-
|
7
|
+
A brief paragraph explaining why the check exists (what best practice is it enforcing, and why is it important?).
|
8
8
|
|
9
|
-
|
9
|
+
## Examples
|
10
|
+
|
11
|
+
The following examples contain code snippets that either fail or pass this check.
|
12
|
+
|
13
|
+
### ✗ Fail
|
10
14
|
|
11
15
|
```liquid
|
12
16
|
```
|
13
17
|
|
14
|
-
|
18
|
+
### ✓ Pass
|
15
19
|
|
16
20
|
```liquid
|
17
21
|
```
|
18
22
|
|
19
|
-
##
|
23
|
+
## Options
|
20
24
|
|
21
|
-
The default configuration for this check
|
25
|
+
The following example contains the default configuration for this check:
|
22
26
|
|
23
27
|
```yaml
|
24
28
|
<%= class_name %>:
|
25
|
-
enabled:
|
26
|
-
|
29
|
+
enabled: false
|
30
|
+
severity: suggestion
|
31
|
+
other_option: 10_000
|
27
32
|
```
|
28
33
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
40
|
+
## Disabling this check
|
36
41
|
|
37
|
-
|
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
|
44
|
+
[ This check is disabled by default when <condition>. ]
|
40
45
|
|
41
46
|
## Resources
|
42
47
|
|
43
|
-
- [Rule
|
44
|
-
- [Documentation
|
48
|
+
- [Rule source][codesource]
|
49
|
+
- [Documentation source][docsource]
|
45
50
|
|
46
51
|
[codesource]: /<%= code_source %>
|
47
52
|
[docsource]: /<%= doc_source %>
|
data/lib/theme_check/analyzer.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
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(
|
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
|
-
|
50
|
-
|
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.
|
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
|
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
|
-
|
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
|
@@ -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
|