• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

type-ruby / t-ruby / 20525264502

26 Dec 2025 03:58PM UTC coverage: 76.169% (+0.1%) from 76.02%
20525264502

Pull #22

github

web-flow
Merge 8e9648dee into a9d85aa44
Pull Request #22: fix: parser improvements for v0.0.40

75 of 82 new or added lines in 6 files covered. (91.46%)

2 existing lines in 2 files now uncovered.

5082 of 6672 relevant lines covered (76.17%)

1195.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.77
/lib/t_ruby/lsp_server.rb
1
# frozen_string_literal: true
2

3
require "json"
1✔
4

5
module TRuby
1✔
6
  # LSP (Language Server Protocol) Server for T-Ruby
7
  # Provides IDE integration with autocomplete, diagnostics, and navigation
8
  class LSPServer
1✔
9
    VERSION = "0.1.0"
1✔
10

11
    # LSP Error codes
12
    module ErrorCodes
1✔
13
      PARSE_ERROR = -32_700
1✔
14
      INVALID_REQUEST = -32_600
1✔
15
      METHOD_NOT_FOUND = -32_601
1✔
16
      INVALID_PARAMS = -32_602
1✔
17
      INTERNAL_ERROR = -32_603
1✔
18
      SERVER_NOT_INITIALIZED = -32_002
1✔
19
      UNKNOWN_ERROR_CODE = -32_001
1✔
20
    end
21

22
    # LSP Completion item kinds
23
    module CompletionItemKind
1✔
24
      TEXT = 1
1✔
25
      METHOD = 2
1✔
26
      FUNCTION = 3
1✔
27
      CONSTRUCTOR = 4
1✔
28
      FIELD = 5
1✔
29
      VARIABLE = 6
1✔
30
      CLASS = 7
1✔
31
      INTERFACE = 8
1✔
32
      MODULE = 9
1✔
33
      PROPERTY = 10
1✔
34
      UNIT = 11
1✔
35
      VALUE = 12
1✔
36
      ENUM = 13
1✔
37
      KEYWORD = 14
1✔
38
      SNIPPET = 15
1✔
39
      COLOR = 16
1✔
40
      FILE = 17
1✔
41
      REFERENCE = 18
1✔
42
      FOLDER = 19
1✔
43
      ENUM_MEMBER = 20
1✔
44
      CONSTANT = 21
1✔
45
      STRUCT = 22
1✔
46
      EVENT = 23
1✔
47
      OPERATOR = 24
1✔
48
      TYPE_PARAMETER = 25
1✔
49
    end
50

51
    # LSP Diagnostic severity
52
    module DiagnosticSeverity
1✔
53
      ERROR = 1
1✔
54
      WARNING = 2
1✔
55
      INFORMATION = 3
1✔
56
      HINT = 4
1✔
57
    end
58

59
    # Semantic Token Types (LSP 3.16+)
60
    module SemanticTokenTypes
1✔
61
      NAMESPACE = 0
1✔
62
      TYPE = 1
1✔
63
      CLASS = 2
1✔
64
      ENUM = 3
1✔
65
      INTERFACE = 4
1✔
66
      STRUCT = 5
1✔
67
      TYPE_PARAMETER = 6
1✔
68
      PARAMETER = 7
1✔
69
      VARIABLE = 8
1✔
70
      PROPERTY = 9
1✔
71
      ENUM_MEMBER = 10
1✔
72
      EVENT = 11
1✔
73
      FUNCTION = 12
1✔
74
      METHOD = 13
1✔
75
      MACRO = 14
1✔
76
      KEYWORD = 15
1✔
77
      MODIFIER = 16
1✔
78
      COMMENT = 17
1✔
79
      STRING = 18
1✔
80
      NUMBER = 19
1✔
81
      REGEXP = 20
1✔
82
      OPERATOR = 21
1✔
83
    end
84

85
    # Semantic Token Modifiers (bit flags)
86
    module SemanticTokenModifiers
1✔
87
      DECLARATION = 0x01
1✔
88
      DEFINITION = 0x02
1✔
89
      READONLY = 0x04
1✔
90
      STATIC = 0x08
1✔
91
      DEPRECATED = 0x10
1✔
92
      ABSTRACT = 0x20
1✔
93
      ASYNC = 0x40
1✔
94
      MODIFICATION = 0x80
1✔
95
      DOCUMENTATION = 0x100
1✔
96
      DEFAULT_LIBRARY = 0x200
1✔
97
    end
98

99
    # Token type names for capability registration
