parsanol 1.3.96 → 1.3.98
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/Cargo.lock +4 -4
- data/lib/parsanol/parg/artifact.rb +3 -2
- data/lib/parsanol/parg/bindings.rb +3 -1
- data/lib/parsanol/parg/compiler.rb +9 -2
- data/lib/parsanol/parg/lexer.rb +1 -1
- data/lib/parsanol/parg/lsp.rb +464 -43
- data/lib/parsanol/parg/parser.rb +75 -7
- data/lib/parsanol/parg/selfhost.rb +2 -0
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 141dfea6902c1018accd9962fc346017dbb8d070303323df0d98e5f16bf12516
|
|
4
|
+
data.tar.gz: ec3ae0172d80d7c6c84743376958fb2ceb7dffd9072afbfe641fcf4e0267e079
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40e41398c910b644fe3be0a7042d0a2ed763e2e622d61e98d3efa01d5bf74301c2adc61dbfdd689f99987c6677a7737402d97f013050b4cd6080c3706ee656b9
|
|
7
|
+
data.tar.gz: 46e233a13a96163779eb6eca5d07687cc6fe9c7fbd3e88c81e20d2af9aa43939ddca682e1210b3d2e5cc5b1555c2660aad8cecc01d9ff45f0adc25b6d0fc9ff7
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.18.
|
|
310
|
+
version = "0.18.10"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "54bbd6459639572bc085c504112bc9a4acbca47719736aebad19d9113549bdad"
|
|
313
313
|
dependencies = [
|
|
314
314
|
"ahash",
|
|
315
315
|
"getrandom 0.3.4",
|
|
@@ -326,9 +326,9 @@ dependencies = [
|
|
|
326
326
|
|
|
327
327
|
[[package]]
|
|
328
328
|
name = "parsanol-derive"
|
|
329
|
-
version = "0.18.
|
|
329
|
+
version = "0.18.10"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "40adc63c6d2afcd1b095518d55f0095c5c04445f46994592161f0fa60cf6331b"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -178,8 +178,9 @@ module Parsanol
|
|
|
178
178
|
when "capture"
|
|
179
179
|
captures = Bindings.collect_captures(shape)
|
|
180
180
|
mismatched = hash["expect"].reject do |key, value|
|
|
181
|
-
|
|
182
|
-
!
|
|
181
|
+
values = captures[key.to_sym]
|
|
182
|
+
values && !values.empty? &&
|
|
183
|
+
Bindings.canonical(values.first) == Bindings.canonical(value)
|
|
183
184
|
end
|
|
184
185
|
next if mismatched.empty?
|
|
185
186
|
|
|
@@ -64,7 +64,9 @@ module Parsanol
|
|
|
64
64
|
count = group.map { |binding| captures[binding["capture"].to_sym].length }.max || 0
|
|
65
65
|
return if count.zero?
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# Field names are symbols everywhere else (scalar bindings, element
|
|
68
|
+
# fields); the array prefix must match or `example` lookups miss it.
|
|
69
|
+
list = (out[prefix.to_sym] ||= [])
|
|
68
70
|
count.times do |index|
|
|
69
71
|
element = {}
|
|
70
72
|
group.each do |binding|
|
|
@@ -337,6 +337,12 @@ module Parsanol
|
|
|
337
337
|
find_left_cycle_from(name, [], {})
|
|
338
338
|
end
|
|
339
339
|
|
|
340
|
+
# In-file test failures without raising: the LSP surfaces them as
|
|
341
|
+
# diagnostics instead of failing a build.
|
|
342
|
+
def test_failures
|
|
343
|
+
run_tests
|
|
344
|
+
end
|
|
345
|
+
|
|
340
346
|
private
|
|
341
347
|
|
|
342
348
|
# In-file grammar tests run at compile time: the build fails when an
|
|
@@ -372,8 +378,9 @@ module Parsanol
|
|
|
372
378
|
when :capture
|
|
373
379
|
captures = Bindings.collect_captures(shape)
|
|
374
380
|
mismatched = test.expect.reject do |key, value|
|
|
375
|
-
|
|
376
|
-
!
|
|
381
|
+
values = captures[key.to_sym]
|
|
382
|
+
values && !values.empty? &&
|
|
383
|
+
Bindings.canonical(values.first) == Bindings.canonical(value)
|
|
377
384
|
end
|
|
378
385
|
next if mismatched.empty?
|
|
379
386
|
|
data/lib/parsanol/parg/lexer.rb
CHANGED
data/lib/parsanol/parg/lsp.rb
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "
|
|
4
|
+
require "strscan"
|
|
5
|
+
require "uri"
|
|
5
6
|
|
|
6
7
|
module Parsanol
|
|
7
8
|
module PARG
|
|
8
|
-
#
|
|
9
|
+
# PARG language server over stdio (JSON-RPC 2.0, no framework).
|
|
9
10
|
#
|
|
10
11
|
# initialize / shutdown / exit
|
|
11
|
-
# textDocument/didOpen|didChange -> publishDiagnostics
|
|
12
|
-
# (parse errors, lint errors, failing inline tests)
|
|
13
|
-
# textDocument/hover
|
|
12
|
+
# textDocument/didOpen|didChange|didClose -> publishDiagnostics
|
|
13
|
+
# (parse errors, lint errors, failing inline tests; imports merged)
|
|
14
|
+
# textDocument/hover -> rule docs (Imports-aware)
|
|
15
|
+
# textDocument/definition -> rule definition / imported file
|
|
16
|
+
# textDocument/references -> rule occurrences
|
|
17
|
+
# textDocument/completion -> keywords, rule names, snippets
|
|
18
|
+
# textDocument/semanticTokens/full
|
|
19
|
+
# textDocument/codeAction -> longest-first reorder
|
|
14
20
|
#
|
|
15
|
-
#
|
|
16
|
-
# language implementation, every editor.
|
|
21
|
+
# Everything runs on the compiler API and the parsed Document model — one
|
|
22
|
+
# language implementation, every editor. Imported grammars are resolved
|
|
23
|
+
# from the document's directory and PG_IMPORT_DIR (path-separator list).
|
|
17
24
|
class Lsp
|
|
18
25
|
CONTENT_LENGTH = "Content-Length: "
|
|
19
26
|
|
|
27
|
+
SEMANTIC_TYPES = %w[keyword rule capture string number comment].freeze
|
|
28
|
+
SEMANTIC_MODIFIERS = %w[declaration].freeze
|
|
29
|
+
DECLARATION_BIT = 1
|
|
30
|
+
|
|
31
|
+
KEYWORDS = (%w[version test accept reject example use] + Parser::KEYWORDS).uniq.freeze
|
|
32
|
+
|
|
33
|
+
COMPLETION_KEYWORDS = KEYWORDS.map do |keyword|
|
|
34
|
+
{ "label" => keyword, "kind" => 14 }
|
|
35
|
+
end.freeze
|
|
36
|
+
|
|
37
|
+
SNIPPETS = [
|
|
38
|
+
["%i\"...\"", "%i\"${1:text}\"", "case-insensitive literal"],
|
|
39
|
+
["%x41", "%x41", "one code point"],
|
|
40
|
+
["%x41-5A", "%x41-5A", "code point range"],
|
|
41
|
+
["%x0D.0A", "%x0D.0A", "code point sequence"],
|
|
42
|
+
["[ ... ]", "[ ${1:content} ]", "optional"],
|
|
43
|
+
["*( ... )", "*( ${1:content} )", "zero or more"],
|
|
44
|
+
["1*( ... )", "1*( ${1:content} )", "one or more"],
|
|
45
|
+
["!x", "!${1:x}", "negative lookahead"],
|
|
46
|
+
["&x", "&${1:x}", "positive lookahead"],
|
|
47
|
+
["!<x", "!< ${1:x}", "negative behind-guard"],
|
|
48
|
+
["&<x", "&< ${1:x}", "positive behind-guard"],
|
|
49
|
+
["as name", "as ${1:name}", "capture"],
|
|
50
|
+
].map do |label, insert, detail|
|
|
51
|
+
{ "label" => label, "kind" => 15, "detail" => detail,
|
|
52
|
+
"insertText" => insert, "insertTextFormat" => 2 }
|
|
53
|
+
end.freeze
|
|
54
|
+
|
|
20
55
|
def initialize(input = $stdin, output = $stdout)
|
|
21
56
|
@input = input
|
|
22
57
|
@output = output
|
|
23
58
|
@documents = {}
|
|
59
|
+
@parsed = {}
|
|
60
|
+
@import_paths = {}
|
|
24
61
|
end
|
|
25
62
|
|
|
26
63
|
def run
|
|
@@ -45,6 +82,19 @@ module Parsanol
|
|
|
45
82
|
"capabilities" => {
|
|
46
83
|
"textDocumentSync" => 1,
|
|
47
84
|
"hoverProvider" => true,
|
|
85
|
+
"definitionProvider" => true,
|
|
86
|
+
"referencesProvider" => true,
|
|
87
|
+
"codeActionProvider" => true,
|
|
88
|
+
"completionProvider" => {
|
|
89
|
+
"triggerCharacters" => [".", "%", "\""],
|
|
90
|
+
},
|
|
91
|
+
"semanticTokensProvider" => {
|
|
92
|
+
"legend" => {
|
|
93
|
+
"tokenTypes" => SEMANTIC_TYPES,
|
|
94
|
+
"tokenModifiers" => SEMANTIC_MODIFIERS,
|
|
95
|
+
},
|
|
96
|
+
"full" => true,
|
|
97
|
+
},
|
|
48
98
|
},
|
|
49
99
|
"serverInfo" => { "name" => "parsanol-parg-lsp" },
|
|
50
100
|
})
|
|
@@ -56,17 +106,32 @@ module Parsanol
|
|
|
56
106
|
doc = message["params"]["textDocument"]
|
|
57
107
|
@documents[doc["uri"]] =
|
|
58
108
|
doc["text"] || message.dig("params", "contentChanges", -1, "text")
|
|
109
|
+
@parsed.delete(doc["uri"])
|
|
59
110
|
publish_diagnostics(doc["uri"])
|
|
111
|
+
when "textDocument/didClose"
|
|
112
|
+
uri = message.dig("params", "textDocument", "uri")
|
|
113
|
+
@documents.delete(uri)
|
|
114
|
+
@parsed.delete(uri)
|
|
60
115
|
when "textDocument/codeAction"
|
|
61
116
|
respond(message, code_action(message["params"]))
|
|
62
117
|
when "textDocument/hover"
|
|
63
|
-
respond(message, hover(message
|
|
64
|
-
|
|
118
|
+
respond(message, hover(*position_params(message["params"])))
|
|
119
|
+
when "textDocument/definition"
|
|
120
|
+
respond(message, definition(*position_params(message["params"])))
|
|
121
|
+
when "textDocument/references"
|
|
122
|
+
params = message["params"]
|
|
123
|
+
respond(message, references(params.dig("textDocument", "uri"),
|
|
124
|
+
params["position"],
|
|
125
|
+
params.dig("context", "includeDeclaration")))
|
|
126
|
+
when "textDocument/completion"
|
|
127
|
+
respond(message, completion(message.dig("params", "textDocument", "uri")))
|
|
128
|
+
when "textDocument/semanticTokens/full"
|
|
129
|
+
respond(message, semantic_tokens(message.dig("params", "textDocument", "uri")))
|
|
65
130
|
end
|
|
66
131
|
end
|
|
67
132
|
|
|
68
133
|
def respond(message, result)
|
|
69
|
-
send_message("id" => message["id"], "result" => result)
|
|
134
|
+
send_message("jsonrpc" => "2.0", "id" => message["id"], "result" => result)
|
|
70
135
|
end
|
|
71
136
|
|
|
72
137
|
def notify(method, params)
|
|
@@ -79,12 +144,379 @@ module Parsanol
|
|
|
79
144
|
@output.flush
|
|
80
145
|
end
|
|
81
146
|
|
|
147
|
+
# ---- documents and imports -------------------------------------------
|
|
148
|
+
|
|
149
|
+
def position_params(params)
|
|
150
|
+
[params.dig("textDocument", "uri"), params["position"]]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def document_source(uri)
|
|
154
|
+
return @documents[uri] if @documents.key?(uri)
|
|
155
|
+
|
|
156
|
+
path = @import_paths[uri] || file_path(uri)
|
|
157
|
+
return nil unless path && File.file?(path)
|
|
158
|
+
|
|
159
|
+
File.read(path)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def parsed(uri)
|
|
163
|
+
source = @documents[uri]
|
|
164
|
+
return nil unless source
|
|
165
|
+
|
|
166
|
+
cached = @parsed[uri]
|
|
167
|
+
return cached[1] if cached && cached[0] == source
|
|
168
|
+
|
|
169
|
+
document = begin
|
|
170
|
+
Parser.new(source).parse
|
|
171
|
+
rescue ParseError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
@parsed[uri] = [source, document]
|
|
175
|
+
document
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def file_path(uri)
|
|
179
|
+
text = uri.to_s
|
|
180
|
+
return nil unless text.start_with?("file://")
|
|
181
|
+
|
|
182
|
+
URI::DEFAULT_PARSER.unescape(text.delete_prefix("file://"))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def import_dirs(uri)
|
|
186
|
+
dirs = []
|
|
187
|
+
path = file_path(uri)
|
|
188
|
+
dirs << File.dirname(path) if path
|
|
189
|
+
ENV.fetch("PG_IMPORT_DIR", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
190
|
+
dirs << dir unless dir.to_s.empty?
|
|
191
|
+
end
|
|
192
|
+
dirs.uniq
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def import_uri(uri, name)
|
|
196
|
+
import_dirs(uri).each do |dir|
|
|
197
|
+
path = File.join(dir, "#{name}.parg")
|
|
198
|
+
next unless File.file?(path)
|
|
199
|
+
|
|
200
|
+
expanded = File.expand_path(path)
|
|
201
|
+
target = "file://#{URI::DEFAULT_PARSER.escape(expanded)}"
|
|
202
|
+
@import_paths[target] = expanded
|
|
203
|
+
return target
|
|
204
|
+
end
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ---- rule model ------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
Definition = Struct.new(:line, :character, :name_length, :docs)
|
|
211
|
+
|
|
212
|
+
# Rule definitions with their preceding ## doc comments. Works on a
|
|
213
|
+
# source that does not parse: hover/definition stay useful mid-edit.
|
|
214
|
+
def rule_definitions(source)
|
|
215
|
+
defs = {}
|
|
216
|
+
lines = source.to_s.lines
|
|
217
|
+
lines.each_with_index do |line, index|
|
|
218
|
+
match = line.match(/\A(\s*)(?:atomic\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)
|
|
219
|
+
next unless match
|
|
220
|
+
|
|
221
|
+
name = match[2]
|
|
222
|
+
docs = []
|
|
223
|
+
cursor = index - 1
|
|
224
|
+
while cursor.negative? == false && lines[cursor].to_s.strip.start_with?("##")
|
|
225
|
+
docs.unshift(lines[cursor].strip.sub(/\A##\s?/, ""))
|
|
226
|
+
cursor -= 1
|
|
227
|
+
end
|
|
228
|
+
defs[name] = Definition.new(index, match.begin(2), name.length, docs.join("\n"))
|
|
229
|
+
end
|
|
230
|
+
defs
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def rule_names(uri)
|
|
234
|
+
document = parsed(uri)
|
|
235
|
+
return document.rules.keys if document
|
|
236
|
+
|
|
237
|
+
rule_definitions(document_source(uri)).keys
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Resolve a (possibly dotted) rule reference to its defining file.
|
|
241
|
+
def resolve(uri, word)
|
|
242
|
+
segments = word.to_s.split(".").reject(&:empty?)
|
|
243
|
+
return nil if segments.empty?
|
|
244
|
+
|
|
245
|
+
resolve_segments(uri, segments)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def resolve_segments(uri, segments)
|
|
249
|
+
source = document_source(uri)
|
|
250
|
+
return nil unless source
|
|
251
|
+
|
|
252
|
+
if segments.size == 1
|
|
253
|
+
name = segments.first
|
|
254
|
+
defs = rule_definitions(source)
|
|
255
|
+
definition = defs[name]
|
|
256
|
+
return nil unless definition
|
|
257
|
+
|
|
258
|
+
return { uri: uri, name: name, definition: definition }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
document = if @documents.key?(uri)
|
|
262
|
+
parsed(uri)
|
|
263
|
+
else
|
|
264
|
+
begin
|
|
265
|
+
Parser.new(source).parse
|
|
266
|
+
rescue ParseError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
return nil unless document&.uses&.include?(segments.first)
|
|
271
|
+
|
|
272
|
+
target = import_uri(uri, segments.first)
|
|
273
|
+
return nil unless target
|
|
274
|
+
|
|
275
|
+
resolve_segments(target, segments[1..])
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def word_at(source, position)
|
|
279
|
+
return nil unless source && position
|
|
280
|
+
|
|
281
|
+
line = source.lines[position["line"].to_i].to_s
|
|
282
|
+
character = position["character"].to_i
|
|
283
|
+
left = line[0...character].to_s
|
|
284
|
+
right = line[character..].to_s
|
|
285
|
+
prefix = left[/[A-Za-z0-9_.]*\z/] || ""
|
|
286
|
+
suffix = right[/\A[A-Za-z0-9_.]*/] || ""
|
|
287
|
+
word = (prefix + suffix).sub(/\A\.+/, "").sub(/\.+\z/, "")
|
|
288
|
+
return nil if word.empty?
|
|
289
|
+
|
|
290
|
+
word
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def import_use_line?(source, position)
|
|
294
|
+
line = source.lines[position["line"].to_i].to_s
|
|
295
|
+
line.match?(/\A\s*use\s+/)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def location(uri, line, character, length)
|
|
299
|
+
{
|
|
300
|
+
"uri" => uri,
|
|
301
|
+
"range" => {
|
|
302
|
+
"start" => { "line" => line, "character" => character },
|
|
303
|
+
"end" => { "line" => line, "character" => character + length },
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# ---- hover / definition / references ---------------------------------
|
|
309
|
+
|
|
310
|
+
def hover(uri, position)
|
|
311
|
+
source = document_source(uri)
|
|
312
|
+
return nil unless source && position
|
|
313
|
+
|
|
314
|
+
word = word_at(source, position)
|
|
315
|
+
return nil unless word
|
|
316
|
+
|
|
317
|
+
if import_use_line?(source, position)
|
|
318
|
+
target = import_uri(uri, word)
|
|
319
|
+
return nil unless target
|
|
320
|
+
|
|
321
|
+
return { "contents" => { "kind" => "markdown",
|
|
322
|
+
"value" => "Import `#{word}`\n\n`#{File.basename(target)}`" } }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
resolved = resolve(uri, word)
|
|
326
|
+
return nil unless resolved
|
|
327
|
+
|
|
328
|
+
definition = resolved[:definition]
|
|
329
|
+
return nil unless definition
|
|
330
|
+
|
|
331
|
+
value = if definition.docs.empty?
|
|
332
|
+
"`#{resolved[:name]}` — rule"
|
|
333
|
+
else
|
|
334
|
+
"`#{resolved[:name]}`\n\n#{definition.docs}"
|
|
335
|
+
end
|
|
336
|
+
{ "contents" => { "kind" => "markdown", "value" => value } }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def definition(uri, position)
|
|
340
|
+
source = document_source(uri)
|
|
341
|
+
return nil unless source && position
|
|
342
|
+
|
|
343
|
+
word = word_at(source, position)
|
|
344
|
+
return nil unless word
|
|
345
|
+
|
|
346
|
+
if import_use_line?(source, position)
|
|
347
|
+
target = import_uri(uri, word)
|
|
348
|
+
return target ? location(target, 0, 0, 0) : nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
resolved = resolve(uri, word)
|
|
352
|
+
return nil unless resolved
|
|
353
|
+
|
|
354
|
+
definition = resolved[:definition]
|
|
355
|
+
location(resolved[:uri], definition.line, definition.character,
|
|
356
|
+
definition.name_length)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def references(uri, position, include_declaration)
|
|
360
|
+
source = document_source(uri)
|
|
361
|
+
return nil unless source && position
|
|
362
|
+
|
|
363
|
+
word = word_at(source, position)
|
|
364
|
+
resolved = word && resolve(uri, word)
|
|
365
|
+
return nil unless resolved
|
|
366
|
+
|
|
367
|
+
name = resolved[:name]
|
|
368
|
+
uris = [resolved[:uri], uri].uniq
|
|
369
|
+
results = []
|
|
370
|
+
uris.each do |target_uri|
|
|
371
|
+
target_source = document_source(target_uri)
|
|
372
|
+
next unless target_source
|
|
373
|
+
|
|
374
|
+
qualified = target_uri == uri && resolved[:uri] != uri && word.include?(".")
|
|
375
|
+
needle = qualified ? word : name
|
|
376
|
+
declaration = rule_definitions(target_source)[name]
|
|
377
|
+
target_source.lines.each_with_index do |line, index|
|
|
378
|
+
identifier_occurrences(line, needle).each do |character, length|
|
|
379
|
+
declaration_here = declaration && declaration.line == index &&
|
|
380
|
+
character == declaration.character && needle == name
|
|
381
|
+
results << location(target_uri, index, character, length) if
|
|
382
|
+
include_declaration || !declaration_here
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
results
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Identifier tokens outside strings and comments: a rule name inside a
|
|
390
|
+
# literal or a doc comment is not a reference.
|
|
391
|
+
def identifier_occurrences(line, name)
|
|
392
|
+
found = []
|
|
393
|
+
scanner = StringScanner.new(line)
|
|
394
|
+
until scanner.eos?
|
|
395
|
+
start = scanner.pos
|
|
396
|
+
start_char = line.byteslice(0, start).length
|
|
397
|
+
if scanner.scan(/#.*/)
|
|
398
|
+
break
|
|
399
|
+
elsif scanner.scan(/%(?:i|s)?"(?:\\.|[^"\\])*"/) ||
|
|
400
|
+
scanner.scan(/"(?:\\.|[^"\\])*"/) ||
|
|
401
|
+
scanner.scan(/%x[0-9A-Fa-f]{2}(?:[.-][0-9A-Fa-f]{2})*|[0-9]+/) ||
|
|
402
|
+
scanner.scan(/\s+/)
|
|
403
|
+
next
|
|
404
|
+
elsif (text = scanner.scan(/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/))
|
|
405
|
+
found << [start_char, text.length] if text == name
|
|
406
|
+
else
|
|
407
|
+
scanner.getch
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
found
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# ---- completion ------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def completion(uri)
|
|
416
|
+
source = document_source(uri)
|
|
417
|
+
return nil unless source
|
|
418
|
+
|
|
419
|
+
items = COMPLETION_KEYWORDS.dup + SNIPPETS.dup
|
|
420
|
+
rule_names(uri).each do |name|
|
|
421
|
+
items << { "label" => name, "kind" => 18, "detail" => "rule" }
|
|
422
|
+
end
|
|
423
|
+
document = parsed(uri)
|
|
424
|
+
(document&.uses || []).each do |import|
|
|
425
|
+
target = import_uri(uri, import)
|
|
426
|
+
next unless target
|
|
427
|
+
|
|
428
|
+
rule_definitions(document_source(target)).each_key do |name|
|
|
429
|
+
items << { "label" => "#{import}.#{name}", "kind" => 18,
|
|
430
|
+
"detail" => "imported rule" }
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
{ "isIncomplete" => false, "items" => items }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# ---- semantic tokens -------------------------------------------------
|
|
437
|
+
|
|
438
|
+
def semantic_tokens(uri)
|
|
439
|
+
source = document_source(uri)
|
|
440
|
+
return nil unless source
|
|
441
|
+
|
|
442
|
+
names = rule_names(uri)
|
|
443
|
+
tokens = []
|
|
444
|
+
source.lines.each_with_index do |line, line_no|
|
|
445
|
+
scan_line(line, line_no, names, tokens)
|
|
446
|
+
end
|
|
447
|
+
{ "data" => encode_tokens(tokens) }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def scan_line(line, line_no, names, tokens)
|
|
451
|
+
scanner = StringScanner.new(line)
|
|
452
|
+
previous_word = nil
|
|
453
|
+
until scanner.eos?
|
|
454
|
+
start = scanner.pos
|
|
455
|
+
start_char = line.byteslice(0, start).length
|
|
456
|
+
|
|
457
|
+
if (text = scanner.scan(/#.*/))
|
|
458
|
+
tokens << [line_no, start_char, text.length, "comment", 0]
|
|
459
|
+
break
|
|
460
|
+
elsif (text = scanner.scan(/%(?:i|s)?"(?:\\.|[^"\\])*"|"(?:\\.|[^"\\])*"/))
|
|
461
|
+
tokens << [line_no, start_char, text.length, "string", 0]
|
|
462
|
+
previous_word = nil
|
|
463
|
+
elsif (text = scanner.scan(/%x[0-9A-Fa-f]{2}(?:[.-][0-9A-Fa-f]{2})*|[0-9]+/))
|
|
464
|
+
tokens << [line_no, start_char, text.length, "number", 0]
|
|
465
|
+
previous_word = nil
|
|
466
|
+
elsif (text = scanner.scan(/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*/))
|
|
467
|
+
type, modifiers = classify_word(line, start_char, text, names, previous_word)
|
|
468
|
+
tokens << [line_no, start_char, text.length, type, modifiers] if type
|
|
469
|
+
previous_word = text
|
|
470
|
+
elsif (text = scanner.scan(/\s+/))
|
|
471
|
+
_ = text
|
|
472
|
+
else
|
|
473
|
+
scanner.getch
|
|
474
|
+
previous_word = nil
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def classify_word(line, start_char, text, names, previous_word)
|
|
480
|
+
base = text.split(".").last
|
|
481
|
+
if KEYWORDS.include?(text)
|
|
482
|
+
["keyword", 0]
|
|
483
|
+
elsif previous_word == "as"
|
|
484
|
+
["capture", 0]
|
|
485
|
+
elsif names.include?(base) || text.include?(".")
|
|
486
|
+
modifier = declaration_here?(line, start_char, text) ? DECLARATION_BIT : 0
|
|
487
|
+
["rule", modifier]
|
|
488
|
+
elsif %w[entry bindings test].include?(previous_word)
|
|
489
|
+
["rule", DECLARATION_BIT]
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def declaration_here?(line, start_char, text)
|
|
494
|
+
before = line[0...start_char].to_s
|
|
495
|
+
after = line[(start_char + text.length)..].to_s
|
|
496
|
+
before.match?(/\A\s*(?:atomic\s+)?\z/) && after.match?(/\A\s*=/)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def encode_tokens(tokens)
|
|
500
|
+
data = []
|
|
501
|
+
last_line = 0
|
|
502
|
+
last_char = 0
|
|
503
|
+
tokens.sort_by { |token| [token[0], token[1]] }.each do |line, char, length, type, modifiers|
|
|
504
|
+
delta_line = line - last_line
|
|
505
|
+
delta_char = delta_line.zero? ? char - last_char : char
|
|
506
|
+
data.push(delta_line, delta_char, length, SEMANTIC_TYPES.index(type), modifiers)
|
|
507
|
+
last_line = line
|
|
508
|
+
last_char = char
|
|
509
|
+
end
|
|
510
|
+
data
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# ---- diagnostics -----------------------------------------------------
|
|
514
|
+
|
|
82
515
|
def publish_diagnostics(uri)
|
|
83
516
|
source = @documents[uri]
|
|
84
517
|
diagnostics = []
|
|
85
|
-
document =
|
|
86
|
-
|
|
87
|
-
document = Parser.new(source).parse
|
|
518
|
+
document = begin
|
|
519
|
+
Parser.new(source).parse
|
|
88
520
|
rescue ParseError => e
|
|
89
521
|
line = 0
|
|
90
522
|
# The self-hosting artifact yields a precise offset (F7 wire);
|
|
@@ -97,16 +529,23 @@ module Parsanol
|
|
|
97
529
|
end
|
|
98
530
|
end
|
|
99
531
|
diagnostics << diagnostic(line, e.message)
|
|
532
|
+
nil
|
|
100
533
|
end
|
|
101
534
|
if document
|
|
535
|
+
dirs = import_dirs(uri)
|
|
102
536
|
begin
|
|
103
|
-
|
|
537
|
+
Imports.merge!(document, dirs) if document.uses.any?
|
|
538
|
+
rescue PARG::Error, ParseError => e
|
|
539
|
+
diagnostics << diagnostic(use_line(source, document), e.message)
|
|
540
|
+
end
|
|
541
|
+
begin
|
|
542
|
+
tables_dir = ENV.fetch("PG_TABLES_DIR", nil) || dirs.first
|
|
104
543
|
compiler = Compiler.new(document, tables_dir)
|
|
105
544
|
document.rules.each_key { |rule| compiler.atom_for(rule) }
|
|
106
545
|
Lints.errors(document, compiler).each do |error|
|
|
107
546
|
diagnostics << diagnostic(rule_line(source, error[/\Arule (\w+):/, 1]), error)
|
|
108
547
|
end
|
|
109
|
-
compiler.
|
|
548
|
+
compiler.test_failures.each do |failure|
|
|
110
549
|
diagnostics << diagnostic(0, failure)
|
|
111
550
|
end
|
|
112
551
|
rescue ParseError, PARG::Error => e
|
|
@@ -117,11 +556,18 @@ module Parsanol
|
|
|
117
556
|
"uri" => uri, "diagnostics" => diagnostics)
|
|
118
557
|
end
|
|
119
558
|
|
|
559
|
+
def use_line(source, document)
|
|
560
|
+
name = document.uses.first
|
|
561
|
+
return 0 unless name
|
|
562
|
+
|
|
563
|
+
source.lines.index { |line| line.match?(/^\s*use\s+#{Regexp.escape(name)}\b/) } || 0
|
|
564
|
+
end
|
|
565
|
+
|
|
120
566
|
# Rule-granular position: the line the named rule is defined on.
|
|
121
567
|
def rule_line(source, rule_name)
|
|
122
568
|
return 0 unless rule_name
|
|
123
569
|
|
|
124
|
-
source.lines.index { |
|
|
570
|
+
source.lines.index { |line| line.match?(/^\s*#{rule_name}\s*=/) } || 0
|
|
125
571
|
end
|
|
126
572
|
|
|
127
573
|
def diagnostic(line, message)
|
|
@@ -134,6 +580,8 @@ module Parsanol
|
|
|
134
580
|
}
|
|
135
581
|
end
|
|
136
582
|
|
|
583
|
+
# ---- code action -----------------------------------------------------
|
|
584
|
+
|
|
137
585
|
# Reorder code action: only when EVERY top-level alternative of the
|
|
138
586
|
# rule is a quoted literal is the reorder mechanically safe.
|
|
139
587
|
def code_action(params)
|
|
@@ -176,33 +624,6 @@ module Parsanol
|
|
|
176
624
|
{ "start" => { "line" => line_no, "character" => 0 },
|
|
177
625
|
"end" => { "line" => line_no, "character" => 1000 } }
|
|
178
626
|
end
|
|
179
|
-
|
|
180
|
-
def hover(uri, position)
|
|
181
|
-
source = @documents[uri]
|
|
182
|
-
return nil unless source && position
|
|
183
|
-
|
|
184
|
-
line_no = position.dig("position", "line") || position["line"]
|
|
185
|
-
text = source.lines[line_no.to_i].to_s
|
|
186
|
-
rule = text[/^\s*([a-z_]+)\s*=/, 1]
|
|
187
|
-
return nil unless rule
|
|
188
|
-
|
|
189
|
-
lines = source.lines
|
|
190
|
-
rule_line = lines.index { |l| l.match?(/^\s*#{rule}\s*=/) }
|
|
191
|
-
return nil unless rule_line
|
|
192
|
-
|
|
193
|
-
doc_lines = []
|
|
194
|
-
cursor = rule_line - 1
|
|
195
|
-
while cursor >= 0 && lines[cursor].strip.start_with?("##")
|
|
196
|
-
doc_lines.unshift(lines[cursor])
|
|
197
|
-
cursor -= 1
|
|
198
|
-
end
|
|
199
|
-
return nil if doc_lines.empty?
|
|
200
|
-
|
|
201
|
-
doc = doc_lines.map(&:strip).join("\n")
|
|
202
|
-
|
|
203
|
-
{ "contents" => { "kind" => "markdown",
|
|
204
|
-
"value" => doc.gsub(/^##\s?/, "").strip } }
|
|
205
|
-
end
|
|
206
627
|
end
|
|
207
628
|
end
|
|
208
629
|
end
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -91,6 +91,8 @@ module Parsanol
|
|
|
91
91
|
punct(":")
|
|
92
92
|
expect_pairs[key.to_sym] = parse_test_value
|
|
93
93
|
skip_newlines
|
|
94
|
+
advance if peek&.type == :punct && peek.value == ","
|
|
95
|
+
skip_newlines
|
|
94
96
|
end
|
|
95
97
|
punct("}")
|
|
96
98
|
end
|
|
@@ -101,18 +103,84 @@ module Parsanol
|
|
|
101
103
|
skip_newlines
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
# Expectation values: strings, numbers, true/false/null, arrays, and
|
|
107
|
+
# nested objects. Nested structure is what pins array bindings,
|
|
108
|
+
# absent-optional nil, and flatten/merge results in the corpus;
|
|
109
|
+
# before this the test grammar only accepted scalars.
|
|
104
110
|
def parse_test_value
|
|
105
|
-
token =
|
|
111
|
+
token = peek
|
|
106
112
|
case token&.type
|
|
107
|
-
when :str then unquote(
|
|
108
|
-
when :num then
|
|
109
|
-
when :
|
|
110
|
-
|
|
113
|
+
when :str then unquote(advance.value)
|
|
114
|
+
when :num then parse_test_number
|
|
115
|
+
when :punct
|
|
116
|
+
case token.value
|
|
117
|
+
when "[" then parse_test_array
|
|
118
|
+
when "{" then parse_test_object
|
|
119
|
+
when "-"
|
|
120
|
+
advance
|
|
121
|
+
unless peek&.type == :num
|
|
122
|
+
raise ParseError, "expected a number after '-' in a test value"
|
|
123
|
+
end
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
-parse_test_number
|
|
126
|
+
else
|
|
127
|
+
raise ParseError,
|
|
128
|
+
"expected a test value (string, number, true, false, null, array, object)"
|
|
129
|
+
end
|
|
130
|
+
when :ident
|
|
131
|
+
advance
|
|
132
|
+
case token.value
|
|
133
|
+
when "true" then true
|
|
134
|
+
when "false" then false
|
|
135
|
+
when "null" then nil
|
|
136
|
+
else
|
|
137
|
+
raise ParseError,
|
|
138
|
+
"expected true, false, or null, got #{token.value.inspect}"
|
|
139
|
+
end
|
|
113
140
|
else
|
|
114
|
-
raise ParseError,
|
|
141
|
+
raise ParseError,
|
|
142
|
+
"expected a test value (string, number, true, false, null, array, object)"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The lexer emits integers only; a decimal is num "." num.
|
|
147
|
+
def parse_test_number
|
|
148
|
+
text = advance.value
|
|
149
|
+
if peek&.type == :punct && peek.value == "." && @tokens[@pos + 1]&.type == :num
|
|
150
|
+
advance
|
|
151
|
+
text = "#{text}.#{advance.value}"
|
|
115
152
|
end
|
|
153
|
+
text.include?(".") ? Float(text) : Integer(text, 10)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_test_array
|
|
157
|
+
punct("[")
|
|
158
|
+
values = []
|
|
159
|
+
skip_newlines
|
|
160
|
+
until peek&.type == :punct && peek.value == "]"
|
|
161
|
+
values << parse_test_value
|
|
162
|
+
skip_newlines
|
|
163
|
+
advance if peek&.type == :punct && peek.value == ","
|
|
164
|
+
skip_newlines
|
|
165
|
+
end
|
|
166
|
+
punct("]")
|
|
167
|
+
values
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parse_test_object
|
|
171
|
+
punct("{")
|
|
172
|
+
value = {}
|
|
173
|
+
skip_newlines
|
|
174
|
+
until peek&.type == :punct && peek.value == "}"
|
|
175
|
+
key = ident.value
|
|
176
|
+
punct(":")
|
|
177
|
+
value[key.to_sym] = parse_test_value
|
|
178
|
+
skip_newlines
|
|
179
|
+
advance if peek&.type == :punct && peek.value == ","
|
|
180
|
+
skip_newlines
|
|
181
|
+
end
|
|
182
|
+
punct("}")
|
|
183
|
+
value
|
|
116
184
|
end
|
|
117
185
|
|
|
118
186
|
def eof? = @pos >= @tokens.length
|
data/lib/parsanol/version.rb
CHANGED