Module: MilkTea::LSP::Server::ServerInlayHints

Included in:
MilkTea::LSP::Server
Defined in:
lib/milk_tea/lsp/server/inlay_hints.rb

Instance Method Summary collapse

Instance Method Details

#collect_function_defs(ast_node) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 195

def collect_function_defs(ast_node)
  results = []
  case ast_node
  when Array
    ast_node.each { |n| results.concat(collect_function_defs(n)) }
  when AST::SourceFile
    ast_node.declarations.each do |d|
      case d
      when AST::FunctionDef
        results << d
      when AST::ExtendingBlock
        results.concat(d.methods)
      end
    end
  end
  results
end

#collect_inferred_return_hints(facts, start_line, start_char, end_line, end_char) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 130

def collect_inferred_return_hints(facts, start_line, start_char, end_line, end_char)
  hints = []
  collect_function_defs(facts.ast).each do |func|
    next unless func.return_type.nil?
    next unless position_in_range?(func.line - 1, func.column - 1, start_line, start_char, end_line, end_char)

    binding = facts.functions[func.name]
    unless binding
      facts.imports.each_value do |mod|
        binding = mod.functions[func.name]
        break if binding
      end
    end
    next unless binding

    return_type = binding.type.return_type
    next unless return_type

    display_type = describe_type_for_hint(return_type)
    next unless display_type
    next if display_type == "void"

    label = "-> #{display_type}"
    hints << {
      position: { line: func.line - 1, character: func.column - 1 + func.name.length },
      label: label,
      kind: 1,
      paddingRight: false
    }
  end
  hints
end

#collect_inferred_type_hints(facts, start_line, start_char, end_line, end_char) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 108

def collect_inferred_type_hints(facts, start_line, start_char, end_line, end_char)
  hints = []
  collect_local_decls(facts.ast).each do |decl|
    next unless decl.type.nil?
    next unless position_in_range?(decl.line - 1, decl.column - 1, start_line, start_char, end_line, end_char)

    binding = facts.values[decl.name]
    next unless binding

    display_type = describe_type_for_hint(binding.storage_type)
    next unless display_type

    hints << {
      position: { line: decl.line - 1, character: decl.column - 1 + decl.name.length },
      label: ": #{display_type}",
      kind: 1,
      paddingRight: false
    }
  end
  hints
end

#collect_local_decls(ast_node) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 163

def collect_local_decls(ast_node)
  results = []
  case ast_node
  when Array
    ast_node.each { |n| results.concat(collect_local_decls(n)) }
  when AST::SourceFile
    ast_node.declarations.each { |d| results.concat(collect_local_decls(d)) }
  when AST::FunctionDef
    results.concat(collect_local_decls(ast_node.body)) if ast_node.body
  when AST::MethodDef
    results.concat(collect_local_decls(ast_node.body)) if ast_node.body
  when AST::ExtendingBlock
    ast_node.methods.each { |m| results.concat(collect_local_decls(m)) }
  when AST::IfStmt
    ast_node.branches.each { |b| results.concat(collect_local_decls(b.body)) }
    results.concat(collect_local_decls(ast_node.else_body)) if ast_node.else_body
  when AST::MatchStmt
    ast_node.arms.each { |a| results.concat(collect_local_decls(a.body)) }
  when AST::ForStmt
    results.concat(collect_local_decls(ast_node.body))
  when AST::WhileStmt
    results.concat(collect_local_decls(ast_node.body))
  when AST::UnsafeStmt
    results.concat(collect_local_decls(ast_node.body))
  when AST::LocalDecl
    results << ast_node
  when AST::VariantArm
    # no body
  end
  results
end

#collect_parameter_name_hints(tokens, facts, start_line, start_char, end_line, end_char) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 39

def collect_parameter_name_hints(tokens, facts, start_line, start_char, end_line, end_char)
  hints = []
  i = 0
  while i < tokens.length - 1
    callee = tokens[i]
    next_tok = tokens[i + 1]
    prev_tok = i > 0 ? tokens[i - 1] : nil

    binding = nil
    lparen_index = nil

    if callee.type == :identifier && prev_tok&.type != :function
      # Direct function call: func(...)
      if next_tok&.type == :lparen
        binding = facts.functions[callee.lexeme]
        binding ||= facts.imports.each_value.find { |mod| mod.functions.key?(callee.lexeme) }&.functions&.dig(callee.lexeme)
        lparen_index = i + 1
      elsif next_tok&.type == :dot
        member = tokens[i + 2]
        member_lparen = tokens[i + 3]
        if member&.type == :identifier && member_lparen&.type == :lparen
          # Module-qualified call: mod.func(...)
          mod_binding = facts.imports[callee.lexeme]
          if mod_binding
            binding = mod_binding.functions[member.lexeme]
          else
            # Instance method call: obj.method(...) or Type.static(...)
            receiver_type = resolve_dot_receiver_value_type(facts, callee.lexeme, callee.line, callee.column)
            receiver_type ||= facts.types[callee.lexeme]
            unless receiver_type
              receiver_type = facts.imports.each_value.find { |mod| mod.types.key?(callee.lexeme) }&.types&.dig(callee.lexeme)
            end
            if receiver_type
              methods = methods_for_receiver_type(facts, receiver_type)
              binding = methods[member.lexeme] || methods["static:#{member.lexeme}"]
            end
          end
          lparen_index = i + 3
        end
      end
    end

    if binding && lparen_index
      arg_starts, closing_index = collect_call_argument_starts(tokens, lparen_index)
      params_list = binding.respond_to?(:type) ? binding.type.params : []

      arg_starts.each_with_index do |arg_tok, index|
        break if index >= params_list.length
        next unless position_in_range?(arg_tok.line - 1, arg_tok.column - 1, start_line, start_char, end_line, end_char)
        next if self_describing_argument_expression?(tokens, arg_tok)
        param_name = params_list[index].name
        next if arg_tok.type == :identifier && arg_tok.lexeme == param_name

        hints << {
          position: { line: arg_tok.line - 1, character: arg_tok.column - 1 },
          label: "#{params_list[index].name}: ",
          kind: 2,
          paddingRight: true
        }
      end

      i = closing_index if closing_index
    end

    i += 1
  end
  hints
end

#describe_type_for_hint(type) ⇒ Object



213
214
215
216
217
218
219
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 213

def describe_type_for_hint(type)
  return nil unless type

  type.to_s
rescue StandardError
  nil
end

#handle_inlay_hint(params) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/milk_tea/lsp/server/inlay_hints.rb', line 7

def handle_inlay_hint(params)
  uri = params.dig('textDocument', 'uri')
  range = params['range'] || {}
  start_line = range.dig('start', 'line') || 0
  start_char = range.dig('start', 'character') || 0
  end_line = range.dig('end', 'line') || 0
  end_char = range.dig('end', 'character') || 0

  content = @workspace.get_content(uri)
  return [] if content && skip_expensive_work_reason(uri, content)

  facts = @workspace.get_facts(uri)
  tokens = @workspace.get_tokens(uri)
  return [] unless facts && tokens

  hints = []

  # Parameter-name hints at call sites
  hints.concat(collect_parameter_name_hints(tokens, facts, start_line, start_char, end_line, end_char))

  # Type annotation hints for inferred locals
  hints.concat(collect_inferred_type_hints(facts, start_line, start_char, end_line, end_char))

  # Return type hints for inferred return types
  hints.concat(collect_inferred_return_hints(facts, start_line, start_char, end_line, end_char))

  hints
rescue StandardError => e
  warn "Error in inlayHint handler: #{e.message}"
  []
end