typeprof 0.15.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/Gemfile.lock +4 -4
  4. data/exe/typeprof +5 -1
  5. data/lib/typeprof/analyzer.rb +261 -66
  6. data/lib/typeprof/arguments.rb +1 -0
  7. data/lib/typeprof/builtin.rb +30 -22
  8. data/lib/typeprof/cli.rb +22 -2
  9. data/lib/typeprof/code-range.rb +177 -0
  10. data/lib/typeprof/config.rb +43 -18
  11. data/lib/typeprof/container-type.rb +10 -1
  12. data/lib/typeprof/export.rb +199 -20
  13. data/lib/typeprof/import.rb +30 -4
  14. data/lib/typeprof/iseq.rb +504 -200
  15. data/lib/typeprof/lsp.rb +865 -0
  16. data/lib/typeprof/method.rb +17 -13
  17. data/lib/typeprof/type.rb +46 -38
  18. data/lib/typeprof/utils.rb +18 -1
  19. data/lib/typeprof/version.rb +1 -1
  20. data/lib/typeprof.rb +3 -0
  21. data/smoke/array15.rb +1 -1
  22. data/smoke/array6.rb +2 -2
  23. data/smoke/array8.rb +1 -1
  24. data/smoke/block-args2.rb +3 -3
  25. data/smoke/block-args3.rb +4 -4
  26. data/smoke/break2.rb +1 -1
  27. data/smoke/gvar2.rb +0 -3
  28. data/smoke/hash-bot.rb +1 -1
  29. data/smoke/hash4.rb +1 -1
  30. data/smoke/identifier_keywords.rb +17 -0
  31. data/smoke/next2.rb +1 -1
  32. data/smoke/or_raise.rb +18 -0
  33. data/smoke/parameterizedd-self.rb +2 -2
  34. data/smoke/pattern-match1.rb +1 -6
  35. data/smoke/rbs-vars.rb +0 -3
  36. data/testbed/ao.rb +1 -1
  37. data/typeprof-lsp +3 -0
  38. data/typeprof.gemspec +1 -1
  39. data/vscode/.gitignore +5 -0
  40. data/vscode/.vscode/launch.json +16 -0
  41. data/vscode/.vscodeignore +7 -0
  42. data/vscode/README.md +22 -0
  43. data/vscode/development.md +31 -0
  44. data/vscode/package-lock.json +2211 -0
  45. data/vscode/package.json +71 -0
  46. data/vscode/sandbox/test.rb +24 -0
  47. data/vscode/src/extension.ts +285 -0
  48. data/vscode/tsconfig.json +15 -0
  49. metadata +20 -5
