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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/bin/trc +6 -0
- data/lib/t_ruby/benchmark.rb +592 -0
- data/lib/t_ruby/bundler_integration.rb +569 -0
- data/lib/t_ruby/cache.rb +774 -0
- data/lib/t_ruby/cli.rb +106 -0
- data/lib/t_ruby/compiler.rb +299 -0
- data/lib/t_ruby/config.rb +53 -0
- data/lib/t_ruby/constraint_checker.rb +441 -0
- data/lib/t_ruby/declaration_generator.rb +298 -0
- data/lib/t_ruby/doc_generator.rb +474 -0
- data/lib/t_ruby/error_handler.rb +132 -0
- data/lib/t_ruby/generic_type_parser.rb +68 -0
- data/lib/t_ruby/intersection_type_parser.rb +30 -0
- data/lib/t_ruby/ir.rb +1301 -0
- data/lib/t_ruby/lsp_server.rb +994 -0
- data/lib/t_ruby/package_manager.rb +735 -0
- data/lib/t_ruby/parser.rb +245 -0
- data/lib/t_ruby/parser_combinator.rb +942 -0
- data/lib/t_ruby/rbs_generator.rb +71 -0
- data/lib/t_ruby/runtime_validator.rb +367 -0
- data/lib/t_ruby/smt_solver.rb +1076 -0
- data/lib/t_ruby/type_alias_registry.rb +102 -0
- data/lib/t_ruby/type_checker.rb +770 -0
- data/lib/t_ruby/type_erasure.rb +26 -0
- data/lib/t_ruby/type_inferencer.rb +580 -0
- data/lib/t_ruby/union_type_parser.rb +38 -0
- data/lib/t_ruby/version.rb +5 -0
- data/lib/t_ruby/watcher.rb +320 -0
- data/lib/t_ruby.rb +42 -0
- metadata +87 -0
|
@@ -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
|