typeprof 0.15.3 → 0.20.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.
@@ -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