collie-lsp 0.1.0
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/.collie.yml.example +144 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +21 -0
- data/README.md +273 -0
- data/Rakefile +8 -0
- data/collie-lsp.gemspec +36 -0
- data/exe/collie-lsp +9 -0
- data/lib/collie_lsp/collie_wrapper.rb +97 -0
- data/lib/collie_lsp/document_store.rb +66 -0
- data/lib/collie_lsp/handlers/code_action.rb +77 -0
- data/lib/collie_lsp/handlers/completion.rb +72 -0
- data/lib/collie_lsp/handlers/definition.rb +117 -0
- data/lib/collie_lsp/handlers/diagnostics.rb +64 -0
- data/lib/collie_lsp/handlers/document_symbol.rb +185 -0
- data/lib/collie_lsp/handlers/folding_range.rb +211 -0
- data/lib/collie_lsp/handlers/formatting.rb +55 -0
- data/lib/collie_lsp/handlers/hover.rb +104 -0
- data/lib/collie_lsp/handlers/references.rb +185 -0
- data/lib/collie_lsp/handlers/rename.rb +226 -0
- data/lib/collie_lsp/handlers/semantic_tokens.rb +302 -0
- data/lib/collie_lsp/handlers/workspace_symbol.rb +161 -0
- data/lib/collie_lsp/protocol/initialize.rb +58 -0
- data/lib/collie_lsp/protocol/shutdown.rb +25 -0
- data/lib/collie_lsp/protocol/text_document.rb +81 -0
- data/lib/collie_lsp/server.rb +104 -0
- data/lib/collie_lsp/version.rb +5 -0
- data/lib/collie_lsp.rb +25 -0
- data/vscode-extension/.gitignore +3 -0
- data/vscode-extension/.vscode/launch.json +17 -0
- data/vscode-extension/.vscode/tasks.json +14 -0
- data/vscode-extension/README.md +35 -0
- data/vscode-extension/package.json +49 -0
- data/vscode-extension/src/extension.ts +48 -0
- data/vscode-extension/test-grammar.y +42 -0
- data/vscode-extension/tsconfig.json +12 -0
- metadata +98 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CollieLsp
|
|
4
|
+
module Handlers
|
|
5
|
+
# Code folding support
|
|
6
|
+
module FoldingRange
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Handle textDocument/foldingRange request
|
|
10
|
+
# @param request [Hash] LSP request
|
|
11
|
+
# @param document_store [DocumentStore] Document store
|
|
12
|
+
# @param _collie [CollieWrapper] Collie wrapper (unused)
|
|
13
|
+
# @param writer [Object] Response writer
|
|
14
|
+
def handle(request, document_store, _collie, writer)
|
|
15
|
+
uri = request[:params][:textDocument][:uri]
|
|
16
|
+
doc = document_store.get(uri)
|
|
17
|
+
|
|
18
|
+
unless doc
|
|
19
|
+
writer.write(id: request[:id], result: [])
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ranges = build_folding_ranges(doc[:text], doc[:ast])
|
|
24
|
+
|
|
25
|
+
writer.write(
|
|
26
|
+
id: request[:id],
|
|
27
|
+
result: ranges
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Build folding ranges from document
|
|
32
|
+
# @param text [String] Document text
|
|
33
|
+
# @param ast [Hash, nil] Parsed AST (may be nil)
|
|
34
|
+
# @return [Array<Hash>] LSP folding ranges
|
|
35
|
+
def build_folding_ranges(text, ast)
|
|
36
|
+
ranges = []
|
|
37
|
+
|
|
38
|
+
# Add ranges from AST if available
|
|
39
|
+
ranges.concat(build_ast_folding_ranges(ast)) if ast
|
|
40
|
+
|
|
41
|
+
# Add ranges from text structure
|
|
42
|
+
ranges.concat(build_text_folding_ranges(text))
|
|
43
|
+
|
|
44
|
+
# Sort and remove overlaps
|
|
45
|
+
ranges.sort_by { |r| [r[:startLine], r[:endLine]] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Build folding ranges from AST
|
|
49
|
+
# @param ast [Hash] Parsed AST
|
|
50
|
+
# @return [Array<Hash>] Folding ranges
|
|
51
|
+
def build_ast_folding_ranges(ast)
|
|
52
|
+
ranges = []
|
|
53
|
+
|
|
54
|
+
# Fold grammar rules with multiple alternatives
|
|
55
|
+
ast[:rules]&.each do |rule|
|
|
56
|
+
next unless rule[:location] && rule[:alternatives]
|
|
57
|
+
next if rule[:alternatives].size < 2
|
|
58
|
+
|
|
59
|
+
# Find the end of the rule (look for semicolon)
|
|
60
|
+
start_line = rule[:location][:line] - 1
|
|
61
|
+
end_line = find_rule_end_line(rule, ast)
|
|
62
|
+
|
|
63
|
+
# Only create a range if the rule spans multiple lines
|
|
64
|
+
ranges << create_folding_range(start_line, end_line, 'region') if end_line && end_line > start_line
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ranges
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Build folding ranges from text structure
|
|
71
|
+
# @param text [String] Document text
|
|
72
|
+
# @return [Array<Hash>] Folding ranges
|
|
73
|
+
def build_text_folding_ranges(text)
|
|
74
|
+
ranges = []
|
|
75
|
+
lines = text.lines
|
|
76
|
+
|
|
77
|
+
# Fold block comments
|
|
78
|
+
ranges.concat(find_comment_blocks(lines))
|
|
79
|
+
|
|
80
|
+
# Fold C code blocks (%{ ... %})
|
|
81
|
+
ranges.concat(find_c_code_blocks(lines))
|
|
82
|
+
|
|
83
|
+
# Fold action blocks ({ ... })
|
|
84
|
+
ranges.concat(find_action_blocks(lines))
|
|
85
|
+
|
|
86
|
+
ranges
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Find the end line of a rule
|
|
90
|
+
# @param rule [Hash] Rule
|
|
91
|
+
# @param ast [Hash] AST (for context)
|
|
92
|
+
# @return [Integer, nil] End line number or nil
|
|
93
|
+
def find_rule_end_line(rule, ast)
|
|
94
|
+
# Find the next rule's start line
|
|
95
|
+
rule_index = ast[:rules].index(rule)
|
|
96
|
+
return nil unless rule_index
|
|
97
|
+
|
|
98
|
+
if rule_index < ast[:rules].size - 1
|
|
99
|
+
next_rule = ast[:rules][rule_index + 1]
|
|
100
|
+
return next_rule[:location][:line] - 2 if next_rule[:location]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Last rule - use a default offset
|
|
104
|
+
rule[:location][:line] + 10
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Find block comment ranges
|
|
108
|
+
# @param lines [Array<String>] Document lines
|
|
109
|
+
# @return [Array<Hash>] Folding ranges
|
|
110
|
+
def find_comment_blocks(lines)
|
|
111
|
+
ranges = []
|
|
112
|
+
in_comment = false
|
|
113
|
+
comment_start = nil
|
|
114
|
+
|
|
115
|
+
lines.each_with_index do |line, idx|
|
|
116
|
+
if !in_comment && line.include?('/*')
|
|
117
|
+
in_comment = true
|
|
118
|
+
comment_start = idx
|
|
119
|
+
elsif in_comment && line.include?('*/')
|
|
120
|
+
in_comment = false
|
|
121
|
+
ranges << create_folding_range(comment_start, idx, 'comment') if comment_start && idx > comment_start
|
|
122
|
+
comment_start = nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
ranges
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Find C code block ranges
|
|
130
|
+
# @param lines [Array<String>] Document lines
|
|
131
|
+
# @return [Array<Hash>] Folding ranges
|
|
132
|
+
def find_c_code_blocks(lines)
|
|
133
|
+
ranges = []
|
|
134
|
+
in_block = false
|
|
135
|
+
block_start = nil
|
|
136
|
+
|
|
137
|
+
lines.each_with_index do |line, idx|
|
|
138
|
+
if !in_block && line.strip == '%{'
|
|
139
|
+
in_block = true
|
|
140
|
+
block_start = idx
|
|
141
|
+
elsif in_block && line.strip == '%}'
|
|
142
|
+
in_block = false
|
|
143
|
+
ranges << create_folding_range(block_start, idx, 'region') if block_start && idx > block_start
|
|
144
|
+
block_start = nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
ranges
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Find action block ranges (multi-line only)
|
|
152
|
+
# @param lines [Array<String>] Document lines
|
|
153
|
+
# @return [Array<Hash>] Folding ranges
|
|
154
|
+
def find_action_blocks(lines)
|
|
155
|
+
ranges = []
|
|
156
|
+
|
|
157
|
+
lines.each_with_index do |line, idx|
|
|
158
|
+
# Look for opening brace
|
|
159
|
+
brace_pos = line.index('{')
|
|
160
|
+
next unless brace_pos
|
|
161
|
+
|
|
162
|
+
# Find matching closing brace
|
|
163
|
+
end_line = find_matching_brace(lines, idx, brace_pos)
|
|
164
|
+
next unless end_line && end_line > idx
|
|
165
|
+
|
|
166
|
+
ranges << create_folding_range(idx, end_line, 'region')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
ranges
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Find matching closing brace
|
|
173
|
+
# @param lines [Array<String>] Document lines
|
|
174
|
+
# @param start_line [Integer] Line with opening brace
|
|
175
|
+
# @param start_pos [Integer] Position of opening brace
|
|
176
|
+
# @return [Integer, nil] Line with closing brace or nil
|
|
177
|
+
def find_matching_brace(lines, start_line, start_pos)
|
|
178
|
+
depth = 1
|
|
179
|
+
pos = start_pos + 1
|
|
180
|
+
|
|
181
|
+
(start_line...lines.length).each do |line_idx|
|
|
182
|
+
line = lines[line_idx]
|
|
183
|
+
start = line_idx == start_line ? pos : 0
|
|
184
|
+
|
|
185
|
+
(start...line.length).each do |char_idx|
|
|
186
|
+
char = line[char_idx]
|
|
187
|
+
depth += 1 if char == '{'
|
|
188
|
+
depth -= 1 if char == '}'
|
|
189
|
+
|
|
190
|
+
return line_idx if depth.zero?
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Create a folding range
|
|
198
|
+
# @param start_line [Integer] Start line (0-indexed)
|
|
199
|
+
# @param end_line [Integer] End line (0-indexed)
|
|
200
|
+
# @param kind [String] Folding range kind
|
|
201
|
+
# @return [Hash] LSP folding range
|
|
202
|
+
def create_folding_range(start_line, end_line, kind)
|
|
203
|
+
{
|
|
204
|
+
startLine: start_line,
|
|
205
|
+
endLine: end_line,
|
|
206
|
+
kind: kind
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CollieLsp
|
|
4
|
+
module Handlers
|
|
5
|
+
# Document formatting support
|
|
6
|
+
module Formatting
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Handle textDocument/formatting request
|
|
10
|
+
# @param request [Hash] LSP request
|
|
11
|
+
# @param document_store [DocumentStore] Document store
|
|
12
|
+
# @param collie [CollieWrapper] Collie wrapper
|
|
13
|
+
# @param writer [Object] Response writer
|
|
14
|
+
def handle(request, document_store, collie, writer)
|
|
15
|
+
uri = request[:params][:textDocument][:uri]
|
|
16
|
+
doc = document_store.get(uri)
|
|
17
|
+
|
|
18
|
+
unless doc
|
|
19
|
+
writer.write(id: request[:id], result: nil)
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
filename = uri.gsub(%r{^file://}, '')
|
|
24
|
+
formatted = collie.format(doc[:text], filename: filename)
|
|
25
|
+
|
|
26
|
+
unless formatted
|
|
27
|
+
writer.write(id: request[:id], result: nil)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Calculate text edits (replace entire document)
|
|
32
|
+
edits = [{
|
|
33
|
+
range: full_document_range(doc[:text]),
|
|
34
|
+
newText: formatted
|
|
35
|
+
}]
|
|
36
|
+
|
|
37
|
+
writer.write(
|
|
38
|
+
id: request[:id],
|
|
39
|
+
result: edits
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the range covering the entire document
|
|
44
|
+
# @param text [String] Document text
|
|
45
|
+
# @return [Hash] LSP range
|
|
46
|
+
def full_document_range(text)
|
|
47
|
+
lines = text.lines.count
|
|
48
|
+
{
|
|
49
|
+
start: { line: 0, character: 0 },
|
|
50
|
+
end: { line: lines, character: 0 }
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CollieLsp
|
|
4
|
+
module Handlers
|
|
5
|
+
# Show information on hover
|
|
6
|
+
module Hover
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Handle textDocument/hover request
|
|
10
|
+
# @param request [Hash] LSP request
|
|
11
|
+
# @param document_store [DocumentStore] Document store
|
|
12
|
+
# @param _collie [CollieWrapper] Collie wrapper (unused)
|
|
13
|
+
# @param writer [Object] Response writer
|
|
14
|
+
def handle(request, document_store, _collie, writer)
|
|
15
|
+
uri = request[:params][:textDocument][:uri]
|
|
16
|
+
position = request[:params][:position]
|
|
17
|
+
doc = document_store.get(uri)
|
|
18
|
+
|
|
19
|
+
unless doc
|
|
20
|
+
writer.write(id: request[:id], result: nil)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ast = doc[:ast]
|
|
25
|
+
unless ast
|
|
26
|
+
writer.write(id: request[:id], result: nil)
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find symbol at position
|
|
31
|
+
symbol = find_symbol_at_position(doc[:text], position)
|
|
32
|
+
unless symbol
|
|
33
|
+
writer.write(id: request[:id], result: nil)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
hover_content = build_hover_content(ast, symbol)
|
|
38
|
+
|
|
39
|
+
if hover_content
|
|
40
|
+
writer.write(
|
|
41
|
+
id: request[:id],
|
|
42
|
+
result: {
|
|
43
|
+
contents: hover_content
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
else
|
|
47
|
+
writer.write(id: request[:id], result: nil)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find symbol at the given position
|
|
52
|
+
# @param text [String] Document text
|
|
53
|
+
# @param position [Hash] LSP position
|
|
54
|
+
# @return [String, nil] Symbol name or nil
|
|
55
|
+
def find_symbol_at_position(text, position)
|
|
56
|
+
lines = text.lines
|
|
57
|
+
line = lines[position[:line]]
|
|
58
|
+
return nil unless line
|
|
59
|
+
|
|
60
|
+
# Extract word at character position
|
|
61
|
+
char = position[:character]
|
|
62
|
+
start_pos = char
|
|
63
|
+
end_pos = char
|
|
64
|
+
|
|
65
|
+
# Move backwards to find word start
|
|
66
|
+
start_pos -= 1 while start_pos.positive? && line[start_pos - 1] =~ /[A-Za-z0-9_]/
|
|
67
|
+
# Move forwards to find word end
|
|
68
|
+
end_pos += 1 while end_pos < line.length && line[end_pos] =~ /[A-Za-z0-9_]/
|
|
69
|
+
|
|
70
|
+
line[start_pos...end_pos]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build hover content for a symbol
|
|
74
|
+
# @param ast [Hash] Parsed AST
|
|
75
|
+
# @param symbol [String] Symbol name
|
|
76
|
+
# @return [Hash, nil] LSP markup content or nil
|
|
77
|
+
def build_hover_content(ast, symbol)
|
|
78
|
+
# Check if it's a token
|
|
79
|
+
ast[:declarations]&.each do |decl|
|
|
80
|
+
next unless decl[:kind] == :token
|
|
81
|
+
|
|
82
|
+
if decl[:names]&.include?(symbol)
|
|
83
|
+
return {
|
|
84
|
+
kind: 'markdown',
|
|
85
|
+
value: "**Token**: `#{symbol}`\n\nType: `#{decl[:type_tag] || 'none'}`"
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if it's a nonterminal
|
|
91
|
+
rule = ast[:rules]&.find { |r| r[:name] == symbol }
|
|
92
|
+
if rule
|
|
93
|
+
alt_count = rule[:alternatives]&.size || 0
|
|
94
|
+
return {
|
|
95
|
+
kind: 'markdown',
|
|
96
|
+
value: "**Nonterminal**: `#{symbol}`\n\n#{alt_count} alternative(s)"
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CollieLsp
|
|
4
|
+
module Handlers
|
|
5
|
+
# Find all references to a symbol
|
|
6
|
+
module References
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Handle textDocument/references request
|
|
10
|
+
# @param request [Hash] LSP request
|
|
11
|
+
# @param document_store [DocumentStore] Document store
|
|
12
|
+
# @param _collie [CollieWrapper] Collie wrapper (unused)
|
|
13
|
+
# @param writer [Object] Response writer
|
|
14
|
+
def handle(request, document_store, _collie, writer)
|
|
15
|
+
uri = request[:params][:textDocument][:uri]
|
|
16
|
+
position = request[:params][:position]
|
|
17
|
+
include_declaration = request[:params][:context][:includeDeclaration]
|
|
18
|
+
doc = document_store.get(uri)
|
|
19
|
+
|
|
20
|
+
unless doc
|
|
21
|
+
writer.write(id: request[:id], result: [])
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ast = doc[:ast]
|
|
26
|
+
unless ast
|
|
27
|
+
writer.write(id: request[:id], result: [])
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Find symbol at position
|
|
32
|
+
symbol = find_symbol_at_position(doc[:text], position)
|
|
33
|
+
unless symbol
|
|
34
|
+
writer.write(id: request[:id], result: [])
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
locations = find_references(ast, symbol, uri, doc[:text], include_declaration)
|
|
39
|
+
|
|
40
|
+
writer.write(
|
|
41
|
+
id: request[:id],
|
|
42
|
+
result: locations
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Find symbol at the given position
|
|
47
|
+
# @param text [String] Document text
|
|
48
|
+
# @param position [Hash] LSP position
|
|
49
|
+
# @return [String, nil] Symbol name or nil
|
|
50
|
+
def find_symbol_at_position(text, position)
|
|
51
|
+
lines = text.lines
|
|
52
|
+
line = lines[position[:line]]
|
|
53
|
+
return nil unless line
|
|
54
|
+
|
|
55
|
+
# Extract word at character position
|
|
56
|
+
char = position[:character]
|
|
57
|
+
start_pos = char
|
|
58
|
+
end_pos = char
|
|
59
|
+
|
|
60
|
+
# Move backwards to find word start
|
|
61
|
+
start_pos -= 1 while start_pos.positive? && line[start_pos - 1] =~ /[A-Za-z0-9_]/
|
|
62
|
+
# Move forwards to find word end
|
|
63
|
+
end_pos += 1 while end_pos < line.length && line[end_pos] =~ /[A-Za-z0-9_]/
|
|
64
|
+
|
|
65
|
+
line[start_pos...end_pos]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Find all references to a symbol
|
|
69
|
+
# @param ast [Hash] Parsed AST
|
|
70
|
+
# @param symbol [String] Symbol name
|
|
71
|
+
# @param uri [String] Document URI
|
|
72
|
+
# @param text [String] Document text
|
|
73
|
+
# @param include_declaration [Boolean] Include declaration in results
|
|
74
|
+
# @return [Array<Hash>] LSP locations
|
|
75
|
+
def find_references(ast, symbol, uri, text, include_declaration)
|
|
76
|
+
locations = []
|
|
77
|
+
|
|
78
|
+
# Find declaration location
|
|
79
|
+
declaration_loc = find_declaration(ast, symbol)
|
|
80
|
+
|
|
81
|
+
# Add declaration if requested
|
|
82
|
+
locations << create_location(uri, declaration_loc, symbol) if include_declaration && declaration_loc
|
|
83
|
+
|
|
84
|
+
# Find all usages in rules
|
|
85
|
+
locations.concat(find_usage_in_rules(ast, symbol, uri, text))
|
|
86
|
+
|
|
87
|
+
locations
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Find symbol usage in grammar rules
|
|
91
|
+
# @param ast [Hash] Parsed AST
|
|
92
|
+
# @param symbol [String] Symbol name
|
|
93
|
+
# @param uri [String] Document URI
|
|
94
|
+
# @param text [String] Document text
|
|
95
|
+
# @return [Array<Hash>] LSP locations
|
|
96
|
+
def find_usage_in_rules(ast, symbol, uri, text)
|
|
97
|
+
locations = []
|
|
98
|
+
|
|
99
|
+
ast[:rules]&.each do |rule|
|
|
100
|
+
rule[:alternatives]&.each_with_index do |alt, alt_index|
|
|
101
|
+
alt[:symbols]&.each_with_index do |sym, sym_index|
|
|
102
|
+
next unless sym[:name] == symbol
|
|
103
|
+
|
|
104
|
+
loc = estimate_symbol_location(text, rule, alt_index, sym_index, symbol)
|
|
105
|
+
locations << create_location(uri, loc, symbol) if loc
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
locations
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Find declaration location for a symbol
|
|
114
|
+
# @param ast [Hash] Parsed AST
|
|
115
|
+
# @param symbol [String] Symbol name
|
|
116
|
+
# @return [Hash, nil] Location hash or nil
|
|
117
|
+
def find_declaration(ast, symbol)
|
|
118
|
+
# Check token declarations
|
|
119
|
+
ast[:declarations]&.each do |decl|
|
|
120
|
+
next unless decl[:kind] == :token
|
|
121
|
+
|
|
122
|
+
return decl[:location] if decl[:names]&.include?(symbol) && decl[:location]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check nonterminal rules
|
|
126
|
+
rule = ast[:rules]&.find { |r| r[:name] == symbol }
|
|
127
|
+
return rule[:location] if rule && rule[:location]
|
|
128
|
+
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Estimate symbol location in text
|
|
133
|
+
# @param text [String] Document text
|
|
134
|
+
# @param rule [Hash] Rule containing the symbol
|
|
135
|
+
# @param _alt_index [Integer] Alternative index (unused)
|
|
136
|
+
# @param _sym_index [Integer] Symbol index (unused)
|
|
137
|
+
# @param symbol [String] Symbol name
|
|
138
|
+
# @return [Hash, nil] Location hash or nil
|
|
139
|
+
def estimate_symbol_location(text, rule, _alt_index, _sym_index, symbol)
|
|
140
|
+
# This is a simplified implementation that searches for the symbol
|
|
141
|
+
# In a real implementation, positions would be tracked during parsing
|
|
142
|
+
return nil unless rule[:location]
|
|
143
|
+
|
|
144
|
+
search_symbol_from_line(text.lines, rule[:location][:line] - 1, symbol)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Search for symbol starting from a specific line
|
|
148
|
+
# @param lines [Array<String>] Document lines
|
|
149
|
+
# @param start_line [Integer] Starting line number
|
|
150
|
+
# @param symbol [String] Symbol to search for
|
|
151
|
+
# @return [Hash, nil] Location hash or nil
|
|
152
|
+
def search_symbol_from_line(lines, start_line, symbol)
|
|
153
|
+
lines[start_line..].each_with_index do |line, offset|
|
|
154
|
+
col = line.index(symbol)
|
|
155
|
+
next unless col
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
line: start_line + offset + 1,
|
|
159
|
+
column: col + 1
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Create LSP location from position
|
|
167
|
+
# @param uri [String] Document URI
|
|
168
|
+
# @param location [Hash] Location hash with :line and :column
|
|
169
|
+
# @param symbol [String] Symbol name
|
|
170
|
+
# @return [Hash] LSP location
|
|
171
|
+
def create_location(uri, location, symbol)
|
|
172
|
+
line = location[:line] - 1
|
|
173
|
+
column = location[:column] - 1
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
uri: uri,
|
|
177
|
+
range: {
|
|
178
|
+
start: { line: line, character: column },
|
|
179
|
+
end: { line: line, character: column + symbol.length }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|