rubik 0.3.0 → 0.3.2
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 +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +84 -0
- data/lib/rubik/ast/cache_store.rb +4 -2
- data/lib/rubik/ast/graph.rb +49 -94
- data/lib/rubik/ast/graph_extensions.rb +202 -0
- data/lib/rubik/ast/ruby_extractor.rb +8 -3
- data/lib/rubik/ast/symbol.rb +7 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +4 -3
- data/lib/rubik/ast/treesitter/c_walker.rb +10 -8
- data/lib/rubik/ast/treesitter/go_walker.rb +4 -2
- data/lib/rubik/ast/treesitter/rust_walker.rb +9 -7
- data/lib/rubik/ast/treesitter/typescript_walker.rb +7 -5
- data/lib/rubik/ast/treesitter/zig_walker.rb +44 -6
- data/lib/rubik/lsp/client.rb +105 -26
- data/lib/rubik/lsp/manager.rb +136 -83
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +15 -5
- data/lib/rubik/prompt/token_estimator.rb +15 -3
- data/lib/rubik/repl/ast_warmup.rb +35 -4
- data/lib/rubik/repl/shell.rb +1 -1
- data/lib/rubik/tools/builtins/symbols.rb +56 -20
- data/lib/rubik/tools/fuzzy_matcher.rb +39 -19
- data/lib/rubik/tools/symbol_tool_support.rb +113 -0
- data/lib/rubik/tools/truncator.rb +54 -21
- data/lib/rubik/version.rb +1 -1
- metadata +3 -1
|
@@ -49,7 +49,9 @@ module Rubik
|
|
|
49
49
|
# `scope` is the enclosing declaration path (namespaces, then the
|
|
50
50
|
# innermost class, outermost first); `member` marks a class body so
|
|
51
51
|
# declarations are read as member declarations.
|
|
52
|
-
def walk_children(node, symbols, scope, member:)
|
|
52
|
+
def walk_children(node, symbols, scope, member:, depth: 0)
|
|
53
|
+
return if depth > 50
|
|
54
|
+
|
|
53
55
|
(0...node.named_child_count).each do |i|
|
|
54
56
|
child = node.named_child(i)
|
|
55
57
|
case child.kind
|
|
@@ -58,9 +60,9 @@ module Rubik
|
|
|
58
60
|
when "type_definition"
|
|
59
61
|
walk_typedef(child, symbols, scope)
|
|
60
62
|
when *RECORD_KINDS.keys
|
|
61
|
-
walk_record(child, symbols, scope)
|
|
63
|
+
walk_record(child, symbols, scope, depth: depth + 1)
|
|
62
64
|
when "namespace_definition"
|
|
63
|
-
walk_namespace(child, symbols, scope)
|
|
65
|
+
walk_namespace(child, symbols, scope, depth: depth + 1)
|
|
64
66
|
when "function_definition"
|
|
65
67
|
walk_function(child, symbols, scope, member: member)
|
|
66
68
|
when "field_declaration", "declaration"
|
|
@@ -69,7 +71,7 @@ module Rubik
|
|
|
69
71
|
# Plain recursion also unwraps template_declaration nodes (their
|
|
70
72
|
# inner class/function is walked in place) and linkage
|
|
71
73
|
# specifications.
|
|
72
|
-
walk_children(child, symbols, scope, member: member)
|
|
74
|
+
walk_children(child, symbols, scope, member: member, depth: depth + 1)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
end
|
|
@@ -104,7 +106,7 @@ module Rubik
|
|
|
104
106
|
)
|
|
105
107
|
end
|
|
106
108
|
|
|
107
|
-
def walk_record(node, symbols, scope)
|
|
109
|
+
def walk_record(node, symbols, scope, depth:)
|
|
108
110
|
name_node = node.child_by_field_name("name")
|
|
109
111
|
return unless name_node
|
|
110
112
|
|
|
@@ -122,16 +124,16 @@ module Rubik
|
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
body = node.child_by_field_name("body")
|
|
125
|
-
walk_children(body, symbols, scope + [name], member: true) if body
|
|
127
|
+
walk_children(body, symbols, scope + [name], member: true, depth: depth + 1) if body
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
# Namespace names extend the qualification path; everything inside is
|
|
129
131
|
# walked with the namespace prefix (app::run, app::Widget#run).
|
|
130
|
-
def walk_namespace(node, symbols, scope)
|
|
132
|
+
def walk_namespace(node, symbols, scope, depth:)
|
|
131
133
|
name_node = node.child_by_field_name("name")
|
|
132
134
|
ns_scope = name_node ? scope + [text_for(name_node).strip] : scope
|
|
133
135
|
body = node.child_by_field_name("body")
|
|
134
|
-
walk_children(body, symbols, ns_scope, member: false) if body
|
|
136
|
+
walk_children(body, symbols, ns_scope, member: false, depth: depth + 1) if body
|
|
135
137
|
end
|
|
136
138
|
|
|
137
139
|
def walk_function(node, symbols, scope, member:)
|
|
@@ -16,7 +16,9 @@ module Rubik
|
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def walk_children(node, symbols)
|
|
19
|
+
def walk_children(node, symbols, depth: 0)
|
|
20
|
+
return if depth > 50
|
|
21
|
+
|
|
20
22
|
(0...node.named_child_count).each do |i|
|
|
21
23
|
child = node.named_child(i)
|
|
22
24
|
case child.kind
|
|
@@ -31,7 +33,7 @@ module Rubik
|
|
|
31
33
|
when "import_declaration"
|
|
32
34
|
walk_import(child, symbols)
|
|
33
35
|
else
|
|
34
|
-
walk_children(child, symbols)
|
|
36
|
+
walk_children(child, symbols, depth: depth + 1)
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
end
|
|
@@ -16,7 +16,9 @@ module Rubik
|
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def walk_children(node, symbols, impl_type)
|
|
19
|
+
def walk_children(node, symbols, impl_type, depth: 0)
|
|
20
|
+
return if depth > 50
|
|
21
|
+
|
|
20
22
|
(0...node.named_child_count).each do |i|
|
|
21
23
|
child = node.named_child(i)
|
|
22
24
|
case child.kind
|
|
@@ -25,13 +27,13 @@ module Rubik
|
|
|
25
27
|
when "enum_item"
|
|
26
28
|
walk_enum(child, symbols)
|
|
27
29
|
when "impl_item"
|
|
28
|
-
walk_impl(child, symbols)
|
|
30
|
+
walk_impl(child, symbols, depth: depth + 1)
|
|
29
31
|
when "function_item"
|
|
30
32
|
walk_function(child, symbols, impl_type)
|
|
31
33
|
when "use_declaration"
|
|
32
34
|
walk_use(child, symbols)
|
|
33
35
|
else
|
|
34
|
-
walk_children(child, symbols, impl_type)
|
|
36
|
+
walk_children(child, symbols, impl_type, depth: depth + 1)
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
end
|
|
@@ -64,12 +66,12 @@ module Rubik
|
|
|
64
66
|
)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
def walk_impl(node, symbols)
|
|
69
|
+
def walk_impl(node, symbols, depth: 0)
|
|
68
70
|
type_node = node.child_by_field_name("type")
|
|
69
71
|
impl_name = type_node ? text_for(type_node).strip : nil
|
|
70
72
|
|
|
71
73
|
body = node.child_by_field_name("body")
|
|
72
|
-
walk_children(body, symbols, impl_name) if body
|
|
74
|
+
walk_children(body, symbols, impl_name, depth: depth + 1) if body
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
def walk_function(node, symbols, impl_type)
|
|
@@ -110,13 +112,13 @@ module Rubik
|
|
|
110
112
|
if n.kind == "call_expression"
|
|
111
113
|
fn = n.child_by_field_name("function")
|
|
112
114
|
if fn
|
|
113
|
-
fn_text = text_for(fn).
|
|
115
|
+
fn_text = text_for(fn).strip
|
|
114
116
|
calls << fn_text if fn_text && !fn_text.empty?
|
|
115
117
|
end
|
|
116
118
|
elsif n.kind == "macro_invocation"
|
|
117
119
|
macro = n.child_by_field_name("macro") || n.named_child(0)
|
|
118
120
|
if macro
|
|
119
|
-
macro_text = text_for(macro).sub(/!\z/, "").
|
|
121
|
+
macro_text = text_for(macro).sub(/!\z/, "").strip
|
|
120
122
|
calls << macro_text if macro_text && !macro_text.empty?
|
|
121
123
|
end
|
|
122
124
|
end
|
|
@@ -16,12 +16,14 @@ module Rubik
|
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def walk_children(node, symbols, current_class)
|
|
19
|
+
def walk_children(node, symbols, current_class, depth: 0)
|
|
20
|
+
return if depth > 50
|
|
21
|
+
|
|
20
22
|
(0...node.named_child_count).each do |i|
|
|
21
23
|
child = node.named_child(i)
|
|
22
24
|
case child.kind
|
|
23
25
|
when "export_statement"
|
|
24
|
-
walk_export(child, symbols, current_class)
|
|
26
|
+
walk_export(child, symbols, current_class, depth: depth + 1)
|
|
25
27
|
when "interface_declaration"
|
|
26
28
|
walk_interface(child, symbols)
|
|
27
29
|
when "enum_declaration"
|
|
@@ -35,12 +37,12 @@ module Rubik
|
|
|
35
37
|
when "import_statement"
|
|
36
38
|
walk_import(child, symbols)
|
|
37
39
|
else
|
|
38
|
-
walk_children(child, symbols, current_class)
|
|
40
|
+
walk_children(child, symbols, current_class, depth: depth + 1)
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
def walk_export(node, symbols, current_class)
|
|
45
|
+
def walk_export(node, symbols, current_class, depth:)
|
|
44
46
|
(0...node.named_child_count).each do |i|
|
|
45
47
|
child = node.named_child(i)
|
|
46
48
|
case child.kind
|
|
@@ -49,7 +51,7 @@ module Rubik
|
|
|
49
51
|
when "class_declaration" then walk_class(child, symbols)
|
|
50
52
|
when "function_declaration" then walk_function(child, symbols)
|
|
51
53
|
when "lexical_declaration", "variable_declaration" then walk_variable_declaration(child, symbols)
|
|
52
|
-
else walk_children(child, symbols, current_class)
|
|
54
|
+
else walk_children(child, symbols, current_class, depth: depth + 1)
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
end
|
|
@@ -19,14 +19,50 @@ module Rubik
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# Zig represents a method receiver as the prefix of a SuffixExpr while
|
|
23
|
+
# FieldOrFnCall contains only the trailing `.method()` node. Preserve
|
|
24
|
+
# the full expression so unresolved receiver dispatch is not flattened
|
|
25
|
+
# into a call to every same-named function.
|
|
26
|
+
def extract_calls(node, call_kinds = ["call", "call_expression", "invocation"])
|
|
27
|
+
return super unless call_kinds.include?("FieldOrFnCall")
|
|
28
|
+
|
|
29
|
+
calls = []
|
|
30
|
+
stack = [node]
|
|
31
|
+
while (current = stack.pop)
|
|
32
|
+
if current.kind == "FieldOrFnCall"
|
|
33
|
+
args_node = (0...current.named_child_count).map { |i| current.named_child(i) }
|
|
34
|
+
.find { |child| child.kind == "FnCallArguments" }
|
|
35
|
+
if args_node
|
|
36
|
+
suffix = current.parent
|
|
37
|
+
start_byte = suffix&.kind == "SuffixExpr" ? suffix.start_byte : current.start_byte
|
|
38
|
+
fn_text = (@code.byteslice(start_byte...args_node.start_byte) || "").strip
|
|
39
|
+
calls << fn_text unless fn_text.empty?
|
|
40
|
+
end
|
|
41
|
+
elsif current.kind == "SuffixExpr" &&
|
|
42
|
+
current.named_child_count >= 2 &&
|
|
43
|
+
current.named_child(1)&.kind == "FnCallArguments"
|
|
44
|
+
fn_text = text_for(current.named_child(0)).strip
|
|
45
|
+
calls << fn_text unless fn_text.empty?
|
|
46
|
+
elsif call_kinds.include?(current.kind)
|
|
47
|
+
fn = current.child_by_field_name("function") || current.child_by_field_name("name") || current.named_child(0)
|
|
48
|
+
fn_text = text_for(fn).strip if fn
|
|
49
|
+
calls << fn_text if fn_text && !fn_text.empty?
|
|
50
|
+
end
|
|
51
|
+
(0...current.named_child_count).reverse_each { |i| stack.push(current.named_child(i)) }
|
|
52
|
+
end
|
|
53
|
+
calls.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def walk_children(node, symbols, depth: 0)
|
|
57
|
+
return if depth > 50
|
|
58
|
+
|
|
23
59
|
(0...node.named_child_count).each do |i|
|
|
24
60
|
child = node.named_child(i)
|
|
25
61
|
case child.kind
|
|
26
62
|
when "Decl"
|
|
27
63
|
walk_decl(child, symbols)
|
|
28
64
|
else
|
|
29
|
-
walk_children(child, symbols)
|
|
65
|
+
walk_children(child, symbols, depth: depth + 1)
|
|
30
66
|
end
|
|
31
67
|
end
|
|
32
68
|
end
|
|
@@ -82,11 +118,12 @@ module Rubik
|
|
|
82
118
|
# Locates the node that owns this Decl's function definition. Zig
|
|
83
119
|
# exposes it under several kind names across grammar versions, so we
|
|
84
120
|
# accept every one we know about rather than only "FnProto".
|
|
85
|
-
def find_function(node)
|
|
121
|
+
def find_function(node, depth: 0)
|
|
122
|
+
return if depth > 50
|
|
86
123
|
return node if FN_KINDS.include?(node.kind)
|
|
87
124
|
|
|
88
125
|
(0...node.named_child_count).each do |i|
|
|
89
|
-
found = find_function(node.named_child(i))
|
|
126
|
+
found = find_function(node.named_child(i), depth: depth + 1)
|
|
90
127
|
return found if found
|
|
91
128
|
end
|
|
92
129
|
nil
|
|
@@ -94,11 +131,12 @@ module Rubik
|
|
|
94
131
|
|
|
95
132
|
# Walks the subtree for the first identifier node — used as a fallback
|
|
96
133
|
# when the grammar does not expose the function name as a named field.
|
|
97
|
-
def find_identifier(node)
|
|
134
|
+
def find_identifier(node, depth: 0)
|
|
135
|
+
return if depth > 50
|
|
98
136
|
return node if node.kind == "identifier"
|
|
99
137
|
|
|
100
138
|
(0...node.named_child_count).each do |i|
|
|
101
|
-
found = find_identifier(node.named_child(i))
|
|
139
|
+
found = find_identifier(node.named_child(i), depth: depth + 1)
|
|
102
140
|
return found if found
|
|
103
141
|
end
|
|
104
142
|
nil
|
data/lib/rubik/lsp/client.rb
CHANGED
|
@@ -4,10 +4,20 @@ require "open3"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
6
|
require_relative "../platform"
|
|
7
|
+
require_relative "../log"
|
|
7
8
|
|
|
8
9
|
module Rubik
|
|
9
10
|
module LSP
|
|
10
11
|
class Client
|
|
12
|
+
class RequestError < StandardError
|
|
13
|
+
attr_reader :reason
|
|
14
|
+
|
|
15
|
+
def initialize(reason, message)
|
|
16
|
+
@reason = reason
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
SERVERS = {
|
|
12
22
|
"go" => "gopls",
|
|
13
23
|
"rust" => "rust-analyzer",
|
|
@@ -30,13 +40,14 @@ module Rubik
|
|
|
30
40
|
@language = language.to_s.downcase
|
|
31
41
|
@root_dir = Rubik::Platform.expand_path(root_dir)
|
|
32
42
|
cmd = server_cmd || SERVERS[@language]
|
|
33
|
-
if
|
|
43
|
+
if cmd.to_s.start_with?("typeprof", "typescript-language-server") && !cmd.to_s.include?("--stdio")
|
|
34
44
|
cmd = "#{cmd} --stdio"
|
|
35
45
|
elsif cmd.to_s.start_with?("solargraph") && !cmd.to_s.include?("stdio")
|
|
36
46
|
cmd = "#{cmd} stdio"
|
|
37
47
|
end
|
|
38
48
|
@server_cmd = cmd
|
|
39
49
|
@running = false
|
|
50
|
+
@crashed = false
|
|
40
51
|
@req_id = 0
|
|
41
52
|
@stdin = nil
|
|
42
53
|
@stdout = nil
|
|
@@ -48,6 +59,7 @@ module Rubik
|
|
|
48
59
|
@cv = ConditionVariable.new
|
|
49
60
|
@responses = {}
|
|
50
61
|
@pending_requests = {}
|
|
62
|
+
@reader_failure = nil
|
|
51
63
|
@diagnostics = {}
|
|
52
64
|
@open_documents = {}
|
|
53
65
|
@version_counter = 0
|
|
@@ -72,6 +84,7 @@ module Rubik
|
|
|
72
84
|
@req_id = 0
|
|
73
85
|
@responses.clear
|
|
74
86
|
@pending_requests.clear
|
|
87
|
+
@reader_failure = nil
|
|
75
88
|
@open_documents.clear
|
|
76
89
|
@diagnostics.clear
|
|
77
90
|
@version_counter = 0
|
|
@@ -91,6 +104,7 @@ module Rubik
|
|
|
91
104
|
@stdin.binmode
|
|
92
105
|
@stdout.binmode
|
|
93
106
|
@running = false
|
|
107
|
+
@crashed = false
|
|
94
108
|
@reader_alive = false
|
|
95
109
|
|
|
96
110
|
start_reader_thread
|
|
@@ -137,6 +151,10 @@ module Rubik
|
|
|
137
151
|
true
|
|
138
152
|
end
|
|
139
153
|
|
|
154
|
+
def crashed?
|
|
155
|
+
defined?(@crashed) && @crashed == true
|
|
156
|
+
end
|
|
157
|
+
|
|
140
158
|
def reconnect!
|
|
141
159
|
stop if @running
|
|
142
160
|
start
|
|
@@ -153,6 +171,7 @@ module Rubik
|
|
|
153
171
|
end
|
|
154
172
|
|
|
155
173
|
@running = false
|
|
174
|
+
@crashed = false
|
|
156
175
|
@reader_alive = false
|
|
157
176
|
# Drain first: its thread may be blocked in gets on @stderr.
|
|
158
177
|
@stderr_drain&.stop
|
|
@@ -189,9 +208,8 @@ module Rubik
|
|
|
189
208
|
def definition(file_path, line, character)
|
|
190
209
|
return { ok: false, error: "LSP server not running" } unless alive?
|
|
191
210
|
|
|
192
|
-
abs_path = File.expand_path(file_path, @root_dir)
|
|
193
211
|
res = send_request("textDocument/definition", {
|
|
194
|
-
textDocument: { uri:
|
|
212
|
+
textDocument: { uri: document_uri(file_path) },
|
|
195
213
|
position: { line: line, character: character }
|
|
196
214
|
})
|
|
197
215
|
|
|
@@ -203,9 +221,8 @@ module Rubik
|
|
|
203
221
|
def references(file_path, line, character)
|
|
204
222
|
return { ok: false, error: "LSP server not running" } unless alive?
|
|
205
223
|
|
|
206
|
-
abs_path = File.expand_path(file_path, @root_dir)
|
|
207
224
|
res = send_request("textDocument/references", {
|
|
208
|
-
textDocument: { uri:
|
|
225
|
+
textDocument: { uri: document_uri(file_path) },
|
|
209
226
|
position: { line: line, character: character },
|
|
210
227
|
context: { includeDeclaration: true }
|
|
211
228
|
})
|
|
@@ -218,9 +235,8 @@ module Rubik
|
|
|
218
235
|
def hover(file_path, line, character)
|
|
219
236
|
return { ok: false, error: "LSP server not running" } unless alive?
|
|
220
237
|
|
|
221
|
-
abs_path = File.expand_path(file_path, @root_dir)
|
|
222
238
|
res = send_request("textDocument/hover", {
|
|
223
|
-
textDocument: { uri:
|
|
239
|
+
textDocument: { uri: document_uri(file_path) },
|
|
224
240
|
position: { line: line, character: character }
|
|
225
241
|
})
|
|
226
242
|
|
|
@@ -232,7 +248,7 @@ module Rubik
|
|
|
232
248
|
def did_open(file_path, content, language_id = @language)
|
|
233
249
|
return false unless alive?
|
|
234
250
|
|
|
235
|
-
abs_path =
|
|
251
|
+
abs_path = normalized_document_path(file_path)
|
|
236
252
|
@mutex.synchronize do
|
|
237
253
|
@version_counter += 1
|
|
238
254
|
@open_documents[abs_path] = @version_counter
|
|
@@ -240,7 +256,7 @@ module Rubik
|
|
|
240
256
|
|
|
241
257
|
send_notification("textDocument/didOpen", {
|
|
242
258
|
textDocument: {
|
|
243
|
-
uri:
|
|
259
|
+
uri: document_uri(abs_path),
|
|
244
260
|
languageId: language_id.to_s,
|
|
245
261
|
version: @version_counter,
|
|
246
262
|
text: content
|
|
@@ -254,7 +270,7 @@ module Rubik
|
|
|
254
270
|
def did_change(file_path, content)
|
|
255
271
|
return false unless alive?
|
|
256
272
|
|
|
257
|
-
abs_path =
|
|
273
|
+
abs_path = normalized_document_path(file_path)
|
|
258
274
|
version = @mutex.synchronize do
|
|
259
275
|
@version_counter += 1
|
|
260
276
|
@open_documents[abs_path] = @version_counter
|
|
@@ -266,7 +282,7 @@ module Rubik
|
|
|
266
282
|
|
|
267
283
|
send_notification("textDocument/didChange", {
|
|
268
284
|
textDocument: {
|
|
269
|
-
uri:
|
|
285
|
+
uri: document_uri(abs_path),
|
|
270
286
|
version: version
|
|
271
287
|
},
|
|
272
288
|
contentChanges: [
|
|
@@ -281,11 +297,16 @@ module Rubik
|
|
|
281
297
|
def did_close(file_path)
|
|
282
298
|
return false unless @running
|
|
283
299
|
|
|
284
|
-
abs_path =
|
|
285
|
-
|
|
300
|
+
abs_path = normalized_document_path(file_path)
|
|
301
|
+
raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
|
|
302
|
+
@mutex.synchronize do
|
|
303
|
+
@open_documents.delete(abs_path)
|
|
304
|
+
@open_documents.delete(raw_abs)
|
|
305
|
+
@open_documents.delete_if { |k, _| Rubik::Platform.path_equal?(k, abs_path) }
|
|
306
|
+
end
|
|
286
307
|
|
|
287
308
|
send_notification("textDocument/didClose", {
|
|
288
|
-
textDocument: { uri:
|
|
309
|
+
textDocument: { uri: document_uri(abs_path) }
|
|
289
310
|
})
|
|
290
311
|
true
|
|
291
312
|
rescue StandardError
|
|
@@ -295,18 +316,25 @@ module Rubik
|
|
|
295
316
|
def document_open?(file_path)
|
|
296
317
|
return false if file_path.nil? || file_path.to_s.strip.empty?
|
|
297
318
|
|
|
298
|
-
abs_path =
|
|
299
|
-
|
|
319
|
+
abs_path = normalized_document_path(file_path)
|
|
320
|
+
raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
|
|
321
|
+
@mutex.synchronize do
|
|
322
|
+
@open_documents.key?(abs_path) ||
|
|
323
|
+
@open_documents.key?(raw_abs) ||
|
|
324
|
+
@open_documents.keys.any? { |k| Rubik::Platform.path_equal?(k, abs_path) }
|
|
325
|
+
end
|
|
300
326
|
rescue ArgumentError
|
|
301
327
|
false
|
|
302
328
|
end
|
|
303
329
|
|
|
304
330
|
def diagnostics_for(file_path)
|
|
305
|
-
abs_path =
|
|
331
|
+
abs_path = normalized_document_path(file_path)
|
|
332
|
+
raw_abs = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
|
|
306
333
|
root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
|
|
307
|
-
rel_path =
|
|
334
|
+
rel_path = raw_abs.start_with?("#{root_norm}/") ? raw_abs.delete_prefix("#{root_norm}/") : file_path.to_s.tr("\\", "/")
|
|
308
335
|
@mutex.synchronize do
|
|
309
336
|
@diagnostics[abs_path] ||
|
|
337
|
+
@diagnostics[raw_abs] ||
|
|
310
338
|
@diagnostics[rel_path] ||
|
|
311
339
|
@diagnostics[file_path] ||
|
|
312
340
|
@diagnostics.find { |k, _| Rubik::Platform.path_equal?(k, abs_path) }&.last ||
|
|
@@ -347,23 +375,30 @@ module Rubik
|
|
|
347
375
|
parsed = JSON.parse(body)
|
|
348
376
|
handle_incoming_message(parsed)
|
|
349
377
|
rescue StandardError => e
|
|
350
|
-
warn "
|
|
378
|
+
Rubik::Log.warn(:lsp, "Failed to handle message: #{e.class}: #{e.message}")
|
|
351
379
|
end
|
|
352
380
|
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
353
381
|
break
|
|
354
382
|
rescue StandardError => e
|
|
355
|
-
warn "
|
|
383
|
+
Rubik::Log.warn(:lsp, "Reader loop error: #{e.class}: #{e.message}")
|
|
356
384
|
next
|
|
357
385
|
end
|
|
358
386
|
end
|
|
359
387
|
ensure
|
|
360
|
-
@
|
|
361
|
-
|
|
388
|
+
@mutex.synchronize do
|
|
389
|
+
@crashed = true if @running
|
|
390
|
+
@reader_alive = false
|
|
391
|
+
@running = false
|
|
392
|
+
@reader_failure ||= "LSP response reader stopped before completing the request"
|
|
393
|
+
@cv.broadcast
|
|
394
|
+
end
|
|
362
395
|
end
|
|
363
396
|
end
|
|
364
397
|
|
|
365
398
|
def handle_incoming_message(msg)
|
|
366
|
-
if msg
|
|
399
|
+
if msg.key?("method") && msg.key?("id")
|
|
400
|
+
handle_server_request(msg)
|
|
401
|
+
elsif msg.key?("id")
|
|
367
402
|
req_id = msg["id"]
|
|
368
403
|
@mutex.synchronize do
|
|
369
404
|
# Discard late response if request is neither pending nor already tracked
|
|
@@ -381,12 +416,14 @@ module Rubik
|
|
|
381
416
|
params = msg["params"] || {}
|
|
382
417
|
uri = params["uri"].to_s
|
|
383
418
|
raw_path = Rubik::Platform.from_file_uri(uri)
|
|
419
|
+
norm_path = normalized_document_path(raw_path)
|
|
384
420
|
abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(raw_path, @root_dir))
|
|
385
421
|
root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
|
|
386
422
|
rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : abs_path
|
|
387
423
|
diags = params["diagnostics"] || []
|
|
388
424
|
|
|
389
425
|
@mutex.synchronize do
|
|
426
|
+
@diagnostics[norm_path] = diags
|
|
390
427
|
@diagnostics[abs_path] = diags
|
|
391
428
|
@diagnostics[rel_path] = diags
|
|
392
429
|
@diagnostics[raw_path] = diags
|
|
@@ -412,15 +449,28 @@ module Rubik
|
|
|
412
449
|
|
|
413
450
|
def wait_for_response(req_id, timeout: 5)
|
|
414
451
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
415
|
-
res = nil
|
|
416
452
|
timed_out = false
|
|
453
|
+
reader_failure = nil
|
|
417
454
|
|
|
418
455
|
@mutex.synchronize do
|
|
419
456
|
loop do
|
|
420
457
|
if @responses.key?(req_id)
|
|
421
|
-
|
|
458
|
+
response = @responses.delete(req_id)
|
|
422
459
|
@pending_requests.delete(req_id)
|
|
423
|
-
|
|
460
|
+
if response.key?("error")
|
|
461
|
+
error = response["error"] || {}
|
|
462
|
+
raise RequestError.new(:server_error, error["message"] || "LSP server returned an error")
|
|
463
|
+
end
|
|
464
|
+
return response["result"] if response.key?("result")
|
|
465
|
+
|
|
466
|
+
raise RequestError.new(:invalid_response, "LSP response did not contain a result")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
if @reader_failure
|
|
470
|
+
reader_failure = @reader_failure
|
|
471
|
+
@responses.delete(req_id)
|
|
472
|
+
@pending_requests.delete(req_id)
|
|
473
|
+
break
|
|
424
474
|
end
|
|
425
475
|
|
|
426
476
|
remaining = timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time)
|
|
@@ -442,11 +492,40 @@ module Rubik
|
|
|
442
492
|
rescue StandardError
|
|
443
493
|
nil
|
|
444
494
|
end
|
|
495
|
+
raise RequestError.new(:timeout, "LSP request timed out after #{timeout} seconds")
|
|
445
496
|
end
|
|
446
497
|
|
|
498
|
+
raise RequestError.new(:reader_failure, reader_failure) if reader_failure
|
|
499
|
+
|
|
447
500
|
nil
|
|
448
501
|
end
|
|
449
502
|
|
|
503
|
+
def normalized_document_path(file_path)
|
|
504
|
+
path = Rubik::Platform.expand_path(file_path, @root_dir)
|
|
505
|
+
Rubik::Platform.normalize_path(path)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def document_uri(file_path)
|
|
509
|
+
Rubik::Platform.to_file_uri(normalized_document_path(file_path))
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def handle_server_request(msg)
|
|
513
|
+
case msg["method"]
|
|
514
|
+
when "workspace/configuration"
|
|
515
|
+
params = msg["params"]
|
|
516
|
+
items = params.is_a?(Hash) && params["items"].is_a?(Array) ? params["items"] : []
|
|
517
|
+
write_message({ jsonrpc: "2.0", id: msg["id"], result: items.map { {} } })
|
|
518
|
+
when "client/registerCapability", "client/unregisterCapability", "window/workDoneProgress/create"
|
|
519
|
+
write_message({ jsonrpc: "2.0", id: msg["id"], result: nil })
|
|
520
|
+
else
|
|
521
|
+
write_message({
|
|
522
|
+
jsonrpc: "2.0",
|
|
523
|
+
id: msg["id"],
|
|
524
|
+
error: { code: -32_601, message: "Method not found: #{msg['method']}" }
|
|
525
|
+
})
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
450
529
|
def send_notification(method, params)
|
|
451
530
|
msg = {
|
|
452
531
|
jsonrpc: "2.0",
|