@@ -0,0 +1,865 @@
1
+ require "socket"
2
+ require "json"
3
+ require "uri"
4
+
5
+ module TypeProf
6
+ def self.start_lsp_server(config)
7
+ Socket.tcp_server_sockets("localhost", config.lsp_options[:port]) do |servs|
8
+ serv = servs[0].local_address
9
+ $stdout << JSON.generate({
10
+ host: serv.ip_address,
11
+ port: serv.ip_port,
12
+ pid: $$,
13
+ })
14
+ $stdout.flush
15
+
16
+ $stdout = $stderr
17
+
18
+ Socket.accept_loop(servs) do |sock|
19
+ sock.set_encoding("UTF-8")
20
+ begin
21
+ reader = LSP::Reader.new(sock)
22
+ writer = LSP::Writer.new(sock)
23
+ TypeProf::LSP::Server.new(config, reader, writer).run
24
+ ensure
25
+ sock.close
26
+ end
27
+ exit
28
+ end
29
+ end
30
+ end
31
+
32
+ module LSP
33
+ CompletionSession = Struct.new(:results, :row, :start_col_offset)
34
+ class CompletionSession
35
+ def reusable?(other_row, other_start_col_offset)
36
+ other_row == self.row && other_start_col_offset == self.start_col_offset
37
+ end
38
+ end
39
+
40
+ class Text
41
+ class AnalysisToken < Utils::CancelToken
42
+ def initialize
43
+ @timer = Utils::TimerCancelToken.new(1)
44
+ @cancelled = false
45
+ end
46
+
47
+ def cancel
48
+ @cancelled = true
49
+ end
50
+
51
+ def cancelled?
52
+ @timer.cancelled? || @cancelled
53
+ end
54
+ end
55
+
56
+ def initialize(server, uri, text, version)
57
+ @server = server
58
+ @uri = uri
59
+ @text = text
60
+ @version = version
61
+ @sigs = nil
62
+
63
+ @last_analysis_cancel_token = nil
64
+ @analysis_queue = Queue.new
65
+ @analysis_thread = Thread.new do
66
+ loop do
67
+ work = @analysis_queue.pop
68
+ begin
69
+ work.call
70
+ rescue Exception
71
+ puts "Rescued exception:"
72
+ puts $!.full_message
73
+ puts
74
+ end
75
+ end
76
+ end
77
+
78
+ # analyze synchronously to respond the first codeLens request
79
+ res, def_table, caller_table = self.analyze(uri, text)
80
+ on_text_changed_analysis(res, def_table, caller_table)
81
+ end
82
+
83
+ attr_reader :text, :version, :sigs, :caller_table
84
+ attr_accessor :definition_table
85
+
86
+ def lines
87
+ @text.lines
88
+ end
89
+
90
+ def apply_changes(changes, version)
91
+ @definition_table = nil
92
+ text = @text.empty? ? [] : @text.lines
93
+ changes.each do |change|
94
+ case change
95
+ in {
96
+ range: {
97
+ start: { line: start_row, character: start_col },
98
+ end: { line: end_row , character: end_col }
99
+ },
100
+ text: change_text,
101
+ }
102
+ else
103
+ raise
104
+ end
105
+ text << "" if start_row == text.size
106
+ text << "" if end_row == text.size
107
+ if start_row == end_row
108
+ text[start_row][start_col...end_col] = change_text
109
+ else
110
+ text[start_row][start_col..] = ""
111
+ text[end_row][...end_col] = ""
112
+ change_text = change_text.lines
113
+ case change_text.size
114
+ when 0
115
+ text[start_row] += text[end_row]
116
+ text[start_row + 1 .. end_row] = []
117
+ when 1
118
+ text[start_row] += change_text.first + text[end_row]
119
+ text[start_row + 1 .. end_row] = []
120
+ else
121
+ text[start_row] += change_text.shift
122
+ text[end_row].prepend(change_text.pop)
123
+ text[start_row + 1 ... end_row - 1] = change_text
124
+ end
125
+ end
126
+ end
127
+ @text = text.join
128
+ @version = version
129
+
130
+ on_text_changed
131
+ end
132
+
133
+ def new_code_completion_session(row, start_offset, end_offset)
134
+ lines = @text.lines
135
+ lines[row][start_offset, end_offset] = ".__typeprof_lsp_completion"
136
+ tmp_text = lines.join
137
+ res, = analyze(@uri, tmp_text)
138
+ if res && res[:completion]
139
+ results = res[:completion].keys.map do |name|
140
+ {
141
+ label: name,
142
+ kind: 2, # Method
143
+ }
144
+ end
145
+ return CompletionSession.new(results, row, start_offset)
146
+ else
147
+ nil
148
+ end
149
+ end
150
+
151
+ def code_complete(loc, trigger_kind)
152
+ case loc
153
+ in { line: row, character: col }
154
+ end
155
+ unless row < @text.lines.length && col >= 1 && @text.lines[row][0, col] =~ /\.\w*$/
156
+ return nil
157
+ end
158
+ start_offset = $~.begin(0)
159
+ end_offset = $&.size
160
+
161
+ case trigger_kind
162
+ when LSP::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS
163
+ unless @current_completion_session&.reusable?(row, start_offset)
164
+ puts "no reusable completion session but got TRIGGER_FOR_INCOMPLETE_COMPLETIONS"
165
+ @current_completion_session = new_code_completion_session(row, start_offset, end_offset)
166
+ end
167
+ return @current_completion_session.results
168
+ else
169
+ @current_completion_session = new_code_completion_session(row, start_offset, end_offset)
170
+ return @current_completion_session&.results
171
+ end
172
+ end
173
+
174
+ private def locate_arg_index_in_signature_help(node, loc, sig_help)
175
+ case node.type
176
+ when :FCALL
177
+ _mid, args_node = node.children
178
+ when :CALL
179
+ _recv, _mid, args_node = node.children
180
+ end
181
+
182
+ idx = 0
183
+
184
+ if args_node
185
+ arg_nodes = args_node.children.compact
186
+
187
+ arg_indexes = {}
188
+ hash = arg_nodes.pop if arg_nodes.last&.type == :HASH
189
+
190
+ arg_nodes.each_with_index do |node, i|
191
+ # Ingore arguments after rest argument
192
+ break if node.type == :LIST || node.type == :ARGSCAT
193
+
194
+ arg_indexes[i] = ISeq.code_range_from_node(node)
195
+ end
196
+
197
+ # Handle keyword arguments
198
+ if hash
199
+ hash.children.last.children.compact.each_slice(2) do |node1, node2|
200
+ # key: expression
201
+ # ^^^^ ^^^^^^^^^^
202
+ # node1 node2
203
+ key = node1.children.first
204
+ arg_indexes[key] =
205
+ CodeRange.new(
206
+ CodeLocation.new(node1.first_lineno, node1.first_lineno),
207
+ CodeLocation.new(node2.last_lineno, node2.last_lineno),
208
+ )
209
+ end
210
+ end
211
+
212
+ if arg_indexes.size >= 1 && arg_indexes.values.last.last < loc
213
+ # There is the cursor after the last argument: "foo(111, 222,|)"
214
+ idx = arg_indexes.size - 1
215
+ prev_cr = arg_indexes.values.last
216
+ if prev_cr.last.lineno == loc.lineno
217
+ line = @text.lines[prev_cr.last.lineno - 1]
218
+ idx += 1 if line[prev_cr.last.column..loc.column].include?(",")
219
+ end
220
+ else
221
+ # There is the cursor within any argument: "foo(111,|222)" or foo(111, 22|2)"
222
+ prev_cr = nil
223
+ arg_indexes.each do |i, cr|
224
+ idx = sig_help.keys.index(i)
225
+ if loc < cr.first
226
+ break if !prev_cr || prev_cr.last.lineno != loc.lineno
227
+ line = @text.lines[prev_cr.last.lineno - 1]
228
+ idx -= 1 unless line[prev_cr.last.column..loc.column].include?(",")
229
+ break
230
+ end
231
+ break if loc <= cr.last
232
+ prev_cr = cr
233
+ end
234
+ end
235
+ end
236
+
237
+ idx
238
+ end
239
+
240
+ def signature_help(loc, trigger_kind)
241
+ loc = CodeLocation.from_lsp(loc)
242
+
243
+ res, = analyze(@uri, @text, signature_help_loc: loc)
244
+
245
+ if res
246
+ res[:signature_help].filter_map do |sig_str, sig_help, node_id|
247
+ node = ISeq.find_node_by_id(@text, node_id)
248
+ if node && ISeq.code_range_from_node(node).contain_loc?(loc)
249
+ idx = locate_arg_index_in_signature_help(node, loc, sig_help)
250
+
251
+ {
252
+ label: sig_str,
253
+ parameters: sig_help.values.map do |r|
254
+ {
255
+ label: [r.begin, r.end],
256
+ }
257
+ end,
258
+ activeParameter: idx,
259
+ }
260
+ end
261
+ end
262
+ else
263
+ nil
264
+ end
265
+ end
266
+
267
+ def analyze(uri, text, cancel_token: nil, signature_help_loc: nil)
268
+ config = @server.typeprof_config.dup
269
+ path = URI(uri).path
270
+ config.rb_files = [[path, text]]
271
+ config.rbs_files = ["typeprof.rbs"] # XXX
272
+ config.verbose = 0
273
+ config.max_sec = 1
274
+ config.options[:show_errors] = true
275
+ config.options[:show_indicator] = false
276
+ config.options[:lsp] = true
277
+ config.options[:signature_help_loc] = [path, signature_help_loc] if signature_help_loc
278
+
279
+ TypeProf.analyze(config, cancel_token)
280
+ rescue SyntaxError
281
+ end
282
+
283
+ def push_analysis_queue(&work)
284
+ @analysis_queue.push(work)
285
+ end
286
+
287
+ def on_text_changed
288
+ cancel_token = AnalysisToken.new
289
+ @last_analysis_cancel_token&.cancel
290
+ @last_analysis_cancel_token = cancel_token
291
+
292
+ uri = @uri
293
+ text = @text
294
+ self.push_analysis_queue do
295
+ if cancel_token.cancelled?
296
+ next
297
+ end
298
+ res, def_table, caller_table = self.analyze(uri, text, cancel_token: cancel_token)
299
+ unless cancel_token.cancelled?
300
+ on_text_changed_analysis(res, def_table, caller_table)
301
+ end
302
+ end
303
+ end
304
+
305
+ def on_text_changed_analysis(res, definition_table, caller_table)
306
+ @definition_table = definition_table
307
+ @caller_table = caller_table
308
+ return unless res
309
+
310
+ @sigs = []
311
+ res[:sigs].each do |file, lineno, sig_str, rbs_code_range, class_kind, class_name|
312
+ uri0 = "file://" + file
313
+ if @uri == uri0
314
+ command = { title: sig_str }
315
+ if rbs_code_range
316
+ command[:command] = "typeprof.jumpToRBS"
317
+ command[:arguments] = [uri0, { line: lineno - 1, character: 0 }, @server.root_uri + "/" + rbs_code_range[0], rbs_code_range[1].to_lsp]
318
+ else
319
+ command[:command] = "typeprof.createPrototypeRBS"
320
+ command[:arguments] = [class_kind, class_name, sig_str]
321
+ end
322
+ @sigs << {
323
+ range: {
324
+ start: { line: lineno - 1, character: 0 },
325
+ end: { line: lineno - 1, character: 1 },
326
+ },
327
+ command: command,
328
+ }
329
+ end
330
+ end
331
+
332
+ diagnostics = {}
333
+ res[:errors].each do |(file, code_range), msg|
334
+ next unless file
335
+ uri0 = "file://" + file
336
+ diagnostics[uri0] ||= []
337
+ diagnostics[uri0] << {
338
+ range: code_range.to_lsp,
339
+ severity: 1,
340
+ source: "TypeProf",
341
+ message: msg,
342
+ }
343
+ end
344
+
345
+ @server.send_request("workspace/codeLens/refresh")
346
+
347
+ @server.send_notification(
348
+ "textDocument/publishDiagnostics",
349
+ {
350
+ uri: @uri,
351
+ version: version,
352
+ diagnostics: diagnostics[@uri] || [],
353
+ }
354
+ )
355
+ end
356
+ end
357
+
358
+ class Message
359
+ def initialize(server, json)
360
+ @server = server
361
+ @id = json[:id]
362
+ @method = json[:method]
363
+ @params = json[:params]
364
+ end
365
+
366
+ def run
367
+ p [:ignored, @method]
368
+ end
369
+
370
+ def respond(result)
371
+ raise "do not respond to notification" if @id == nil
372
+ @server.send_response(id: @id, result: result)
373
+ end
374
+
375
+ def respond_error(error)
376
+ raise "do not respond to notification" if @id == nil
377
+ @server.send_response(id: @id, error: error)
378
+ end
379
+
380
+ Classes = []
381
+ def self.inherited(klass)
382
+ Classes << klass
383
+ end
384
+
385
+ Table = Hash.new(Message)
386
+ def self.build_table
387
+ Classes.each {|klass| Table[klass::METHOD] = klass }
388
+ end
389
+
390
+ def self.find(method)
391
+ Table[method]
392
+ end
393
+ end
394
+
395
+ module ErrorCodes
396
+ ParseError = -32700
397
+ InvalidRequest = -32600
398
+ MethodNotFound = -32601
399
+ InvalidParams = -32602
400
+ InternalError = -32603
401
+ end
402
+
403
+ class Message::Initialize < Message
404
+ METHOD = "initialize"
405
+ def run
406
+ @server.root_uri = @params[:rootUri]
407
+ pwd = Dir.pwd
408
+ @params[:workspaceFolders]&.each do |folder|
409
+ folder => { uri:, }
410
+ if pwd == URI(uri).path
411
+ @server.root_uri = uri
412
+ end
413
+ end
414
+
415
+ respond(
416
+ capabilities: {
417
+ textDocumentSync: {
418
+ openClose: true,
419
+ change: 2, # Incremental
420
+ },
421
+ completionProvider: {
422
+ triggerCharacters: ["."],
423
+ },
424
+ signatureHelpProvider: {
425
+ triggerCharacters: ["(", ","],
426
+ },
427
+ #codeActionProvider: {
428
+ # codeActionKinds: ["quickfix", "refactor"],
429
+ # resolveProvider: false,
430
+ #},
431
+ codeLensProvider: {
432
+ resolveProvider: true,
433
+ },
434
+ executeCommandProvider: {
435
+ commands: [
436
+ "typeprof.createPrototypeRBS",
437
+ "typeprof.enableSignature",
438
+ "typeprof.disableSignature",
439
+ ],
440
+ },
441
+ definitionProvider: true,
442
+ typeDefinitionProvider: true,
443
+ referencesProvider: true,
444
+ },
445
+ serverInfo: {
446
+ name: "typeprof",
447
+ version: "0.0.0",
448
+ },
449
+ )
450
+
451
+ puts "TypeProf for IDE is started successfully"
452
+ end
453
+ end
454
+
455
+ class Message::Initialized < Message
456
+ METHOD = "initialized"
457
+ def run
458
+ end
459
+ end
460
+
461
+ class Message::Shutdown < Message
462
+ METHOD = "shutdown"
463
+ def run
464
+ respond(nil)
465
+ end
466
+ end
467
+
468
+ class Message::Exit < Message
469
+ METHOD = "exit"
470
+ def run
471
+ exit
472
+ end
473
+ end
474
+
475
+ module Message::Workspace
476
+ end
477
+
478
+ class Message::Workspace::DidChangeWatchedFiles < Message
479
+ METHOD = "workspace/didChangeWatchedFiles"
480
+ def run
481
+ #p "workspace/didChangeWatchedFiles"
482
+ #pp @params
483
+ end
484
+ end
485
+
486
+ class Message::Workspace::ExecuteCommand < Message
487
+ METHOD = "workspace/executeCommand"
488
+ def run
489
+ case @params[:command]
490
+ when "typeprof.enableSignature"
491
+ @server.signature_enabled = true
492
+ @server.send_request("workspace/codeLens/refresh")
493
+ when "typeprof.disableSignature"
494
+ @server.signature_enabled = false
495
+ @server.send_request("workspace/codeLens/refresh")
496
+ when "typeprof.createPrototypeRBS"
497
+ class_kind, class_name, sig_str = @params[:arguments]
498
+ code_range =
499
+ CodeRange.new(
500
+ CodeLocation.new(1, 0),
501
+ CodeLocation.new(1, 0),
502
+ )
503
+ text = []
504
+ text << "#{ class_kind } #{ class_name.join("::") }\n"
505
+ text << " #{ sig_str }\n"
506
+ text << "end\n\n"
507
+ text = text.join
508
+ @server.send_request(
509
+ "workspace/applyEdit",
510
+ edit: {
511
+ changes: {
512
+ @server.root_uri + "/typeprof.rbs" => [
513
+ {
514
+ range: code_range.to_lsp,
515
+ newText: text,
516
+ }
517
+ ],
518
+ },
519
+ },
520
+ ) do |res|
521
+ code_range =
522
+ CodeRange.new(
523
+ CodeLocation.new(1, 0),
524
+ CodeLocation.new(3, 3), # 3 = "end".size
525
+ )
526
+ @server.send_request(
527
+ "window/showDocument",
528
+ uri: @server.root_uri + "/typeprof.rbs",
529
+ takeFocus: true,
530
+ selection: code_range.to_lsp,
531
+ )
532
+ end
533
+ respond(nil)
534
+ else
535
+ respond_error(
536
+ code: ErrorCodes::InvalidRequest,
537
+ message: "Unknown command: #{ @params[:command] }",
538
+ )
539
+ end
540
+ end
541
+ end
542
+
543
+ module Message::TextDocument
544
+ end
545
+
546
+ class Message::TextDocument::DidOpen < Message
547
+ METHOD = "textDocument/didOpen"
548
+ def run
549
+ case @params
550
+ in { textDocument: { uri:, version:, text: } }
551
+ else
552
+ raise
553
+ end
554
+ if uri.start_with?(@server.root_uri)
555
+ @server.open_texts[uri] = Text.new(@server, uri, text, version)
556
+ end
557
+ end
558
+ end
559
+
560
+ class Message::TextDocument::DidChange < Message
561
+ METHOD = "textDocument/didChange"
562
+ def run
563
+ case @params
564
+ in { textDocument: { uri:, version: }, contentChanges: changes }
565
+ else
566
+ raise
567
+ end
568
+ @server.open_texts[uri]&.apply_changes(changes, version)
569
+ end
570
+
571
+ def cancel
572
+ puts "cancel"
573
+ end
574
+ end
575
+
576
+ class Message::TextDocument::DidClose < Message
577
+ METHOD = "textDocument/didClose"
578
+ def run
579
+ case @params
580
+ in { textDocument: { uri: } }
581
+ else
582
+ raise
583
+ end
584
+ @server.open_texts.delete(uri)
585
+ end
586
+ end
587
+
588
+ class Message::TextDocument::Definition < Message
589
+ METHOD = "textDocument/definition"
590
+ def run
591
+ case @params
592
+ in {
593
+ textDocument: { uri:, },
594
+ position: loc,
595
+ }
596
+ else
597
+ raise
598
+ end
599
+
600
+ definition_table = @server.open_texts[uri]&.definition_table
601
+ code_locations = definition_table[CodeLocation.from_lsp(loc)] if definition_table
602
+ if code_locations
603
+ respond(
604
+ code_locations.map do |path, code_range|
605
+ {
606
+ uri: "file://" + path,
607
+ range: code_range.to_lsp,
608
+ }
609
+ end
610
+ )
611
+ else
612
+ respond(nil)
613
+ end
614
+ end
615
+ end
616
+
617
+ class Message::TextDocument::TypeDefinition < Message
618
+ METHOD = "textDocument/typeDefinition"
619
+ def run
620
+ respond(nil)
621
+ # jump example
622
+ #respond(
623
+ # uri: "file:///path/to/typeprof/vscode/sandbox/test.rbs",
624
+ # range: {
625
+ # start: { line: 1, character: 4 },
626
+ # end: { line: 1, character: 7 },
627
+ # },
628
+ #)
629
+ end
630
+ end
631
+
632
+ class Message::TextDocument::References < Message
633
+ METHOD = "textDocument/references"
634
+ def run
635
+ case @params
636
+ in {
637
+ textDocument: { uri:, },
638
+ position: loc,
639
+ }
640
+ else
641
+ raise
642
+ end
643
+
644
+ caller_table = @server.open_texts[uri]&.caller_table
645
+ code_locations = caller_table[CodeLocation.from_lsp(loc)] if caller_table
646
+ if code_locations
647
+ respond(
648
+ code_locations.map do |path, code_range|
649
+ {
650
+ uri: "file://" + path,
651
+ range: code_range.to_lsp,
652
+ }
653
+ end
654
+ )
655
+ else
656
+ respond(nil)
657
+ end
658
+ end
659
+ end
660
+
661
+ module CompletionTriggerKind
662
+ INVOKED = 1
663
+ TRIGGER_CHARACTER = 2
664
+ TRIGGER_FOR_INCOMPLETE_COMPLETIONS = 3
665
+ end
666
+
667
+ class Message::TextDocument::Completion < Message
668
+ METHOD = "textDocument/completion"
669
+ def run
670
+ case @params
671
+ in {
672
+ textDocument: { uri:, },
673
+ position: loc,
674
+ context: {
675
+ triggerKind: trigger_kind
676
+ },
677
+ }
678
+ else
679
+ raise
680
+ end
681
+
682
+ items = @server.open_texts[uri]&.code_complete(loc, trigger_kind)
683
+
684
+ if items
685
+ respond(
686
+ {
687
+ isIncomplete: true,
688
+ items: items
689
+ }
690
+ )
691
+ else
692
+ respond(nil)
693
+ end
694
+ end
695
+ end
696
+
697
+ class Message::TextDocument::SignatureHelp < Message
698
+ METHOD = "textDocument/signatureHelp"
699
+ def run
700
+ case @params
701
+ in {
702
+ textDocument: { uri:, },
703
+ position: loc,
704
+ context: {
705
+ triggerKind: trigger_kind
706
+ },
707
+ }
708
+ else
709
+ raise
710
+ end
711
+
712
+ items = @server.open_texts[uri]&.signature_help(loc, trigger_kind)
713
+
714
+ if items
715
+ respond({
716
+ signatures: items
717
+ })
718
+ else
719
+ respond(nil)
720
+ end
721
+ end
722
+ end
723
+
724
+ class Message::TextDocument::CodeLens < Message
725
+ METHOD = "textDocument/codeLens"
726
+ def run
727
+ case @params
728
+ in { textDocument: { uri: } }
729
+ else
730
+ raise
731
+ end
732
+
733
+ text = @server.open_texts[uri]
734
+ if text && @server.signature_enabled
735
+ # enqueue in the analysis queue because codeLens is order sensitive
736
+ text.push_analysis_queue do
737
+ respond(text.sigs)
738
+ end
739
+ else
740
+ respond(nil)
741
+ end
742
+ end
743
+ end
744
+
745
+ class Message::CancelRequest < Message
746
+ METHOD = "$/cancelRequest"
747
+ def run
748
+ req = @server.running_requests_from_client[@params[:id]]
749
+ #p [:cancel, @params[:id]]
750
+ req.cancel if req.respond_to?(:cancel)
751
+ end
752
+ end
753
+
754
+ Message.build_table
755
+
756
+ class Reader
757
+ class ProtocolError < StandardError
758
+ end
759
+
760
+ def initialize(io)
761
+ @io = io
762
+ end
763
+
764
+ def read
765
+ while line = @io.gets
766
+ line2 = @io.gets
767
+ if line =~ /\AContent-length: (\d+)\r\n\z/i && line2 == "\r\n"
768
+ len = $1.to_i
769
+ json = JSON.parse(@io.read(len), symbolize_names: true)
770
+ yield json
771
+ else
772
+ raise ProtocolError, "LSP broken header"
773
+ end
774
+ end
775
+ end
776
+ end
777
+
778
+ class Writer
779
+ def initialize(io)
780
+ @io = io
781
+ end
782
+
783
+ def write(**json)
784
+ json = JSON.generate(json.merge(jsonrpc: "2.0"))
785
+ @io << "Content-Length: #{ json.bytesize }\r\n\r\n" << json
786
+ end
787
+
788
+ module ErrorCodes
789
+ ParseError = -32700
790
+ InvalidRequest = -32600
791
+ MethodNotFound = -32601
792
+ InvalidParams = -32602
793
+ InternalError = -32603
794
+ end
795
+ end
796
+
797
+ module Helpers
798
+ def pos(line, character)
799
+ { line: line, character: character }
800
+ end
801
+
802
+ def range(s, e)
803
+ { start: s, end: e }
804
+ end
805
+ end
806
+
807
+ class Server
808
+ class Exit < StandardError; end
809
+
810
+ include Helpers
811
+
812
+ def initialize(config, reader, writer)
813
+ @typeprof_config = config
814
+ @reader = reader
815
+ @writer = writer
816
+ @tx_mutex = Mutex.new
817
+ @request_id = 0
818
+ @running_requests_from_client = {}
819
+ @running_requests_from_server = {}
820
+ @open_texts = {}
821
+ @sigs = {} # tmp
822
+ @signature_enabled = true
823
+ end
824
+
825
+ attr_reader :typeprof_config, :open_texts, :sigs, :running_requests_from_client
826
+ attr_accessor :root_uri, :signature_enabled
827
+
828
+ def run
829
+ @reader.read do |json|
830
+ if json[:method]
831
+ # request or notification
832
+ msg = Message.find(json[:method]).new(self, json)
833
+ @running_requests_from_client[json[:id]] = msg if json[:id]
834
+ msg.run
835
+ else
836
+ callback = @running_requests_from_server.delete(json[:id])
837
+ callback&.call(json[:params])
838
+ end
839
+ end
840
+ rescue Exit
841
+ end
842
+
843
+ def send_response(**msg)
844
+ @running_requests_from_client.delete(msg[:id])
845
+ exclusive_write(**msg)
846
+ end
847
+
848
+ def send_notification(method, params = nil)
849
+ exclusive_write(method: method, params: params)
850
+ end
851
+
852
+ def send_request(method, **params, &blk)
853
+ id = @request_id += 1
854
+ @running_requests_from_server[id] = blk
855
+ exclusive_write(id: id, method: method, params: params)
856
+ end
857
+
858
+ def exclusive_write(**json)
859
+ @tx_mutex.synchronize do
860
+ @writer.write(**json)
861
+ end
862
+ end
863
+ end
864
+ end
865
+ end