theme-check 1.6.2 → 1.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e992338f154cdf5da965bae74af6907af89156170b5bdcf599f8f15b9708bb87
4
- data.tar.gz: 27071bf8d98359410d2486a62502f2e7453465771b932a77a6c27785ba00abbb
3
+ metadata.gz: 943f8cb722f6c19c1c41e494622922e608a99b50e4ad67f2288a2ed99f406ff3
4
+ data.tar.gz: a8fd83f054895fd366bba2512ff6431d8c470bb3e013ff6c6bf9f6be5366bf38
5
5
  SHA512:
6
- metadata.gz: 33969efb3cc9bd670ce7a15a419e7419824e8e48ab5951e1f527f116e3aec42ca01c214eb50cd0eb1058fd6c5975c353f3b84024b990dcbad3a45f27cbb9380f
7
- data.tar.gz: 410dd3f410c5dd0fe23533eae5716d8bfefec6fd4bc57e68c77dfe14832e7c498854d2dc6728b25467188d887860d97d92747cf915ab1c669f40226b46a371ad
6
+ metadata.gz: fb37aed715f2b9f2dd0a8c17f2abf69110d583280d88508ef3f6ee439989c63fe9819025d870cfc1ed64a8deaac19586ac48ab261495e7a371575d9971754980
7
+ data.tar.gz: 64727b7c9684fa094ce498c09a2a3f1dc06ce10fa99aed066af2aa327bd4de70fcc728f76ad127fd08d7b1b6e28b42b06acf96d83b26193814e7eed1b19a2008
data/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
1
 