100
    SEMANTIC_TOKEN_TYPES = %w[
1✔
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[
1✔
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
1✔
114

115
    # Type keywords for completion
116
    TYPE_KEYWORDS = %w[type interface def end].freeze
1✔
117

118
    def initialize(input: $stdin, output: $stdout)
1✔
119
      @input = input
34✔
120
      @output = output
34✔
121
      @documents = {}
34✔
122
      @initialized = false
34✔
123
      @shutdown_requested = false
34✔
124
      @type_alias_registry = TypeAliasRegistry.new
34✔
125
    end
126

127
    # Main run loop for the LSP server
128
    def run
1✔
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
1✔
140
      # Read headers
141
      headers = {}
79✔
142
      loop do
79✔
143
        line = @input.gets
157✔
144
        return nil if line.nil?
157✔
145

146
        line = line.strip
156✔
147
        break if line.empty?
156✔
148

149
        if line =~ /^([^:]+):\s*(.+)$/
78✔
150
          headers[Regexp.last_match(1)] = Regexp.last_match(2)
78✔
151
        end
152
      end
153

154
      content_length = headers["Content-Length"]&.to_i
78✔
155
      return nil unless content_length&.positive?
78✔
156

157
      # Read content
158
      content = @input.read(content_length)
78✔
159
      return nil if content.nil?
78✔
160

161
      JSON.parse(content)
78✔
162
    rescue JSON::ParserError => e
163
      { "error" => "Parse error: #{e.message}" }
1✔
164
    end
165

166
    # Send a response message
167
    def send_response(response)
1✔
168
      return if response.nil?
25✔
169

170
      content = JSON.generate(response)
25✔
171
      message = "Content-Length: #{content.bytesize}\r\n\r\n#{content}"
25✔
172
      @output.write(message)
25✔
173
      @output.flush
25✔
174
    end
175

176
    # Send a notification (no response expected)
177
    def send_notification(method, params)
1✔
178
      notification = {
179
        "jsonrpc" => "2.0",
25✔
180
        "method" => method,
181
        "params" => params,
182
      }
183
      send_response(notification)
25✔
184
    end
185

186
    # Handle an incoming message
187
    def handle_message(message)
1✔
188
      return error_response(nil, ErrorCodes::PARSE_ERROR, "Parse error") if message["error"]
80✔
189

190
      method = message["method"]
80✔
191
      params = message["params"] || {}
80✔
192
      id = message["id"]
80✔
193

194
      # Check if server is initialized for non-init methods
195
      if !@initialized && method != "initialize" && method != "exit"
80✔
196
        return error_response(id, ErrorCodes::SERVER_NOT_INITIALIZED, "Server not initialized")
1✔
197
      end
198

199
      result = dispatch_method(method, params, id)
79✔
200

201
      # For notifications (no id), don't send a response
202
      return nil if id.nil?
79✔
203

204
      if result.is_a?(Hash) && result[:error]
53✔
205
        error_response(id, result[:error][:code], result[:error][:message])
1✔
206
      else
207
        success_response(id, result)
52✔
208
      end
209
    end
210

211
    private
1✔
212

213
    def dispatch_method(method, params, _id)
1✔
214
      case method
79✔
215
      when "initialize"
216
        handle_initialize(params)
31✔
217
      when "initialized"
218
        handle_initialized(params)
1✔
219
      when "shutdown"
220
        handle_shutdown
3✔
221
      when "exit"
222
        handle_exit
×
223
      when "textDocument/didOpen"
224
        handle_did_open(params)
21✔
225
      when "textDocument/didChange"
226
        handle_did_change(params)
2✔
227
      when "textDocument/didClose"
228
        handle_did_close(params)
2✔
229
      when "textDocument/completion"
230
        handle_completion(params)
3✔
231
      when "textDocument/hover"
232
        handle_hover(params)
9✔
233
      when "textDocument/definition"
234
        handle_definition(params)
3✔
235
      when "textDocument/semanticTokens/full"
236
        handle_semantic_tokens_full(params)
×
237
      when "textDocument/diagnostic"
238
        handle_diagnostic(params)
3✔
239
      else
240
        { error: { code: ErrorCodes::METHOD_NOT_FOUND, message: "Method not found: #{method}" } }
1✔
241
      end
242
    end
243

244
    # === LSP Lifecycle Methods ===
245

246
    def handle_initialize(params)
1✔
247
      @initialized = true
31✔
248
      @root_uri = params["rootUri"]
31✔
249
      @workspace_folders = params["workspaceFolders"]
31✔
250

251
      {
252
        "capabilities" => {
31✔
253
          "textDocumentSync" => {
254
            "openClose" => true,
255
            "change" => 1, # Full sync
256
            "save" => { "includeText" => true },
257
          },
258
          "completionProvider" => {
259
            "triggerCharacters" => [":", "<", "|", "&"],
260
            "resolveProvider" => false,
261
          },
262
          "hoverProvider" => true,
263
          "definitionProvider" => true,
264
          "diagnosticProvider" => {
265
            "interFileDependencies" => false,
266
            "workspaceDiagnostics" => false,
267
          },
268
          "semanticTokensProvider" => {
269
            "legend" => {
270
              "tokenTypes" => SEMANTIC_TOKEN_TYPES,
271
              "tokenModifiers" => SEMANTIC_TOKEN_MODIFIERS,
272
            },
273
            "full" => true,
274
            "range" => false,
275
          },
276
        },
277
        "serverInfo" => {
278
          "name" => "t-ruby-lsp",
279
          "version" => VERSION,
280
        },
281
      }
282
    end
283

284
    def handle_initialized(_params)
1✔
285
      # Server is now fully initialized
286
      nil
287
    end
288

289
    def handle_shutdown
1✔
290
      @shutdown_requested = true
3✔
291
      nil
292
    end
293

294
    def handle_exit
1✔
295
      exit(@shutdown_requested ? 0 : 1)
×
296
    end
297

298
    # === Document Synchronization ===
299

300
    def handle_did_open(params)
1✔
301
      text_document = params["textDocument"]
21✔
302
      uri = text_document["uri"]
21✔
303
      text = text_document["text"]
21✔
304

305
      @documents[uri] = {
21✔
306
        text: text,
307
        version: text_document["version"],
308
      }
309

310
      # Parse and send diagnostics
311
      publish_diagnostics(uri, text)
21✔
312
      nil
313
    end
314

315
    def handle_did_change(params)
1✔
316
      text_document = params["textDocument"]
2✔
317
      uri = text_document["uri"]
2✔
318
      changes = params["contentChanges"]
2✔
319

320
      # For full sync, take the last change
321
      if changes && !changes.empty?
2✔
322
        @documents[uri] = {
2✔
323
          text: changes.last["text"],
324
          version: text_document["version"],
325
        }
326

327
        # Re-parse and send diagnostics
328
        publish_diagnostics(uri, changes.last["text"])
2✔
329
      end
330
      nil
331
    end
332

333
    def handle_did_close(params)
1✔
334
      uri = params["textDocument"]["uri"]
2✔
335
      @documents.delete(uri)
2✔
336

337
      # Clear diagnostics
338
      send_notification("textDocument/publishDiagnostics", {
2✔
339
                          "uri" => uri,
340
                          "diagnostics" => [],
341
                        })
342
      nil
343
    end
344

345
    # === Diagnostics ===
346

347
    # Handle pull-based diagnostics (LSP 3.17+)
348
    def handle_diagnostic(params)
1✔
349
      uri = params.dig("textDocument", "uri")
3✔
350
      return { "kind" => "full", "items" => [] } unless uri
3✔
351

352
      doc = @documents[uri]
3✔
353
      return { "kind" => "full", "items" => [] } unless doc
3✔
354

355
      text = doc[:text]
2✔
356
      return { "kind" => "full", "items" => [] } unless text
2✔
357

358
      diagnostics = analyze_document(text)
2✔
359
      { "kind" => "full", "items" => diagnostics }
2✔
360
    end
361

362
    def publish_diagnostics(uri, text)
1✔
363
      diagnostics = analyze_document(text)
23✔
364

365
      send_notification("textDocument/publishDiagnostics", {
23✔
366
                          "uri" => uri,
367
                          "diagnostics" => diagnostics,
368
                        })
369
    end
370

371
    def analyze_document(text)
1✔
372
      diagnostics = []
25✔
373

374
      # Use ErrorHandler to check for errors
375
      error_handler = ErrorHandler.new(text)
25✔
376
      errors = error_handler.check
25✔
377

378
      errors.each do |error|
25✔
379
        # Parse line number from error message
380
        next unless error =~ /^Line (\d+):\s*(.+)$/
9✔
381

382
        line_num = Regexp.last_match(1).to_i - 1 # LSP uses 0-based line numbers
9✔
383
        message = Regexp.last_match(2)
9✔
384

385
        diagnostics << create_diagnostic(line_num, message, DiagnosticSeverity::ERROR)
9✔
386
      end
387

388
      # Additional validation using Parser
389
      begin
390
        parser = Parser.new(text)
25✔
391
        result = parser.parse
25✔
392

393
        # Validate type aliases
394
        validate_type_aliases(result[:type_aliases] || [], diagnostics, text)
25✔
395

396
        # Validate function types
397
        validate_functions(result[:functions] || [], diagnostics, text)
25✔
398
      rescue StandardError => e
399
        diagnostics << create_diagnostic(0, "Parse error: #{e.message}", DiagnosticSeverity::ERROR)
×
400
      end
401

402
      diagnostics
25✔
403
    end
404

405
    def validate_type_aliases(type_aliases, diagnostics, text)
1✔
406
      lines = text.split("\n")
25✔
407
      registry = TypeAliasRegistry.new
25✔
408

409
      type_aliases.each do |alias_info|
25✔
410
        line_num = find_line_number(lines, /^\s*type\s+#{Regexp.escape(alias_info[:name])}\s*=/)
5✔
411
        next unless line_num
5✔
412

413
        begin
414
          registry.register(alias_info[:name], alias_info[:definition])
5✔
415
        rescue DuplicateTypeAliasError => e
416
          diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
×
417
        rescue CircularTypeAliasError => e
418
          diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
×
419
        end
420
      end
421
    end
422

423
    def validate_functions(functions, diagnostics, text)
1✔
424
      lines = text.split("\n")
25✔
425

426
      functions.each do |func|
25✔
427
        line_num = find_line_number(lines, /^\s*def\s+#{Regexp.escape(func[:name])}\s*\(/)
24✔
428
        next unless line_num
24✔
429

430
        # Validate return type
431
        if func[:return_type] && !valid_type?(func[:return_type])
24✔
432
          diagnostics << create_diagnostic(
3✔
433
            line_num,
434
            "Unknown return type '#{func[:return_type]}'",
435
            DiagnosticSeverity::WARNING
436
          )
437
        end
438

439
        # Validate parameter types
440
        func[:params]&.each do |param|
24✔
441
          next unless param[:type] && !valid_type?(param[:type])
12✔
442

443
          diagnostics << create_diagnostic(
6✔
444
            line_num,
445
            "Unknown parameter type '#{param[:type]}' for '#{param[:name]}'",
446
            DiagnosticSeverity::WARNING
447
          )
448
        end
449
      end
450
    end
451

452
    def find_line_number(lines, pattern)
1✔
453
      lines.each_with_index do |line, idx|
29✔
454
        return idx if line.match?(pattern)
40✔
455
      end
456
      nil
457
    end
458

459
    def valid_type?(type_str)
1✔
460
      return true if type_str.nil?
36✔
461

462
      # Handle union types
463
      if type_str.include?("|")
36✔
464
        return type_str.split("|").map(&:strip).all? { |t| valid_type?(t) }
×
465
      end
466

467
      # Handle intersection types
468
      if type_str.include?("&")
36✔
469
        return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
×
470
      end
471

472
      # Handle generic types
473
      if type_str.include?("<")
36✔
474
        base_type = type_str.split("<").first
×
475
        return BUILT_IN_TYPES.include?(base_type) || @type_alias_registry.valid_type?(base_type)
×
476
      end
477

478
      BUILT_IN_TYPES.include?(type_str) || @type_alias_registry.valid_type?(type_str)
36✔
479
    end
480

481
    def create_diagnostic(line, message, severity)
1✔
482
      {
483
        "range" => {
18✔
484
          "start" => { "line" => line, "character" => 0 },
485
          "end" => { "line" => line, "character" => 1000 },
486
        },
487
        "severity" => severity,
488
        "source" => "t-ruby",
489
        "message" => message,
490
      }
491
    end
492

493
    # === Completion ===
494

495
    def handle_completion(params)
1✔
496
      uri = params["textDocument"]["uri"]
3✔
497
      position = params["position"]
3✔
498

499
      document = @documents[uri]
3✔
500
      return { "items" => [] } unless document
3✔
501

502
      text = document[:text]
3✔
503
      lines = text.split("\n")
3✔
504
      line = lines[position["line"]] || ""
3✔
505
      char_pos = position["character"]
3✔
506

507
      # Get the text before cursor
508
      prefix = line[0...char_pos] || ""
3✔
509

510
      completions = []
3✔
511

512
      # Context-aware completion
513
      case prefix
3✔
514
      when /:\s*$/
515
        # After colon - suggest types
516
        completions.concat(type_completions)
2✔
517
      when /\|\s*$/
518
        # After pipe - suggest types for union
519
        completions.concat(type_completions)
×
520
      when /&\s*$/
521
        # After ampersand - suggest types for intersection
522
        completions.concat(type_completions)
×
523
      when /<\s*$/
524
        # Inside generic - suggest types
525
        completions.concat(type_completions)
×
526
      when /^\s*$/
527
        # Start of line - suggest keywords
528
        completions.concat(keyword_completions)
1✔
529
      when /^\s*def\s+\w*$/
530
        # Function definition - no completion needed
531
        completions = []
×
532
      when /^\s*type\s+\w*$/
533
        # Type alias definition - no completion needed
534
        completions = []
×
535
      when /^\s*interface\s+\w*$/
536
        # Interface definition - no completion needed
537
        completions = []
×
538
      else
539
        # Default - suggest all
540
        completions.concat(type_completions)
×
541
        completions.concat(keyword_completions)
×
542
      end
543

544
      # Add document-specific completions
545
      completions.concat(document_type_completions(text))
3✔
546

547
      { "items" => completions }
3✔
548
    end
549

550
    def type_completions
1✔
551
      BUILT_IN_TYPES.map do |type|
2✔
552
        {
553
          "label" => type,
16✔
554
          "kind" => CompletionItemKind::CLASS,
555
          "detail" => "Built-in type",
556
          "documentation" => "T-Ruby built-in type: #{type}",
557
        }
558
      end
559
    end
560

561
    def keyword_completions
1✔
562
      TYPE_KEYWORDS.map do |keyword|
1✔
563
        {
564
          "label" => keyword,
4✔
565
          "kind" => CompletionItemKind::KEYWORD,
566
          "detail" => "Keyword",
567
          "documentation" => keyword_documentation(keyword),
568
        }
569
      end
570
    end
571

572
    def keyword_documentation(keyword)
1✔
573
      case keyword
4✔
574
      when "type"
575
        "Define a type alias: type AliasName = TypeDefinition"
1✔
576
      when "interface"
577
        "Define an interface: interface Name ... end"
1✔
578
      when "def"
579
        "Define a function with type annotations: def name(param: Type): ReturnType"
1✔
580
      when "end"
581
        "End a block (interface, class, method, etc.)"
1✔
582
      else
583
        keyword
×
584
      end
585
    end
586

587
    def document_type_completions(text)
1✔
588
      parser = Parser.new(text)
3✔
589
      result = parser.parse
3✔
590

591
      # Add type aliases from the document
592
      completions = (result[:type_aliases] || []).map do |alias_info|
3✔
593
        {
594
          "label" => alias_info[:name],
2✔
595
          "kind" => CompletionItemKind::CLASS,
596
          "detail" => "Type alias",
597
          "documentation" => "type #{alias_info[:name]} = #{alias_info[:definition]}",
598
        }
599
      end
600

601
      # Add interfaces from the document
602
      (result[:interfaces] || []).each do |interface_info|
3✔
603
        completions << {
×
604
          "label" => interface_info[:name],
605
          "kind" => CompletionItemKind::INTERFACE,
606
          "detail" => "Interface",
607
          "documentation" => "interface #{interface_info[:name]}",
608
        }
609
      end
610

611
      completions
3✔
612
    end
613

614
    # === Hover ===
615

616
    def handle_hover(params)
1✔
617
      uri = params["textDocument"]["uri"]
9✔
618
      position = params["position"]
9✔
619

620
      document = @documents[uri]
9✔
621
      return nil unless document
9✔
622

623
      text = document[:text]
8✔
624
      lines = text.split("\n")
8✔
625
      line = lines[position["line"]] || ""
8✔
626
      char_pos = position["character"]
8✔
627

628
      # Find the word at cursor position
629
      word = extract_word_at_position(line, char_pos)
8✔
630
      return nil if word.nil? || word.empty?
8✔
631

632
      hover_info = get_hover_info(word, text)
8✔
633
      return nil unless hover_info
8✔
634

635
      {
636
        "contents" => {
7✔
637
          "kind" => "markdown",
638
          "value" => hover_info,
639
        },
640
        "range" => word_range(position["line"], line, char_pos, word),
641
      }
642
    end
643

644
    def extract_word_at_position(line, char_pos)
1✔
645
      return nil if char_pos > line.length
11✔
646

647
      # Find word boundaries
648
      start_pos = char_pos
11✔
649
      end_pos = char_pos
11✔
650

651
      # Move start back to word start
652
      start_pos -= 1 while start_pos.positive? && line[start_pos - 1] =~ /[\w<>]/
11✔
653

654
      # Move end forward to word end
655
      end_pos += 1 while end_pos < line.length && line[end_pos] =~ /[\w<>]/
11✔
656

657
      return nil if start_pos == end_pos
11✔
658

659
      line[start_pos...end_pos]
11✔
660
    end
661

662
    def word_range(line_num, line, char_pos, word)
1✔
663
      start_pos = line.index(word) || char_pos
7✔
664
      end_pos = start_pos + word.length
7✔
665

666
      {
667
        "start" => { "line" => line_num, "character" => start_pos },
7✔
668
        "end" => { "line" => line_num, "character" => end_pos },
669
      }
670
    end
671

672
    def get_hover_info(word, text)
1✔
673
      # Check if it's a built-in type
674
      if BUILT_IN_TYPES.include?(word)
8✔
675
        return "**#{word}** - Built-in T-Ruby type"
1✔
676
      end
677

678
      # Check if it's a type alias
679
      parser = Parser.new(text)
7✔
680
      result = parser.parse
7✔
681

682
      (result[:type_aliases] || []).each do |alias_info|
7✔
683
        if alias_info[:name] == word
1✔
684
          return "**Type Alias**\n\n```ruby\ntype #{alias_info[:name]} = #{alias_info[:definition]}\n```"
1✔
685
        end
686
      end
687

688
      # Check if it's an interface
689
      (result[:interfaces] || []).each do |interface_info|
6✔
690
        if interface_info[:name] == word
1✔
691
          members = interface_info[:members].map { |m| "  #{m[:name]}: #{m[:type]}" }.join("\n")
2✔
692
          return "**Interface**\n\n```ruby\ninterface #{interface_info[:name]}\n#{members}\nend\n```"
1✔
693
        end
694
      end
695

696
      # Check if it's a function
697
      (result[:functions] || []).each do |func|
5✔
698
        next unless func[:name] == word
4✔
699

700
        params = func[:params].map { |p| "#{p[:name]}: #{p[:type] || "untyped"}" }.join(", ")
7✔
701
        return_type = func[:return_type] || "void"
4✔
702
        return "**Function**\n\n```ruby\ndef #{func[:name]}(#{params}): #{return_type}\n```"
4✔
703
      end
704

705
      nil
706
    end
707

708
    # === Definition ===
709

710
    def handle_definition(params)
1✔
711
      uri = params["textDocument"]["uri"]
3✔
712
      position = params["position"]
3✔
713

714
      document = @documents[uri]
3✔
715
      return nil unless document
3✔
716

717
      text = document[:text]
3✔
718
      lines = text.split("\n")
3✔
719
      line = lines[position["line"]] || ""
3✔
720
      char_pos = position["character"]
3✔
721

722
      word = extract_word_at_position(line, char_pos)
3✔
723
      return nil if word.nil? || word.empty?
3✔
724

725
      # Find definition location
726
      location = find_definition(word, text, uri)
3✔
727
      return nil unless location
3✔
728

729
      location
3✔
730
    end
731

732
    def find_definition(word, text, uri)
1✔
733
      lines = text.split("\n")
3✔
734

735
      # Search for type alias definition
736
      lines.each_with_index do |line, idx|
3✔
737
        if line.match?(/^\s*type\s+#{Regexp.escape(word)}\s*=/)
3✔
738
          return {
739
            "uri" => uri,
1✔
740
            "range" => {
741
              "start" => { "line" => idx, "character" => 0 },
742
              "end" => { "line" => idx, "character" => line.length },
743
            },
744
          }
745
        end
746

747
        # Search for interface definition
748
        if line.match?(/^\s*interface\s+#{Regexp.escape(word)}\s*$/)
2✔
749
          return {
750
            "uri" => uri,
1✔
751
            "range" => {
752
              "start" => { "line" => idx, "character" => 0 },
753
              "end" => { "line" => idx, "character" => line.length },
754
            },
755
          }
756
        end
757

758
        # Search for function definition
759
        if line.match?(/^\s*def\s+#{Regexp.escape(word)}\s*\(/)
1✔
760
          return {
761
            "uri" => uri,
1✔
762
            "range" => {
763
              "start" => { "line" => idx, "character" => 0 },
764
              "end" => { "line" => idx, "character" => line.length },
765
            },
766
          }
767
        end
768
      end
769

770
      nil
771
    end
772

773
    # === Semantic Tokens ===
774

775
    def handle_semantic_tokens_full(params)
1✔
776
      uri = params["textDocument"]["uri"]
×
777
      document = @documents[uri]
×
778
      return { "data" => [] } unless document
×
779

780
      text = document[:text]
×
781
      tokens = generate_semantic_tokens(text)
×
782

783
      { "data" => tokens }
×
784
    end
785

786
    def generate_semantic_tokens(text)
1✔
787
      lines = text.split("\n")
×
788

789
      # Parse the document to get IR
NEW
790
      parser = Parser.new(text)
×
791
      parse_result = parser.parse
×
792
      parser.ir_program
×
793

794
      # Collect all tokens from parsing
795
      raw_tokens = []
×
796

797
      # Process type aliases
798
      (parse_result[:type_aliases] || []).each do |alias_info|
×
799
        lines.each_with_index do |line, line_idx|
×
800
          next unless (match = line.match(/^\s*type\s+(#{Regexp.escape(alias_info[:name])})\s*=/))
×
801

802
          # 'type' keyword
803
          type_pos = line.index("type")
×
804
          raw_tokens << [line_idx, type_pos, 4, SemanticTokenTypes::KEYWORD, SemanticTokenModifiers::DECLARATION]
×
805

806
          # Type name
807
          name_pos = match.begin(1)
×
808
          raw_tokens << [line_idx, name_pos, alias_info[:name].length, SemanticTokenTypes::TYPE, SemanticTokenModifiers::DEFINITION]
×
809

810
          # Type definition (after =)
811
          add_type_tokens(raw_tokens, line, line_idx, alias_info[:definition])
×
812
        end
813
      end
814

815
      # Process interfaces
816
      (parse_result[:interfaces] || []).each do |interface_info|
×
817
        lines.each_with_index do |line, line_idx|
×
818
          if (match = line.match(/^\s*interface\s+(#{Regexp.escape(interface_info[:name])})/))
×
819
            # 'interface' keyword
820
            interface_pos = line.index("interface")
×
821
            raw_tokens << [line_idx, interface_pos, 9, SemanticTokenTypes::KEYWORD, SemanticTokenModifiers::DECLARATION]
×
822

823
            # Interface name
824
            name_pos = match.begin(1)
×
825
            raw_tokens << [line_idx, name_pos, interface_info[:name].length, SemanticTokenTypes::INTERFACE, SemanticTokenModifiers::DEFINITION]
×
826
          end
827

828
          # Interface members
829
          interface_info[:members]&.each do |member|
×
830
            next unless (match = line.match(/^\s*(#{Regexp.escape(member[:name])})\s*:\s*/))
×
831

832
            prop_pos = match.begin(1)
×
833
            raw_tokens << [line_idx, prop_pos, member[:name].length, SemanticTokenTypes::PROPERTY, 0]
×
834

835
            # Member type
836
            add_type_tokens(raw_tokens, line, line_idx, member[:type])
×
837
          end
838
        end
839
      end
840

841
      # Process functions
842
      (parse_result[:functions] || []).each do |func|
×
843
        lines.each_with_index do |line, line_idx|
×
844
          next unless (match = line.match(/^\s*def\s+(#{Regexp.escape(func[:name])})\s*\(/))
×
845

846
          # 'def' keyword
847
          def_pos = line.index("def")
×
848
          raw_tokens << [line_idx, def_pos, 3, SemanticTokenTypes::KEYWORD, 0]
×
849

850
          # Function name
851
          name_pos = match.begin(1)
×
852
          raw_tokens << [line_idx, name_pos, func[:name].length, SemanticTokenTypes::FUNCTION, SemanticTokenModifiers::DEFINITION]
×
853

854
          # Parameters
855
          func[:params]&.each do |param|
×
856
            next unless (param_match = line.match(/\b(#{Regexp.escape(param[:name])})\s*(?::\s*)?/))
×
857

858
            param_pos = param_match.begin(1)
×
859
            raw_tokens << [line_idx, param_pos, param[:name].length, SemanticTokenTypes::PARAMETER, 0]
×
860

861
            # Parameter type if present
862
            if param[:type]
×
863
              add_type_tokens(raw_tokens, line, line_idx, param[:type])
×
864
            end
865
          end
866

867
          # Return type
868
          if func[:return_type]
×
869
            add_type_tokens(raw_tokens, line, line_idx, func[:return_type])
×
870
          end
871
        end
872
      end
873

874
      # Process 'end' keywords
875
      lines.each_with_index do |line, line_idx|
×
876
        if (match = line.match(/^\s*(end)\s*$/))
×
877
          end_pos = match.begin(1)
×
878
          raw_tokens << [line_idx, end_pos, 3, SemanticTokenTypes::KEYWORD, 0]
×
879
        end
880
      end
881

882
      # Sort tokens by line, then by character position
883
      raw_tokens.sort_by! { |t| [t[0], t[1]] }
×
884

885
      # Convert to delta encoding
886
      encode_tokens(raw_tokens)
×
887
    end
888

889
    def add_type_tokens(raw_tokens, line, line_idx, type_str)
1✔
890
      return unless type_str
×
891

892
      # Find position of the type in the line
893
      pos = line.index(type_str)
×
894
      return unless pos
×
895

896
      # Handle built-in types
897
      if BUILT_IN_TYPES.include?(type_str)
×
898
        raw_tokens << [line_idx, pos, type_str.length, SemanticTokenTypes::TYPE, SemanticTokenModifiers::DEFAULT_LIBRARY]
×
899
        return
×
900
      end
901

902
      # Handle generic types like Array<String>
903
      if type_str.include?("<")
×
904
        if (match = type_str.match(/^(\w+)<(.+)>$/))
×
905
          base = match[1]
×
906
          base_pos = line.index(base, pos)
×
907
          if base_pos
×
908
            modifier = BUILT_IN_TYPES.include?(base) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
×
909
            raw_tokens << [line_idx, base_pos, base.length, SemanticTokenTypes::TYPE, modifier]
×
910
          end
911
          # Recursively process type arguments
912
          # (simplified - just mark them as types)
913
          args = match[2]
×
914
          args.split(/[,\s]+/).each do |arg|
×
915
            arg = arg.strip.gsub(/[<>]/, "")
×
916
            next if arg.empty?
×
917

918
            arg_pos = line.index(arg, pos)
×
919
            if arg_pos
×
920
              modifier = BUILT_IN_TYPES.include?(arg) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
×
921
              raw_tokens << [line_idx, arg_pos, arg.length, SemanticTokenTypes::TYPE, modifier]
×
922
            end
923
          end
924
        end
925
        return
×
926
      end
927

928
      # Handle union types
929
      if type_str.include?("|")
×
930
        type_str.split("|").map(&:strip).each do |t|
×
931
          t_pos = line.index(t, pos)
×
932
          if t_pos
×
933
            modifier = BUILT_IN_TYPES.include?(t) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
×
934
            raw_tokens << [line_idx, t_pos, t.length, SemanticTokenTypes::TYPE, modifier]
×
935
          end
936
        end
937
        return
×
938
      end
939

940
      # Handle intersection types
941
      if type_str.include?("&")
×
942
        type_str.split("&").map(&:strip).each do |t|
×
943
          t_pos = line.index(t, pos)
×
944
          if t_pos
×
945
            modifier = BUILT_IN_TYPES.include?(t) ? SemanticTokenModifiers::DEFAULT_LIBRARY : 0
×
946
            raw_tokens << [line_idx, t_pos, t.length, SemanticTokenTypes::TYPE, modifier]
×
947
          end
948
        end
949
        return
×
950
      end
951

952
      # Simple type
953
      raw_tokens << [line_idx, pos, type_str.length, SemanticTokenTypes::TYPE, 0]
×
954
    end
955

956
    def encode_tokens(raw_tokens)
1✔
957
      encoded = []
×
958
      prev_line = 0
×
959
      prev_char = 0
×
960

961
      raw_tokens.each do |token|
×
962
        line, char, length, token_type, modifiers = token
×
963

964
        delta_line = line - prev_line
×
965
        delta_char = delta_line.zero? ? char - prev_char : char
×
966

967
        encoded << delta_line
×
968
        encoded << delta_char
×
969
        encoded << length
×
970
        encoded << token_type
×
971
        encoded << modifiers
×
972

973
        prev_line = line
×
974
        prev_char = char
×
975
      end
976

977
      encoded
×
978
    end
979

980
    # === Response Helpers ===
981

982
    def success_response(id, result)
1✔
983
      {
984
        "jsonrpc" => "2.0",
52✔
985
        "id" => id,
986
        "result" => result,
987
      }
988
    end
989

990
    def error_response(id, code, message)
1✔
991
      {
992
        "jsonrpc" => "2.0",
2✔
993
        "id" => id,
994
        "error" => {
995
          "code" => code,
996
          "message" => message,
997
        },
998
      }
999
    end
1000
  end
1001
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc