t-ruby 0.0.1

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,994 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TRuby
6
+ # LSP (Language Server Protocol) Server for T-Ruby
7
+ # Provides IDE integration with autocomplete, diagnostics, and navigation
8
+ class LSPServer
9
+ VERSION = "0.1.0"
10
+
11
+ # LSP Error codes
12
+ module ErrorCodes
13
+ PARSE_ERROR = -32700
14
+ INVALID_REQUEST = -32600
15
+ METHOD_NOT_FOUND = -32601
16
+ INVALID_PARAMS = -32602
17
+ INTERNAL_ERROR = -32603
18
+ SERVER_NOT_INITIALIZED = -32002
19
+ UNKNOWN_ERROR_CODE = -32001
20
+ end
21
+
22
+ # LSP Completion item kinds
23
+ module CompletionItemKind
24
+ TEXT = 1
25
+ METHOD = 2
26
+ FUNCTION = 3
27
+ CONSTRUCTOR = 4
28
+ FIELD = 5
29
+ VARIABLE = 6
30
+ CLASS = 7
31
+ INTERFACE = 8
32
+ MODULE = 9
33
+ PROPERTY = 10
34
+ UNIT = 11
35
+ VALUE = 12
36
+ ENUM = 13
37
+ KEYWORD = 14
38
+ SNIPPET = 15
39
+ COLOR = 16
40
+ FILE = 17
41
+ REFERENCE = 18
42
+ FOLDER = 19
43
+ ENUM_MEMBER = 20
44
+ CONSTANT = 21
45
+ STRUCT = 22
46
+ EVENT = 23
47
+ OPERATOR = 24
48
+ TYPE_PARAMETER = 25
49
+ end
50
+
51
+ # LSP Diagnostic severity
52
+ module DiagnosticSeverity
53
+ ERROR = 1
54
+ WARNING = 2
55
+ INFORMATION = 3
56
+ HINT = 4
57
+ end
58
+
59
+ # Semantic Token Types (LSP 3.16+)
60
+ module SemanticTokenTypes
61
+ NAMESPACE = 0
62
+ TYPE = 1
63
+ CLASS = 2
64
+ ENUM = 3
65
+ INTERFACE = 4
66
+ STRUCT = 5
67
+ TYPE_PARAMETER = 6
68
+ PARAMETER = 7
69
+ VARIABLE = 8
70
+ PROPERTY = 9
71
+ ENUM_MEMBER = 10
72
+ EVENT = 11
73
+ FUNCTION = 12
74
+ METHOD = 13
75
+ MACRO = 14
76
+ KEYWORD = 15
77
+ MODIFIER = 16
78
+ COMMENT = 17
79
+ STRING = 18
80
+ NUMBER = 19
81
+ REGEXP = 20
82
+ OPERATOR = 21
83
+ end
84
+
85
+ # Semantic Token Modifiers (bit flags)
86
+ module SemanticTokenModifiers
87
+ DECLARATION = 0x01
88
+ DEFINITION = 0x02
89
+ READONLY = 0x04
90
+ STATIC = 0x08
91
+ DEPRECATED = 0x10
92
+ ABSTRACT = 0x20
93
+ ASYNC = 0x40
94
+ MODIFICATION = 0x80
95
+ DOCUMENTATION = 0x100
96
+ DEFAULT_LIBRARY = 0x200
97
+ end
98
+
99
+ # Token type names for capability registration
100
+ SEMANTIC_TOKEN_TYPES = %w[
101
+ namespace type class enum interface struct typeParameter
102
+ parameter variable property enumMember event function method
103
+ macro keyword modifier comment string number regexp operator
104
+ ].freeze
105
+
106
+ # Token modifier names
107
+ SEMANTIC_TOKEN_MODIFIERS = %w[
108
+ declaration definition readonly static deprecated
109
+ abstract async modification documentation defaultLibrary
110
+ ].freeze
111
+
112
+ # Built-in types for completion
113
+ BUILT_IN_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
114
+
115
+ # Type keywords for completion
116
+ TYPE_KEYWORDS = %w[type interface def end].freeze
117
+
118
+ def initialize(input: $stdin, output: $stdout)
119
+ @input = input
120
+ @output = output
121
+ @documents = {}
122
+ @initialized = false
123
+ @shutdown_requested = false
124
+ @type_alias_registry = TypeAliasRegistry.new
125
+ end
126
+
127
+ # Main run loop for the LSP server
128
+ def run
129
+ loop do
130
+ message = read_message
131
+ break if message.nil?
132
+
133
+ response = handle_message(message)
134
+ send_response(response) if response
135
+ end
136
+ end
137
+
138
+ # Read a single LSP message from input
139
+ def read_message
140
+ # Read headers
141
+ headers = {}
142
+ loop do
143
+ line = @input.gets
144
+ return nil if line.nil?
145
+
146
+ line = line.strip
147
+ break if line.empty?
148
+
149
+ if line =~ /^([^:]+):\s*(.+)$/
150
+ headers[Regexp.last_match(1)] = Regexp.last_match(2)
151
+ end
152
+ end
153
+
154
+ content_length = headers["Content-Length"]&.to_i
155
+ return nil unless content_length && content_length > 0
156
+
157
+ # Read content
158
+ content = @input.read(content_length)
159
+ return nil if content.nil?
160
+
161
+ JSON.parse(content)
162
+ rescue JSON::ParserError => e
163
+ { "error" => "Parse error: #{e.message}" }
164
+ end
165
+
166
+ # Send a response message
167
+ def send_response(response)
168
+ return if response.nil?
169
+
170
+ content = JSON.generate(response)
171
+ message = "Content-Length: #{content.bytesize}\r\n\r\n#{content}"
172
+ @output.write(message)
173
+ @output.flush
174
+ end
175
+
176
+ # Send a notification (no response expected)
177
+ def send_notification(method, params)
178
+ notification = {
179
+ "jsonrpc" => "2.0",
180
+ "method" => method,
181
+ "params" => params
182
+ }
183
+ send_response(notification)
184
+ end
185
+
186
+ # Handle an incoming message
187
+ def handle_message(message)
188
+ return error_response(nil, ErrorCodes::PARSE_ERROR, "Parse error") if message["error"]
189
+
190
+ method = message["method"]
191
+ params = message["params"] || {}
192
+ id = message["id"]
193
+
194
+ # Check if server is initialized for non-init methods
195
+ if !@initialized && method != "initialize" && method != "exit"
196
+ return error_response(id, ErrorCodes::SERVER_NOT_INITIALIZED, "Server not initialized")
197
+ end
198
+
199
+ result = dispatch_method(method, params, id)
200
+
201
+ # For notifications (no id), don't send a response
202
+ return nil if id.nil?
203
+
204
+ if result.is_a?(Hash) && result[:error]
205
+ error_response(id, result[:error][:code], result[:error][:message])
206
+ else
207
+ success_response(id, result)
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ def dispatch_method(method, params, id)
214
+ case method
215
+ when "initialize"
216
+ handle_initialize(params)
217
+ when "initialized"
218
+ handle_initialized(params)
219
+ when "shutdown"
220
+ handle_shutdown
221
+ when "exit"
222
+ handle_exit
223
+ when "textDocument/didOpen"
224
+ handle_did_open(params)
225
+ when "textDocument/didChange"
226
+ handle_did_change(params)
227
+ when "textDocument/didClose"
228
+ handle_did_close(params)
229
+ when "textDocument/completion"
230
+ handle_completion(params)
231
+ when "textDocument/hover"
232
+ handle_hover(params)
233
+ when "textDocument/definition"
234
+ handle_definition(params)
235
+ when "textDocument/semanticTokens/full"
236
+ handle_semantic_tokens_full(params)
237
+ else
238
+ { error: { code: ErrorCodes::METHOD_NOT_FOUND, message: "Method not found: #{method}" } }
239
+ end
240
+ end
241
+
242
+ # === LSP Lifecycle Methods ===
243
+
244
+ def handle_initialize(params)
245
+ @initialized = true
246
+ @root_uri = params["rootUri"]
247
+ @workspace_folders = params["workspaceFolders"]
248
+
249
+ {
250
+ "capabilities" => {
251
+ "textDocumentSync" => {
252
+ "openClose" => true,
253
+ "change" => 1, # Full sync
254
+ "save" => { "includeText" => true }
255
+ },
256
+ "completionProvider" => {
257
+ "triggerCharacters" => [":", "<", "|", "&"],
258
+ "resolveProvider" => false
259
+ },
260
+ "hoverProvider" => true,
261
+ "definitionProvider" => true,
262
+ "diagnosticProvider" => {
263
+ "interFileDependencies" => false,
264
+ "workspaceDiagnostics" => false
265
+ },
266
+ "semanticTokensProvider" => {
267
+ "legend" => {
268
+ "tokenTypes" => SEMANTIC_TOKEN_TYPES,
269
+ "tokenModifiers" => SEMANTIC_TOKEN_MODIFIERS
270
+ },
271
+ "full" => true,
272
+ "range" => false
273
+ }
274
+ },
275
+ "serverInfo" => {
276
+ "name" => "t-ruby-lsp",
277
+ "version" => VERSION
278
+ }
279
+ }
280
+ end
281
+
282
+ def handle_initialized(_params)
283
+ # Server is now fully initialized
284
+ nil
285
+ end
286
+
287
+ def handle_shutdown
288
+ @shutdown_requested = true
289
+ nil
290
+ end
291
+
292
+ def handle_exit
293
+ exit(@shutdown_requested ? 0 : 1)
294
+ end
295
+
296
+ # === Document Synchronization ===
297
+
298
+ def handle_did_open(params)
299
+ text_document = params["textDocument"]
300
+ uri = text_document["uri"]
301
+ text = text_document["text"]
302
+
303
+ @documents[uri] = {
304
+ text: text,
305
+ version: text_document["version"]
306
+ }
307
+
308
+ # Parse and send diagnostics
309
+ publish_diagnostics(uri, text)
310
+ nil
311
+ end
312
+
313
+ def handle_did_change(params)
314
+ text_document = params["textDocument"]
315
+ uri = text_document["uri"]
316
+ changes = params["contentChanges"]
317
+
318
+ # For full sync, take the last change
319
+ if changes && !changes.empty?
320
+ @documents[uri] = {
321
+ text: changes.last["text"],
322
+ version: text_document["version"]
323
+ }
324
+
325
+ # Re-parse and send diagnostics
326
+ publish_diagnostics(uri, changes.last["text"])
327
+ end
328
+ nil
329
+ end
330
+
331
+ def handle_did_close(params)
332
+ uri = params["textDocument"]["uri"]
333
+ @documents.delete(uri)
334
+
335
+ # Clear diagnostics
336
+ send_notification("textDocument/publishDiagnostics", {
337
+ "uri" => uri,
338
+ "diagnostics" => []
339
+ })
340
+ nil
341
+ end
342
+
343
+ # === Diagnostics ===
344
+
345
+ def publish_diagnostics(uri, text)
346
+ diagnostics = analyze_document(text)
347
+
348
+ send_notification("textDocument/publishDiagnostics", {
349
+ "uri" => uri,
350
+ "diagnostics" => diagnostics
351
+ })
352
+ end
353
+
354
+ def analyze_document(text)
355
+ diagnostics = []
356
+
357
+ # Use ErrorHandler to check for errors
358
+ error_handler = ErrorHandler.new(text)
359
+ errors = error_handler.check
360
+
361
+ errors.each do |error|
362
+ # Parse line number from error message
363
+ if error =~ /^Line (\d+):\s*(.+)$/
364
+ line_num = Regexp.last_match(1).to_i - 1 # LSP uses 0-based line numbers
365
+ message = Regexp.last_match(2)
366
+
367
+ diagnostics << create_diagnostic(line_num, message, DiagnosticSeverity::ERROR)
368
+ end
369
+ end
370
+
371
+ # Additional validation using Parser
372
+ begin
373
+ parser = Parser.new(text)
374
+ result = parser.parse
375
+
376
+ # Validate type aliases
377
+ validate_type_aliases(result[:type_aliases] || [], diagnostics, text)
378
+
379
+ # Validate function types
380
+ validate_functions(result[:functions] || [], diagnostics, text)
381
+ rescue StandardError => e
382
+ diagnostics << create_diagnostic(0, "Parse error: #{e.message}", DiagnosticSeverity::ERROR)
383
+ end
384
+
385
+ diagnostics
386
+ end
387
+
388
+ def validate_type_aliases(type_aliases, diagnostics, text)
389
+ lines = text.split("\n")
390
+ registry = TypeAliasRegistry.new
391
+
392
+ type_aliases.each do |alias_info|
393
+ line_num = find_line_number(lines, /^\s*type\s+#{Regexp.escape(alias_info[:name])}\s*=/)
394
+ next unless line_num
395
+
396
+ begin
397
+ registry.register(alias_info[:name], alias_info[:definition])
398
+ rescue DuplicateTypeAliasError => e
399
+ diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
400
+ rescue CircularTypeAliasError => e
401
+ diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
402
+ end
403
+ end
404
+ end
405
+
406
+ def validate_functions(functions, diagnostics, text)
407
+ lines = text.split("\n")
408
+
409
+ functions.each do |func|
410
+ line_num = find_line_number(lines, /^\s*def\s+#{Regexp.escape(func[:name])}\s*\(/)
411
+ next unless line_num
412
+
413
+ # Validate return type
414
+ if func[:return_type]
415
+ unless valid_type?(func[:return_type])
416
+ diagnostics << create_diagnostic(
417
+ line_num,
418
+ "Unknown return type '#{func[:return_type]}'",
419
+ DiagnosticSeverity::WARNING
420
+ )
421
+ end
422
+ end
423
+
424
+ # Validate parameter types
425
+ func[:params]&.each do |param|
426
+ if param[:type] && !valid_type?(param[:type])
427
+ diagnostics << create_diagnostic(
428
+ line_num,
429
+ "Unknown parameter type '#{param[:type]}' for '#{param[:name]}'",
430
+ DiagnosticSeverity::WARNING
431
+ )
432
+ end
433
+ end
434
+ end
435
+ end
436
+
437
+ def find_line_number(lines, pattern)
438
+ lines.each_with_index do |line, idx|
439
+ return idx if line.match?(pattern)
440
+ end
441
+ nil
442
+ end
443
+
444
+ def valid_type?(type_str)
445
+ return true if type_str.nil?
446
+
447
+ # Handle union types
448
+ if type_str.include?("|")
449
+ return type_str.split("|").map(&:strip).all? { |t| valid_type?(t) }
450
+ end
451
+
452
+ # Handle intersection types
453
+ if type_str.include?("&")
454
+ return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
455
+ end
456
+
457
+ # Handle generic types
458
+ if type_str.include?("<")
459
+ base_type = type_str.split("<").first
460
+ return BUILT_IN_TYPES.include?(base_type) || @type_alias_registry.valid_type?(base_type)
461
+ end
462
+
463
+ BUILT_IN_TYPES.include?(type_str) || @type_alias_registry.valid_type?(type_str)
464
+ end
465
+
466
+ def create_diagnostic(line, message, severity)
467
+ {
468
+ "range" => {
469
+ "start" => { "line" => line, "character" => 0 },
470
+ "end" => { "line" => line, "character" => 1000 }
471
+ },
472
+ "severity" => severity,
473
+ "source" => "t-ruby",
474
+ "message" => message
475
+ }
476
+ end
477
+
478
+ # === Completion ===
479
+
480
+ def handle_completion(params)
481
+ uri = params["textDocument"]["uri"]
482
+ position = params["position"]
483
+
484
+ document = @documents[uri]
485
+ return { "items" => [] } unless document
486
+
487
+ text = document[:text]
488
+ lines = text.split("\n")
489
+ line = lines[position["line"]] || ""
490
+ char_pos = position["character"]
491
+
492
+ # Get the text before cursor
493
+ prefix = line[0...char_pos] || ""
494
+
495
+ completions = []
496
+
497
+ # Context-aware completion
498
+ if prefix =~ /:\s*$/
499
+ # After colon - suggest types
500
+ completions.concat(type_completions)
501
+ elsif prefix =~ /\|\s*$/
502
+ # After pipe - suggest types for union
503
+ completions.concat(type_completions)
504
+ elsif prefix =~ /&\s*$/
505
+ # After ampersand - suggest types for intersection
506
+ completions.concat(type_completions)
507
+ elsif prefix =~ /<\s*$/
508
+ # Inside generic - suggest types
509
+ completions.concat(type_completions)
510
+ elsif prefix =~ /^\s*$/
511
+ # Start of line - suggest keywords
512
+ completions.concat(keyword_completions)
513
+ elsif prefix =~ /^\s*def\s+\w*$/
514
+ # Function definition - no completion needed
515
+ completions = []
516
+ elsif prefix =~ /^\s*type\s+\w*$/
517
+ # Type alias definition - no completion needed
518
+ completions = []
519
+ elsif prefix =~ /^\s*interface\s+\w*$/
520
+ # Interface definition - no completion needed
521
+ completions = []
522
+ else
523
+ # Default - suggest all
524
+ completions.concat(type_completions)
525
+ completions.concat(keyword_completions)
526
+ end
527
+
528
+ # Add document-specific completions
529
+ completions.concat(document_type_completions(text))
530
+
531
+ { "items" => completions }
532
+ end
533
+
534
+ def type_completions
535
+ BUILT_IN_TYPES.map do |type|
536
+ {
537
+ "label" => type,
538
+ "kind" => CompletionItemKind::CLASS,
539
+ "detail" => "Built-in type",
540
+ "documentation" => "T-Ruby built-in type: #{type}"
541
+ }
542
+ end
543
+ end
544
+
545
+ def keyword_completions
546
+ TYPE_KEYWORDS.map do |keyword|
547
+ {
548
+ "label" => keyword,
549
+ "kind" => CompletionItemKind::KEYWORD,
550
+ "detail" => "Keyword",
551
+ "documentation" => keyword_documentation(keyword)
552
+ }
553
+ end
554
+ end
555
+
556
+ def keyword_documentation(keyword)
557
+ case keyword
558
+ when "type"
559
+ "Define a type alias: type AliasName = TypeDefinition"
560
+ when "interface"
561
+ "Define an interface: interface Name ... end"
562
+ when "def"
563
+ "Define a function with type annotations: def name(param: Type): ReturnType"
564
+ when "end"
565
+ "End a block (interface, class, method, etc.)"
566
+ else
567
+ keyword
568
+ end
569
+ end
570
+
571
+ def document_type_completions(text)
572
+ completions = []
573
+ parser = Parser.new(text)
574
+ result = parser.parse
575
+
576
+ # Add type aliases from the document
577
+ (result[:type_aliases] || []).each do |alias_info|
578
+ completions << {
579
+ "label" => alias_info[:name],
580
+ "kind" => CompletionItemKind::CLASS,
581
+ "detail" => "Type alias",
582
+ "documentation" => "type #{alias_info[:name]} = #{alias_info[:definition]}"
583
+ }
584
+ end
585
+
586
+ # Add interfaces from the document
587
+ (result[:interfaces] || []).each do |interface_info|
588
+ completions << {
589
+ "label" => interface_info[:name],
590
+ "kind" => CompletionItemKind::INTERFACE,
591
+ "detail" => "Interface",
592
+ "documentation" => "interface #{interface_info[:name]}"
593
+ }
594
+ end
595
+
596
+ completions
597
+ end
598
+
599
+ # === Hover ===
600
+
601
+ def handle_hover(params)
602
+ uri = params["textDocument"]["uri"]
603
+ position = params["position"]
604
+
605
+ document = @documents[uri]
606
+ return nil unless document
607
+
608
+ text = document[:text]
609
+ lines = text.split("\n")
610
+ line = lines[position["line"]] || ""
611
+ char_pos = position["character"]
612
+
613
+ # Find the word at cursor position
614
+ word = extract_word_at_position(line, char_pos)
615
+ return nil if word.nil? || word.empty?
616
+
617
+ hover_info = get_hover_info(word, text)
618
+ return nil unless hover_info
619
+
620
+ {
621
+ "contents" => {
622
+ "kind" => "markdown",
623
+ "value" => hover_info
624
+ },
625
+ "range" => word_range(position["line"], line, char_pos, word)
626
+ }
627
+ end
628
+
629
+ def extract_word_at_position(line, char_pos)
630
+ return nil if char_pos > line.length
631
+
632
+ # Find word boundaries
633
+ start_pos = char_pos
634
+ end_pos = char_pos
635
+
636
+ # Move start back to word start
637
+ while start_pos > 0 && line[start_pos - 1] =~ /[\w<>]/
638
+ start_pos -= 1
639
+ end
640
+
641
+ # Move end forward to word end
642
+ while end_pos < line.length && line[end_pos] =~ /[\w<>]/
643
+ end_pos += 1
644
+ end
645
+
646
+ return nil if start_pos == end_pos
647
+
648
+ line[start_pos...end_pos]
649
+ end
650
+
651
+ def word_range(line_num, line, char_pos, word)
652
+ start_pos = line.index(word) || char_pos
653
+ end_pos = start_pos + word.length
654
+
655
+ {
656
+ "start" => { "line" => line_num, "character" => start_pos },
657
+ "end" => { "line" => line_num, "character" => end_pos }
658
+ }
659
+ end
660
+
661
+ def get_hover_info(word, text)
662
+ # Check if it's a built-in type
663
+ if BUILT_IN_TYPES.include?(word)
664
+ return "**#{word}** - Built-in T-Ruby type"
665
+ end
666
+
667
+ # Check if it's a type alias
668
+ parser = Parser.new(text)
669
+ result = parser.parse
670
+
671
+ (result[:type_aliases] || []).each do |alias_info|
672
+ if alias_info[:name] == word
673
+ return "**Type Alias**\n\n```ruby\ntype #{alias_info[:name]} = #{alias_info[:definition]}\n```"
674
+ end
675
+ end
676
+
677
+ # Check if it's an interface
678
+ (result[:interfaces] || []).each do |interface_info|
679
+ if interface_info[:name] == word
680
+ members = interface_info[:members].map { |m| " #{m[:name]}: #{m[:type]}" }.join("\n")
681
+ return "**Interface**\n\n```ruby\ninterface #{interface_info[:name]}\n#{members}\nend\n```"
682
+ end
683
+ end
684
+
685
+ # Check if it's a function
686
+ (result[:functions] || []).each do |func|
687
+ if func[:name] == word
688
+ params = func[:params].map { |p| "#{p[:name]}: #{p[:type] || 'untyped'}" }.join(", ")
689
+ return_type = func[:return_type] || "void"
690
+ return "**Function**\n\n```ruby\ndef #{func[:name]}(#{params}): #{return_type}\n```"
691
+ end
692
+ end
693
+
694
+ nil
695
+ end
696
+
697
+ # === Definition ===
698
+
699
+ def handle_definition(params)
700
+ uri = params["textDocument"]["uri"]
701
+ position = params["position"]
702
+
703
+ document = @documents[uri]
704
+ return nil unless document
705
+
706
+ text = document[:text]
707
+ lines = text.split("\n")
708
+ line = lines[position["line"]] || ""
709
+ char_pos = position["character"]
710
+
711
+ word = extract_word_at_position(line, char_pos)
712
+ return nil if word.nil? || word.empty?
713
+
714
+ # Find definition location
715
+ location = find_definition(word, text, uri)
716
+ return nil unless location
717
+
718
+ location
719
+ end
720
+
721
+ def find_definition(word, text, uri)
722
+ lines = text.split("\n")
723
+
724
+ # Search for type alias definition
725
+ lines.each_with_index do |line, idx|
726
+ if line.match?(/^\s*type\s+#{Regexp.escape(word)}\s*=/)
727
+ return {
728
+ "uri" => uri,
729
+ "range" => {
730
+ "start" => { "line" => idx, "character" => 0 },
731
+ "end" => { "line" => idx, "character" => line.length }
732
+ }
733
+ }
734
+ end
735
+ end
736
+
737
+ # Search for interface definition
738
+ lines.each_with_index do |line, idx|
739
+ if line.match?(/^\s*interface\s+#{Regexp.escape(word)}\s*$/)
740
+ return {
741
+ "uri" => uri,
742
+ "range" => {
743
+ "start" => { "line" => idx, "character" => 0 },
744
+ "end" => { "line" => idx, "character" => line.length }
745
+ }
746
+ }
747
+ end
748
+ end
749
+
750
+ # Search for function definition
751
+ lines.each_with_index do |line, idx|
752
+ if line.match?(/^\s*def\s+#{Regexp.escape(word)}\s*\(/)
753
+ return {
754
+ "uri" => uri,
755
+ "range" => {
756
+ "start" => { "line" => idx, "character" => 0 },
757
+ "end" => { "line" => idx, "character" => line.length }
758
+ }
759
+ }
760
+ end
761
+ end
762
+
763
+ nil
764
+ end
765
+
766
+ # === Semantic Tokens ===
767
+
768
+ def handle_semantic_tokens_full(params)
769
+ uri = params["textDocument"]["uri"]
770
+ document = @documents[uri]
771
+ return { "data" => [] } unless document
772
+
773
+ text = document[:text]
774
+ tokens = generate_semantic_tokens(text)
775
+
776
+ { "data" => tokens }
777
+ end
778
+
779
+ def generate_semantic_tokens(text)
780
+ tokens = []
781
+ lines = text.split("\n")
782
+
783
+ # Parse the document to get IR
784
+ parser = Parser.new(text, use_combinator: true)
785
+ parse_result = parser.parse
786
+ ir_program = parser.ir_program
787
+
788
+ # Collect all tokens from parsing
789
+ raw_tokens = []
790
+
791
+ # Process type aliases
792
+ (parse_result[:type_aliases] || []).each do |alias_info|
793
+ lines.each_with_index do |line, line_idx|
794
+ if match = line.match(/^\s*type\s+(#{Regexp.escape(alias_info[:name])})\s*=/)
795
+ # 'type' keyword
796
+ type_pos = line.index("type")
797
+ raw_tokens << [line_idx, type_pos, 4, SemanticTokenTypes::KEYWORD, SemanticTokenModifiers::DECLARATION]
798
+
799
+ # Type name
800
+ name_pos = match.begin(1)
801
+ raw_tokens << [line_idx, name_pos, alias_info[:name].length, SemanticTokenTypes::TYPE, SemanticTokenModifiers::DEFINITION]
802
+
803
+ # Type definition (after =)
804
+ add_type_tokens(raw_tokens, line, line_idx, alias_info[:definition])
805
+ end
806
+ end
807
+ end
808
+
809
+ # Process interfaces
810
+ (parse_result[:interfaces] || []).each do |interface_info|
811
+ lines.each_with_index do |line, line_idx|
812
+ if match = line.match(/^\s*interface\s+(#{Regexp.escape(interface_info[:name])})/)
813
+ # 'interface' keyword
814
+ interface_pos = line.index("interface")
815
+ raw_tokens << [line_idx, interface_pos, 9, SemanticTokenTypes::KEYWORD, SemanticTokenModifiers::DECLARATION]
816
+
817
+ # Interface name
818
+ name_pos = match.begin(1)
819
+ raw_tokens << [line_idx, name_pos, interface_info[:name].length, SemanticTokenTypes::INTERFACE, SemanticTokenModifiers::DEFINITION]
820
+ end
821
+
822
+ # Interface members
823
+ interface_info[:members]&.each do |member|
824
+ if match = line.match(/^\s*(#{Regexp.escape(member[:name])})\s*:\s*/)
825
+ prop_pos = match.begin(1)
826
+ raw_tokens << [line_idx, prop_pos, member[:name].length, SemanticTokenTypes::PROPERTY, 0]
827
+
828
+ # Member type
829
+ add_type_tokens(raw_tokens, line, line_idx, member[:type])
830
+ end
831
+ end
832
+ end
833
+ end
834
+
835
+ # Process functions
836
+ (parse_result[:functions] || []).each do |func|
837
+ lines.each_with_index do |line, line_idx|
838
+ if match = line.match(/^\s*def\s+(#{Regexp.escape(func[:name])})\s*\(/)
839
+ # 'def' keyword
840
+ def_pos = line.index("def")
841
+ raw_tokens << [line_idx, def_pos, 3, SemanticTokenTypes::KEYWORD, 0]
842
+
843
+ # Function name
844
+ name_pos = match.begin(1)
845
+ raw_tokens << [line_idx, name_pos, func[:name].length, SemanticTokenTypes::FUNCTION, SemanticTokenModifiers::DEFINITION]
846
+
847
+ # Parameters
848
+ func[:params]&.each do |param|
849
+ if param_match = line.match(/\b(#{Regexp.escape(param[:name])})\s*(?::\s*)?/)
850
+ param_pos = param_match.begin(1)
851
+ raw_tokens << [line_idx, param_pos, param[:name].length, SemanticTokenTypes::PARAMETER, 0]
852
+
853
+ # Parameter type if present
854
+ if param[:type]
855
+ add_type_tokens(raw_tokens, line, line_idx, param[:type])
856
+ end
857
+ end
858
+ end
859
+
860
+ # Return type
861
+ if func[:return_type]
862
+ add_type_tokens(raw_tokens, line, line_idx, func[:return_type])
863
+ end
864
+ end
865
+ end
866
+ end
867
+
868
+ # Process 'end' keywords
869
+ lines.each_with_index do |line, line_idx|
870
+ if match = line.match(/^\s*(end)\s*$/)
871
+ end_pos = match.begin(1)
872
+ raw_tokens << [line_idx, end_pos, 3, SemanticTokenTypes::KEYWORD, 0]
873
+ end
874
+ end
875
+
876
+ # Sort tokens by line, then by character position
877
+ raw_tokens.sort_by! { |t| [t[0], t[1]] }
878
+
879
+ # Convert to delta encoding
880
+ encode_tokens(raw_tokens)
881
+ end
882
+
883
+ def add_type_tokens(raw_tokens, line, line_idx, type_str)
884
+ return unless type_str
885
+
886
+ # Find position of the type in the line
887
+ pos = line.index(type_str)
888
+ return unless pos
889
+
890
+ # Handle built-in types
891
+ if BUILT_IN_TYPES.include?(type_str)
892
+ raw_tokens << [line_idx, pos, type_str.length, SemanticTokenTypes::TYPE, SemanticTokenModifiers::DEFAULT_LIBRARY]
893
+ return
894
+ end
895
+
896
+ # Handle generic types like Array<String>
897
+ if type_str.include?("<")
898
+ if match = type_str.match(/^(\w+)<(.+)>$/)
899
+ base = match[1]
900
+ base_pos = line.index(base, pos)
901
+ if base_pos
902
+ modifier = BUILT_IN_TYPES.include?(base) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
903
+ raw_tokens << [line_idx, base_pos, base.length, SemanticTokenTypes::TYPE, modifier]
904
+ end
905
+ # Recursively process type arguments
906
+ # (simplified - just mark them as types)
907
+ args = match[2]
908
+ args.split(/[,\s]+/).each do |arg|
909
+ arg = arg.strip.gsub(/[<>]/, '')
910
+ next if arg.empty?
911
+ arg_pos = line.index(arg, pos)
912
+ if arg_pos
913
+ modifier = BUILT_IN_TYPES.include?(arg) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
914
+ raw_tokens << [line_idx, arg_pos, arg.length, SemanticTokenTypes::TYPE, modifier]
915
+ end
916
+ end
917
+ end
918
+ return
919
+ end
920
+
921
+ # Handle union types
922
+ if type_str.include?("|")
923
+ type_str.split("|").map(&:strip).each do |t|
924
+ t_pos = line.index(t, pos)
925
+ if t_pos
926
+ modifier = BUILT_IN_TYPES.include?(t) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
927
+ raw_tokens << [line_idx, t_pos, t.length, SemanticTokenTypes::TYPE, modifier]
928
+ end
929
+ end
930
+ return
931
+ end
932
+
933
+ # Handle intersection types
934
+ if type_str.include?("&")
935
+ type_str.split("&").map(&:strip).each do |t|
936
+ t_pos = line.index(t, pos)
937
+ if t_pos
938
+ modifier = BUILT_IN_TYPES.include?(t) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
939
+ raw_tokens << [line_idx, t_pos, t.length, SemanticTokenTypes::TYPE, modifier]
940
+ end
941
+ end
942
+ return
943
+ end
944
+
945
+ # Simple type
946
+ raw_tokens << [line_idx, pos, type_str.length, SemanticTokenTypes::TYPE, 0]
947
+ end
948
+
949
+ def encode_tokens(raw_tokens)
950
+ encoded = []
951
+ prev_line = 0
952
+ prev_char = 0
953
+
954
+ raw_tokens.each do |token|
955
+ line, char, length, token_type, modifiers = token
956
+
957
+ delta_line = line - prev_line
958
+ delta_char = delta_line == 0 ? char - prev_char : char
959
+
960
+ encoded << delta_line
961
+ encoded << delta_char
962
+ encoded << length
963
+ encoded << token_type
964
+ encoded << modifiers
965
+
966
+ prev_line = line
967
+ prev_char = char
968
+ end
969
+
970
+ encoded
971
+ end
972
+
973
+ # === Response Helpers ===
974
+
975
+ def success_response(id, result)
976
+ {
977
+ "jsonrpc" => "2.0",
978
+ "id" => id,
979
+ "result" => result
980
+ }
981
+ end
982
+
983
+ def error_response(id, code, message)
984
+ {
985
+ "jsonrpc" => "2.0",
986
+ "id" => id,
987
+ "error" => {
988
+ "code" => code,
989
+ "message" => message
990
+ }
991
+ }
992
+ end
993
+ end
994
+ end