theme-check 1.6.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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