theme-check 1.7.0 → 1.9.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/.gitignore +1 -0
- data/CHANGELOG.md +49 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +3 -1
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +79 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -25
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +34 -23
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +8 -3
- data/lib/theme_check/html_node.rb +99 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -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/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +80 -0
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +92 -217
- data/lib/theme_check/language_server/io_messenger.rb +112 -0
- data/lib/theme_check/language_server/messenger.rb +12 -42
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +54 -110
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +28 -6
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +48 -10
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- 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/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +31 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
|
@@ -17,7 +17,16 @@ module ThemeCheck
|
|
|
17
17
|
triggerCharacters: ['.', '{{ ', '{% '],
|
|
18
18
|
context: true,
|
|
19
19
|
},
|
|
20
|
+
codeActionProvider: {
|
|
21
|
+
codeActionKinds: CodeActionProvider.all.map(&:kind),
|
|
22
|
+
resolveProvider: false,
|
|
23
|
+
workDoneProgress: false,
|
|
24
|
+
},
|
|
20
25
|
documentLinkProvider: true,
|
|
26
|
+
executeCommandProvider: {
|
|
27
|
+
workDoneProgress: false,
|
|
28
|
+
commands: ExecuteCommandProvider.all.map(&:command),
|
|
29
|
+
},
|
|
21
30
|
textDocumentSync: {
|
|
22
31
|
openClose: true,
|
|
23
32
|
change: TextDocumentSyncKind::FULL,
|
|
@@ -26,68 +35,104 @@ module ThemeCheck
|
|
|
26
35
|
},
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
def initialize(
|
|
30
|
-
@
|
|
31
|
-
@diagnostics_tracker = DiagnosticsTracker.new
|
|
32
|
-
@diagnostics_lock = Mutex.new
|
|
33
|
-
@supports_progress = false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def supports_progress_notifications?
|
|
37
|
-
@supports_progress
|
|
38
|
+
def initialize(bridge)
|
|
39
|
+
@bridge = bridge
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def on_initialize(id, params)
|
|
41
43
|
@root_path = root_path_from_params(params)
|
|
42
|
-
@supports_progress = params.dig('capabilities', 'window', 'workDoneProgress')
|
|
43
44
|
|
|
44
45
|
# Tell the client we don't support anything if there's no rootPath
|
|
45
|
-
return send_response(id, { capabilities: {} }) if @root_path.nil?
|
|
46
|
+
return @bridge.send_response(id, { capabilities: {} }) if @root_path.nil?
|
|
47
|
+
|
|
48
|
+
@client_capabilities = ClientCapabilities.new(params.dig(:capabilities) || {})
|
|
49
|
+
@configuration = Configuration.new(@bridge, @client_capabilities)
|
|
50
|
+
@bridge.supports_work_done_progress = @client_capabilities.supports_work_done_progress?
|
|
46
51
|
@storage = in_memory_storage(@root_path)
|
|
52
|
+
@diagnostics_manager = DiagnosticsManager.new
|
|
47
53
|
@completion_engine = CompletionEngine.new(@storage)
|
|
48
54
|
@document_link_engine = DocumentLinkEngine.new(@storage)
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
@diagnostics_engine = DiagnosticsEngine.new(@storage, @bridge, @diagnostics_manager)
|
|
56
|
+
@execute_command_engine = ExecuteCommandEngine.new
|
|
57
|
+
@execute_command_engine << CorrectionExecuteCommandProvider.new(@storage, @bridge, @diagnostics_manager)
|
|
58
|
+
@execute_command_engine << RunChecksExecuteCommandProvider.new(@diagnostics_engine, @root_path, config_for_path(@root_path))
|
|
59
|
+
@code_action_engine = CodeActionEngine.new(@storage, @diagnostics_manager)
|
|
60
|
+
@bridge.send_response(id, {
|
|
51
61
|
capabilities: CAPABILITIES,
|
|
52
62
|
serverInfo: SERVER_INFO,
|
|
53
63
|
})
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def on_initialized(_id, _params)
|
|
67
|
+
@configuration.fetch
|
|
68
|
+
@configuration.register_did_change_capability
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on_shutdown(id, _params)
|
|
72
|
+
@bridge.send_response(id, nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
56
75
|
def on_exit(_id, _params)
|
|
57
76
|
close!
|
|
58
77
|
end
|
|
59
|
-
alias_method :on_shutdown, :on_exit
|
|
60
78
|
|
|
61
|
-
def
|
|
79
|
+
def on_text_document_did_open(_id, params)
|
|
62
80
|
relative_path = relative_path_from_text_document_uri(params)
|
|
63
|
-
@storage.write(relative_path,
|
|
81
|
+
@storage.write(relative_path, text_document_text(params), text_document_version(params))
|
|
82
|
+
analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_open?
|
|
64
83
|
end
|
|
65
84
|
|
|
66
|
-
def
|
|
85
|
+
def on_text_document_did_change(_id, params)
|
|
67
86
|
relative_path = relative_path_from_text_document_uri(params)
|
|
68
|
-
@storage.write(relative_path,
|
|
87
|
+
@storage.write(relative_path, content_changes_text(params), text_document_version(params))
|
|
88
|
+
analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_change?
|
|
69
89
|
end
|
|
70
90
|
|
|
71
|
-
def
|
|
91
|
+
def on_text_document_did_close(_id, params)
|
|
72
92
|
relative_path = relative_path_from_text_document_uri(params)
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
file_system_content = Pathname.new(text_document_uri(params)).read(mode: 'rb', encoding: 'UTF-8')
|
|
94
|
+
# On close, the file system becomes the source of truth
|
|
95
|
+
@storage.write(relative_path, file_system_content, nil)
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
def on_text_document_did_save(_id, params)
|
|
78
|
-
analyze_and_send_offenses(text_document_uri(params))
|
|
99
|
+
analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_save?
|
|
79
100
|
end
|
|
80
101
|
|
|
81
102
|
def on_text_document_document_link(id, params)
|
|
82
103
|
relative_path = relative_path_from_text_document_uri(params)
|
|
83
|
-
send_response(id, document_links(relative_path))
|
|
104
|
+
@bridge.send_response(id, @document_link_engine.document_links(relative_path))
|
|
84
105
|
end
|
|
85
106
|
|
|
86
107
|
def on_text_document_completion(id, params)
|
|
87
108
|
relative_path = relative_path_from_text_document_uri(params)
|
|
88
|
-
line = params.dig(
|
|
89
|
-
col = params.dig(
|
|
90
|
-
send_response(id, completions(relative_path, line, col))
|
|
109
|
+
line = params.dig(:position, :line)
|
|
110
|
+
col = params.dig(:position, :character)
|
|
111
|
+
@bridge.send_response(id, @completion_engine.completions(relative_path, line, col))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def on_text_document_code_action(id, params)
|
|
115
|
+
absolute_path = text_document_uri(params)
|
|
116
|
+
start_position = range_element(params, :start)
|
|
117
|
+
end_position = range_element(params, :end)
|
|
118
|
+
only_code_action_kinds = params.dig(:context, :only) || []
|
|
119
|
+
@bridge.send_response(id, @code_action_engine.code_actions(
|
|
120
|
+
absolute_path,
|
|
121
|
+
start_position,
|
|
122
|
+
end_position,
|
|
123
|
+
only_code_action_kinds,
|
|
124
|
+
))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def on_workspace_execute_command(id, params)
|
|
128
|
+
@bridge.send_response(id, @execute_command_engine.execute(
|
|
129
|
+
params[:command],
|
|
130
|
+
params[:arguments],
|
|
131
|
+
))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def on_workspace_did_change_configuration(_id, _params)
|
|
135
|
+
@configuration.fetch(force: true)
|
|
91
136
|
end
|
|
92
137
|
|
|
93
138
|
private
|
|
@@ -101,16 +146,16 @@ module ThemeCheck
|
|
|
101
146
|
ignored_patterns: config.ignored_patterns
|
|
102
147
|
)
|
|
103
148
|
|
|
104
|
-
# Turn that into a hash of
|
|
149
|
+
# Turn that into a hash of buffers
|
|
105
150
|
files = fs.files
|
|
106
|
-
.map { |fn| [fn,
|
|
151
|
+
.map { |fn| [fn, fs.read(fn)] }
|
|
107
152
|
.to_h
|
|
108
153
|
|
|
109
|
-
|
|
154
|
+
VersionedInMemoryStorage.new(files, config.root)
|
|
110
155
|
end
|
|
111
156
|
|
|
112
157
|
def text_document_uri(params)
|
|
113
|
-
file_path(params.dig(
|
|
158
|
+
file_path(params.dig(:textDocument, :uri))
|
|
114
159
|
end
|
|
115
160
|
|
|
116
161
|
def relative_path_from_text_document_uri(params)
|
|
@@ -118,8 +163,8 @@ module ThemeCheck
|
|
|
118
163
|
end
|
|
119
164
|
|
|
120
165
|
def root_path_from_params(params)
|
|
121
|
-
root_uri = params[
|
|
122
|
-
root_path = params[
|
|
166
|
+
root_uri = params[:rootUri]
|
|
167
|
+
root_path = params[:rootPath]
|
|
123
168
|
if root_uri
|
|
124
169
|
file_path(root_uri)
|
|
125
170
|
elsif root_path
|
|
@@ -128,11 +173,15 @@ module ThemeCheck
|
|
|
128
173
|
end
|
|
129
174
|
|
|
130
175
|
def text_document_text(params)
|
|
131
|
-
params.dig(
|
|
176
|
+
params.dig(:textDocument, :text)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def text_document_version(params)
|
|
180
|
+
params.dig(:textDocument, :version)
|
|
132
181
|
end
|
|
133
182
|
|
|
134
183
|
def content_changes_text(params)
|
|
135
|
-
params.dig(
|
|
184
|
+
params.dig(:contentChanges, 0, :text)
|
|
136
185
|
end
|
|
137
186
|
|
|
138
187
|
def config_for_path(path)
|
|
@@ -141,195 +190,21 @@ module ThemeCheck
|
|
|
141
190
|
end
|
|
142
191
|
|
|
143
192
|
def analyze_and_send_offenses(absolute_path)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
storage = ThemeCheck::FileSystemStorage.new(
|
|
148
|
-
config.root,
|
|
149
|
-
ignored_patterns: config.ignored_patterns
|
|
193
|
+
@diagnostics_engine.analyze_and_send_offenses(
|
|
194
|
+
absolute_path,
|
|
195
|
+
config_for_path(absolute_path)
|
|
150
196
|
)
|
|
151
|
-
theme = ThemeCheck::Theme.new(storage)
|
|
152
|
-
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
|
153
|
-
|
|
154
|
-
if @diagnostics_tracker.first_run?
|
|
155
|
-
send_work_done_progress_begin(token, "Full theme check")
|
|
156
|
-
log("Checking #{config.root}")
|
|
157
|
-
offenses = nil
|
|
158
|
-
time = Benchmark.measure do
|
|
159
|
-
offenses = analyzer.analyze_theme do |path, i, total|
|
|
160
|
-
send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
|
|
164
|
-
log(end_message)
|
|
165
|
-
send_work_done_progress_end(token, end_message)
|
|
166
|
-
send_diagnostics(offenses)
|
|
167
|
-
else
|
|
168
|
-
# Analyze selected files
|
|
169
|
-
relative_path = Pathname.new(@storage.relative_path(absolute_path))
|
|
170
|
-
file = theme[relative_path]
|
|
171
|
-
# Skip if not a theme file
|
|
172
|
-
if file
|
|
173
|
-
log("Checking #{relative_path}")
|
|
174
|
-
send_work_done_progress_begin(token, "Partial theme check")
|
|
175
|
-
offenses = nil
|
|
176
|
-
time = Benchmark.measure do
|
|
177
|
-
offenses = analyzer.analyze_files([file]) do |path, i, total|
|
|
178
|
-
send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
|
182
|
-
send_work_done_progress_end(token, end_message)
|
|
183
|
-
log(end_message)
|
|
184
|
-
send_diagnostics(offenses, [absolute_path])
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
@diagnostics_lock.unlock
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def completions(relative_path, line, col)
|
|
191
|
-
@completion_engine.completions(relative_path, line, col)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def document_links(relative_path)
|
|
195
|
-
@document_link_engine.document_links(relative_path)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def send_diagnostics(offenses, analyzed_files = nil)
|
|
199
|
-
@diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
|
|
200
|
-
send_diagnostic(path, diagnostic_offenses)
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def send_diagnostic(path, offenses)
|
|
205
|
-
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
|
206
|
-
send_notification('textDocument/publishDiagnostics', {
|
|
207
|
-
uri: file_uri(path),
|
|
208
|
-
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
|
209
|
-
})
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def offense_to_diagnostic(offense)
|
|
213
|
-
diagnostic = {
|
|
214
|
-
code: offense.code_name,
|
|
215
|
-
message: offense.message,
|
|
216
|
-
range: range(offense),
|
|
217
|
-
severity: severity(offense),
|
|
218
|
-
source: "theme-check",
|
|
219
|
-
}
|
|
220
|
-
diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
|
|
221
|
-
diagnostic
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def code_description(offense)
|
|
225
|
-
{
|
|
226
|
-
href: offense.doc,
|
|
227
|
-
}
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def severity(offense)
|
|
231
|
-
case offense.severity
|
|
232
|
-
when :error
|
|
233
|
-
1
|
|
234
|
-
when :suggestion
|
|
235
|
-
2
|
|
236
|
-
when :style
|
|
237
|
-
3
|
|
238
|
-
else
|
|
239
|
-
4
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def range(offense)
|
|
244
|
-
{
|
|
245
|
-
start: {
|
|
246
|
-
line: offense.start_line,
|
|
247
|
-
character: offense.start_column,
|
|
248
|
-
},
|
|
249
|
-
end: {
|
|
250
|
-
line: offense.end_line,
|
|
251
|
-
character: offense.end_column,
|
|
252
|
-
},
|
|
253
|
-
}
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def send_create_work_done_progress_request
|
|
257
|
-
return unless supports_progress_notifications?
|
|
258
|
-
token = nil
|
|
259
|
-
@server.request do |id|
|
|
260
|
-
token = id # we'll reuse the RQID as token
|
|
261
|
-
send_message({
|
|
262
|
-
id: id,
|
|
263
|
-
method: "window/workDoneProgress/create",
|
|
264
|
-
params: {
|
|
265
|
-
token: id,
|
|
266
|
-
},
|
|
267
|
-
})
|
|
268
|
-
end
|
|
269
|
-
token
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def send_work_done_progress_begin(token, title)
|
|
273
|
-
return unless supports_progress_notifications?
|
|
274
|
-
send_progress(token, {
|
|
275
|
-
kind: 'begin',
|
|
276
|
-
title: title,
|
|
277
|
-
cancellable: false,
|
|
278
|
-
percentage: 0,
|
|
279
|
-
})
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def send_work_done_progress_report(token, message, percentage)
|
|
283
|
-
return unless supports_progress_notifications?
|
|
284
|
-
send_progress(token, {
|
|
285
|
-
kind: 'report',
|
|
286
|
-
message: message,
|
|
287
|
-
cancellable: false,
|
|
288
|
-
percentage: percentage,
|
|
289
|
-
})
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def send_work_done_progress_end(token, message)
|
|
293
|
-
return unless supports_progress_notifications?
|
|
294
|
-
send_progress(token, {
|
|
295
|
-
kind: 'end',
|
|
296
|
-
message: message,
|
|
297
|
-
})
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def send_progress(token, value)
|
|
301
|
-
send_notification("$/progress", token: token, value: value)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def send_message(message)
|
|
305
|
-
message[:jsonrpc] = '2.0'
|
|
306
|
-
@server.send_message(message)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def send_response(id, result = nil, error = nil)
|
|
310
|
-
message = { id: id }
|
|
311
|
-
message[:result] = result if result
|
|
312
|
-
message[:error] = error if error
|
|
313
|
-
send_message(message)
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def send_request(method, params = nil)
|
|
317
|
-
@server.request do |id|
|
|
318
|
-
message = { id: id }
|
|
319
|
-
message[:method] = method
|
|
320
|
-
message[:params] = params if params
|
|
321
|
-
send_message(message)
|
|
322
|
-
end
|
|
323
197
|
end
|
|
324
198
|
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
199
|
+
def range_element(params, start_or_end)
|
|
200
|
+
[
|
|
201
|
+
params.dig(:range, start_or_end, :line),
|
|
202
|
+
params.dig(:range, start_or_end, :character),
|
|
203
|
+
]
|
|
329
204
|
end
|
|
330
205
|
|
|
331
206
|
def log(message)
|
|
332
|
-
@
|
|
207
|
+
@bridge.log(message)
|
|
333
208
|
end
|
|
334
209
|
|
|
335
210
|
def close!
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThemeCheck
|
|
4
|
+
module LanguageServer
|
|
5
|
+
class IOMessenger < Messenger
|
|
6
|
+
def self.err_stream
|
|
7
|
+
if ThemeCheck.debug_log_file
|
|
8
|
+
File.open(ThemeCheck.debug_log_file, "w")
|
|
9
|
+
else
|
|
10
|
+
STDERR
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
in_stream: STDIN,
|
|
16
|
+
out_stream: STDOUT,
|
|
17
|
+
err_stream: IOMessenger.err_stream
|
|
18
|
+
)
|
|
19
|
+
validate!([in_stream, out_stream, err_stream])
|
|
20
|
+
|
|
21
|
+
@in = in_stream
|
|
22
|
+
@out = out_stream
|
|
23
|
+
@err = err_stream
|
|
24
|
+
|
|
25
|
+
# Because programming is fun,
|
|
26
|
+
#
|
|
27
|
+
# Ruby on Windows turns \n into \r\n. Which means that \r\n
|
|
28
|
+
# gets turned into \r\r\n. Which means that the protocol
|
|
29
|
+
# breaks on windows unless we turn STDOUT into binary mode.
|
|
30
|
+
#
|
|
31
|
+
# Hours wasted: 9.
|
|
32
|
+
@out.binmode
|
|
33
|
+
|
|
34
|
+
@out.sync = true # do not buffer
|
|
35
|
+
@err.sync = true # do not buffer
|
|
36
|
+
|
|
37
|
+
# Lock for writing, otherwise messages might be interspersed.
|
|
38
|
+
@writer = Mutex.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read_message
|
|
42
|
+
length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
|
|
43
|
+
content = ''
|
|
44
|
+
length_to_read = 2 + length # 2 is the empty line length (\r\n)
|
|
45
|
+
while content.length < length_to_read
|
|
46
|
+
chunk = @in.read(length_to_read - content.length)
|
|
47
|
+
raise DoneStreaming if chunk.nil?
|
|
48
|
+
content += chunk
|
|
49
|
+
end
|
|
50
|
+
content.lstrip!
|
|
51
|
+
rescue IOError
|
|
52
|
+
raise DoneStreaming
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def send_message(message_body)
|
|
56
|
+
@writer.synchronize do
|
|
57
|
+
@out.write("Content-Length: #{message_body.bytesize}\r\n")
|
|
58
|
+
@out.write("\r\n")
|
|
59
|
+
@out.write(message_body)
|
|
60
|
+
@out.flush
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log(message)
|
|
65
|
+
@err.puts(message)
|
|
66
|
+
@err.flush
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def close_input
|
|
70
|
+
@in.close unless @in.closed?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def close_output
|
|
74
|
+
@err.close
|
|
75
|
+
@out.close
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def initial_line
|
|
81
|
+
# Scanning for lines that fit the protocol.
|
|
82
|
+
while true
|
|
83
|
+
initial_line = @in.gets
|
|
84
|
+
# gets returning nil means the stream was closed.
|
|
85
|
+
raise DoneStreaming if initial_line.nil?
|
|
86
|
+
|
|
87
|
+
if initial_line.match(/Content-Length: (\d+)/)
|
|
88
|
+
break
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
initial_line
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def supported_io_classes
|
|
95
|
+
[IO, StringIO]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate!(streams = [])
|
|
99
|
+
streams.each do |stream|
|
|
100
|
+
unless supported_io_classes.find { |klass| stream.is_a?(klass) }
|
|
101
|
+
raise IncompatibleStream, incompatible_stream_message
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def incompatible_stream_message
|
|
107
|
+
'if provided, in_stream, out_stream, and err_stream must be a kind of '\
|
|
108
|
+
"one of the following: #{supported_io_classes.join(', ')}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -3,54 +3,24 @@
|
|
|
3
3
|
module ThemeCheck
|
|
4
4
|
module LanguageServer
|
|
5
5
|
class Messenger
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
@mutex = Mutex.new
|
|
9
|
-
@id = 0
|
|
6
|
+
def send_message
|
|
7
|
+
raise NotImplementedError
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
# # this will block until the JSON rpc loop has an answer
|
|
17
|
-
# token = @server.request do |id|
|
|
18
|
-
# send_create_work_done_progress_request(id, ...)
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# send_create_work_done_begin_notification(token, "...")
|
|
22
|
-
#
|
|
23
|
-
# do_stuff do |file, i, total|
|
|
24
|
-
# send_create_work_done_progress_notification(token, "...")
|
|
25
|
-
# end
|
|
26
|
-
#
|
|
27
|
-
# send_create_work_done_end_notification(token, "...")
|
|
28
|
-
#
|
|
29
|
-
# end
|
|
30
|
-
def request(&block)
|
|
31
|
-
id = @mutex.synchronize { @id += 1 }
|
|
32
|
-
@responses[id] = SizedQueue.new(1)
|
|
33
|
-
|
|
34
|
-
# Execute the block in the parent thread with an ID
|
|
35
|
-
# So that we're able to relinquish control in the right
|
|
36
|
-
# place when we have a response.
|
|
37
|
-
block.call(id)
|
|
38
|
-
|
|
39
|
-
# this call is blocking until we get a response from somewhere
|
|
40
|
-
result = @responses[id].pop
|
|
10
|
+
def read_message
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
41
13
|
|
|
42
|
-
|
|
43
|
-
|
|
14
|
+
def log
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
44
17
|
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
def close_input
|
|
19
|
+
raise NotImplementedError
|
|
47
20
|
end
|
|
48
21
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# response.
|
|
52
|
-
def respond(id, value)
|
|
53
|
-
@responses[id] << value
|
|
22
|
+
def close_output
|
|
23
|
+
raise NotImplementedError
|
|
54
24
|
end
|
|
55
25
|
end
|
|
56
26
|
end
|