2
+ v1.7.0 / 2021-09-20
3
+ ===================
4
+
5
+ ### Features
6
+
7
+ * Handle LSP messages concurrently in the Language Server ([#459](https://github.com/shopify/theme-check/issues/459))
8
+ * Adds progress reporting while checking (:eyes: VS Code status bar)
9
+ * Makes completions work while checking (more noticeable on Windows since ruby is 3x slower on Windows)
10
+
2
11
  v1.6.2 / 2021-09-16
3
12
  ===================
4
13
 
@@ -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,6 +7,11 @@ module ThemeCheck
7
7
  class Handler
8
8
  include URIHelper
9
9
 
10
+ SERVER_INFO = {
11
+ name: $PROGRAM_NAME,
12
+ version: ThemeCheck::VERSION,
13
+ }
14
+
10
15
  CAPABILITIES = {
11
16
  completionProvider: {
12
17
  triggerCharacters: ['.', '{{ ', '{% '],
@@ -24,10 +29,17 @@ module ThemeCheck
24
29
  def initialize(server)
25
30
  @server = server
26
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
27
38
  end
28
39
 
29
40
  def on_initialize(id, params)
30
41
  @root_path = root_path_from_params(params)
42
+ @supports_progress = params.dig('capabilities', 'window', 'workDoneProgress')
31
43
 
32
44
  # Tell the client we don't support anything if there's no rootPath
33
45
  return send_response(id, { capabilities: {} }) if @root_path.nil?
@@ -37,6 +49,7 @@ module ThemeCheck
37
49
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
38
50
  send_response(id, {
39
51
  capabilities: CAPABILITIES,
52
+ serverInfo: SERVER_INFO,
40
53
  })
41
54
  end
42
55
 
@@ -128,6 +141,8 @@ module ThemeCheck
128
141
  end
129
142
 
130
143
  def analyze_and_send_offenses(absolute_path)
144
+ return unless @diagnostics_lock.try_lock
145
+ token = send_create_work_done_progress_request
131
146
  config = config_for_path(absolute_path)
132
147
  storage = ThemeCheck::FileSystemStorage.new(
133
148
  config.root,
@@ -137,13 +152,17 @@ module ThemeCheck
137
152
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
138
153
 
139
154
  if @diagnostics_tracker.first_run?
140
- # Analyze the full theme on first run
155
+ send_work_done_progress_begin(token, "Full theme check")
141
156
  log("Checking #{config.root}")
142
157
  offenses = nil
143
158
  time = Benchmark.measure do
144
- offenses = analyzer.analyze_theme
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
145
162
  end
146
- log("Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s")
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)
147
166
  send_diagnostics(offenses)
148
167
  else
149
168
  # Analyze selected files
@@ -152,14 +171,20 @@ module ThemeCheck
152
171
  # Skip if not a theme file
153
172
  if file
154
173
  log("Checking #{relative_path}")
174
+ send_work_done_progress_begin(token, "Partial theme check")
155
175
  offenses = nil
156
176
  time = Benchmark.measure do
157
- offenses = analyzer.analyze_files([file])
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
158
180
  end
159
- log("Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s")
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)
160
184
  send_diagnostics(offenses, [absolute_path])
161
185
  end
162
186
  end
187
+ @diagnostics_lock.unlock
163
188
  end
164
189
 
165
190
  def completions(relative_path, line, col)
@@ -228,9 +253,57 @@ module ThemeCheck
228
253
  }
229
254
  end
230
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
+
231
304
  def send_message(message)
232
305
  message[:jsonrpc] = '2.0'
233
- @server.send_response(message)
306
+ @server.send_message(message)
234
307
  end
235
308
 
236
309
  def send_response(id, result = nil, error = nil)
@@ -240,6 +313,15 @@ module ThemeCheck
240
313
  send_message(message)
241
314
  end
242
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
+ end
324
+
243
325
  def send_notification(method, params)
244
326
  message = { method: method }
245
327
  message[:params] = params
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class Messenger
6
+ def initialize
7
+ @responses = {}
8
+ @mutex = Mutex.new
9
+ @id = 0
10
+ end
11
+
12
+ # Here's how you'd use this:
13
+ #
14
+ # def some_method_that_communicates_both_ways
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
41
+
42
+ # cleanup when done
43
+ @responses.delete(id)
44
+
45
+ # return the response
46
+ result
47
+ end
48
+
49
+ # In the JSONRPC loop, when we find the response to the
50
+ # request, we unblock the thread that made the request with the
51
+ # response.
52
+ def respond(id, value)
53
+ @responses[id] << value
54
+ end
55
+ end
56
+ end
57
+ end
@@ -16,7 +16,8 @@ module ThemeCheck
16
16
  in_stream: STDIN,
17
17
  out_stream: STDOUT,
18
18
  err_stream: STDERR,
19
- should_raise_errors: false
19
+ should_raise_errors: false,
20
+ number_of_threads: 2
20
21
  )
21
22
  validate!([in_stream, out_stream, err_stream])
22
23
 
@@ -37,33 +38,90 @@ module ThemeCheck
37
38
  @out.sync = true # do not buffer
38
39
  @err.sync = true # do not buffer
39
40
 
41
+ # The queue holds the JSON RPC messages
42
+ @queue = Queue.new
43
+
44
+ # The JSON RPC thread pushes messages onto the queue
45
+ @json_rpc_thread = nil
46
+
47
+ # The handler threads read messages from the queue
48
+ @number_of_threads = number_of_threads
49
+ @handlers = []
50
+
51
+ # The messenger permits requests to be made from the handler
52
+ # to the language client and for those messages to be resolved in place.
53
+ @messenger = Messenger.new
54
+
55
+ # The error queue holds blocks the main thread. When filled, we exit the program.
56
+ @error = SizedQueue.new(1)
57
+
40
58
  @should_raise_errors = should_raise_errors
41
59
  end
42
60
 
43
61
  def listen
44
- loop do
45
- process_request
46
-
47
- # support ctrl+c and stuff
48
- rescue SignalException, DoneStreaming
49
- cleanup
50
- return 0
51
-
52
- rescue Exception => e # rubocop:disable Lint/RescueException
53
- raise e if should_raise_errors
54
- log(e)
55
- log(e.backtrace)
56
- return 1
62
+ start_handler_threads
63
+ start_json_rpc_thread
64
+ status_code_from_error(@error.pop)
65
+ rescue SignalException
66
+ 0
67
+ ensure
68
+ cleanup
69
+ end
70
+
71
+ def start_json_rpc_thread
72
+ @json_rpc_thread = Thread.new do
73
+ loop do
74
+ message = read_json_rpc_message
75
+ if message['method'] == 'initialize'
76
+ handle_message(message)
77
+ else
78
+ @queue << message
79
+ end
80
+ rescue Exception => e # rubocop:disable Lint/RescueException
81
+ break @error << e
82
+ end
83
+ end
84
+ end
85
+
86
+ def start_handler_threads
87
+ @number_of_threads.times do
88
+ @handlers << Thread.new do
89
+ loop do
90
+ message = @queue.pop
91
+ break if @queue.closed? && @queue.empty?
92
+ handle_message(message)
93
+ rescue Exception => e # rubocop:disable Lint/RescueException
94
+ break @error << e
95
+ end
96
+ end
57
97
  end
58
98
  end
59
99
 
60
- def send_response(response)
61
- response_body = JSON.dump(response)
62
- log(JSON.pretty_generate(response)) if $DEBUG
100
+ def status_code_from_error(e)
101
+ raise e
102
+
103
+ # support ctrl+c and stuff
104
+ rescue SignalException, DoneStreaming
105
+ 0
106
+
107
+ rescue Exception => e # rubocop:disable Lint/RescueException
108
+ raise e if should_raise_errors
109
+ log(e)
110
+ log(e.backtrace)
111
+ 2
112
+ end
113
+
114
+ def request(&block)
115
+ @messenger.request(&block)
116
+ end
117
+
118
+ def send_message(message)
119
+ message_body = JSON.dump(message)
120
+ log(JSON.pretty_generate(message)) if $DEBUG
63
121
 
64
- @out.write("Content-Length: #{response_body.bytesize}\r\n")
122
+ @out.write("Content-Length: #{message_body.bytesize}\r\n")
65
123
  @out.write("\r\n")
66
- @out.write(response_body)
124
+ @out.write(message_body)
67
125
  @out.flush
68
126
  end
69
127
 
@@ -91,17 +149,23 @@ module ThemeCheck
91
149
  "one of the following: #{supported_io_classes.join(', ')}"
92
150
  end
93
151
 
94
- def process_request
95
- request_body = read_new_content
96
- request_json = JSON.parse(request_body)
97
- log(JSON.pretty_generate(request_json)) if $DEBUG
152
+ def read_json_rpc_message
153
+ message_body = read_new_content
154
+ message_json = JSON.parse(message_body)
155
+ log(JSON.pretty_generate(message_json)) if $DEBUG
156
+ message_json
157
+ end
98
158
 
99
- id = request_json['id']
100
- method_name = request_json['method']
101
- params = request_json['params']
102
- method_name = "on_#{to_snake_case(method_name)}"
159
+ def handle_message(message)
160
+ id = message['id']
161
+ method_name = message['method']
162
+ method_name &&= "on_#{to_snake_case(method_name)}"
163
+ params = message['params']
164
+ result = message['result']
103
165
 
104
- if @handler.respond_to?(method_name)
166
+ if message.key?('result')
167
+ @messenger.respond(id, result)
168
+ elsif @handler.respond_to?(method_name)
105
169
  @handler.send(method_name, id, params)
106
170
  end
107
171
  end
@@ -128,26 +192,27 @@ module ThemeCheck
128
192
  length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
129
193
  content = ''
130
194
  while content.length < length + 2
131
- begin
132
- # Why + 2? Because \r\n
133
- content += @in.read(length + 2)
134
- rescue => e
135
- log(e)
136
- log(e.backtrace)
137
- # We have almost certainly been disconnected from the server
138
- cleanup
139
- raise DoneStreaming
140
- end
195
+ # Why + 2? Because \r\n
196
+ content += @in.read(length + 2)
197
+ raise DoneStreaming if @in.closed?
141
198
  end
142
199
 
143
200
  content
144
201
  end
145
202
 
146
203
  def cleanup
204
+ # Stop listenting to RPC calls
205
+ @in.close unless @in.closed?
206
+ # Wait for rpc loop to close
207
+ @json_rpc_thread&.join if @json_rpc_thread&.alive?
208
+ # Close the queue
209
+ @queue.close unless @queue.closed?
210
+ # Give 10 seconds for the handlers to wrap up what they were
211
+ # doing/emptying the queue. 👀 unit tests.
212
+ @handlers.each { |thread| thread.join(10) if thread.alive? }
213
+ ensure
147
214
  @err.close
148
215
  @out.close
149
- rescue
150
- # I did my best
151
216
  end
152
217
  end
153
218
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "language_server/protocol"
3
+ require_relative "language_server/messenger"
3
4
  require_relative "language_server/constants"
4
5
  require_relative "language_server/uri_helper"
5
6
  require_relative "language_server/handler"
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.6.2"
3
+ VERSION = "1.7.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-16 00:00:00.000000000 Z
11
+ date: 2021-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -217,6 +217,7 @@ files:
217
217
  - lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb
218
218
  - lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb
219
219
  - lib/theme_check/language_server/handler.rb
220
+ - lib/theme_check/language_server/messenger.rb
220
221
  - lib/theme_check/language_server/protocol.rb
221
222
  - lib/theme_check/language_server/server.rb
222
223
  - lib/theme_check/language_server/tokens.rb