textbringer-tree-sitter 1.0.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/CLAUDE.md +76 -0
- data/LICENSE.txt +13 -0
- data/README.md +125 -0
- data/Rakefile +12 -0
- data/exe/textbringer-tree-sitter +513 -0
- data/ext/textbringer_tree_sitter/extconf.rb +125 -0
- data/lib/textbringer/tree_sitter/node_maps/bash.rb +57 -0
- data/lib/textbringer/tree_sitter/node_maps/c.rb +64 -0
- data/lib/textbringer/tree_sitter/node_maps/cobol.rb +94 -0
- data/lib/textbringer/tree_sitter/node_maps/csharp.rb +107 -0
- data/lib/textbringer/tree_sitter/node_maps/groovy.rb +77 -0
- data/lib/textbringer/tree_sitter/node_maps/haml.rb +34 -0
- data/lib/textbringer/tree_sitter/node_maps/hcl.rb +53 -0
- data/lib/textbringer/tree_sitter/node_maps/html.rb +33 -0
- data/lib/textbringer/tree_sitter/node_maps/java.rb +98 -0
- data/lib/textbringer/tree_sitter/node_maps/javascript.rb +82 -0
- data/lib/textbringer/tree_sitter/node_maps/json.rb +31 -0
- data/lib/textbringer/tree_sitter/node_maps/pascal.rb +102 -0
- data/lib/textbringer/tree_sitter/node_maps/php.rb +100 -0
- data/lib/textbringer/tree_sitter/node_maps/python.rb +72 -0
- data/lib/textbringer/tree_sitter/node_maps/ruby.rb +82 -0
- data/lib/textbringer/tree_sitter/node_maps/rust.rb +81 -0
- data/lib/textbringer/tree_sitter/node_maps/yaml.rb +45 -0
- data/lib/textbringer/tree_sitter/node_maps.rb +78 -0
- data/lib/textbringer/tree_sitter/version.rb +7 -0
- data/lib/textbringer/tree_sitter_adapter.rb +166 -0
- data/lib/textbringer/tree_sitter_config.rb +91 -0
- data/lib/textbringer_plugin.rb +141 -0
- data/parsers/darwin-arm64/.gitkeep +0 -0
- data/parsers/darwin-x64/.gitkeep +0 -0
- data/parsers/linux-arm64/.gitkeep +0 -0
- data/parsers/linux-x64/.gitkeep +0 -0
- data/scripts/build_parsers.sh +77 -0
- data/scripts/test_parser.rb +223 -0
- metadata +139 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open-uri"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require "open3"
|
|
9
|
+
|
|
10
|
+
module TextbringerTreeSitterCLI
|
|
11
|
+
FAVEOD_VERSION = "v4.11"
|
|
12
|
+
|
|
13
|
+
# Faveod tarball に含まれる parser
|
|
14
|
+
FAVEOD_PARSERS = %w[
|
|
15
|
+
bash c c-sharp cobol embedded-template groovy haml html
|
|
16
|
+
java javascript json pascal php python ruby rust
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# 要ビルド(Faveod に含まれない)
|
|
20
|
+
BUILD_PARSERS = {
|
|
21
|
+
hcl: {
|
|
22
|
+
repo: "mitchellh/tree-sitter-hcl",
|
|
23
|
+
branch: "main",
|
|
24
|
+
build_cmd: ->(src_dir, out_file) {
|
|
25
|
+
"c++ -shared -fPIC -O2 -std=c++14 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.cc -o #{out_file}"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
yaml: {
|
|
29
|
+
repo: "tree-sitter-grammars/tree-sitter-yaml",
|
|
30
|
+
branch: "master",
|
|
31
|
+
build_cmd: ->(src_dir, out_file) {
|
|
32
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.c -o #{out_file}"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
go: {
|
|
36
|
+
repo: "tree-sitter/tree-sitter-go",
|
|
37
|
+
branch: "master",
|
|
38
|
+
build_cmd: ->(src_dir, out_file) {
|
|
39
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c -o #{out_file}"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
typescript: {
|
|
43
|
+
repo: "tree-sitter/tree-sitter-typescript",
|
|
44
|
+
branch: "master",
|
|
45
|
+
subdir: "typescript",
|
|
46
|
+
build_cmd: ->(src_dir, out_file) {
|
|
47
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.c -o #{out_file}"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
tsx: {
|
|
51
|
+
repo: "tree-sitter/tree-sitter-typescript",
|
|
52
|
+
branch: "master",
|
|
53
|
+
subdir: "tsx",
|
|
54
|
+
build_cmd: ->(src_dir, out_file) {
|
|
55
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.c -o #{out_file}"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
sql: {
|
|
59
|
+
repo: "DerekStride/tree-sitter-sql",
|
|
60
|
+
branch: "main",
|
|
61
|
+
build_cmd: ->(src_dir, out_file) {
|
|
62
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.c -o #{out_file}"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
markdown: {
|
|
66
|
+
repo: "tree-sitter-grammars/tree-sitter-markdown",
|
|
67
|
+
branch: "split_parser",
|
|
68
|
+
commit: "9a23c1a", # LANGUAGE_VERSION 14 (ruby_tree_sitter 互換)
|
|
69
|
+
subdir: "tree-sitter-markdown",
|
|
70
|
+
build_cmd: ->(src_dir, out_file) {
|
|
71
|
+
"cc -shared -fPIC -O2 -I#{src_dir}/src #{src_dir}/src/parser.c #{src_dir}/src/scanner.c -o #{out_file}"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
def platform
|
|
78
|
+
os = case RbConfig::CONFIG["host_os"]
|
|
79
|
+
when /darwin/i then "darwin"
|
|
80
|
+
when /linux/i then "linux"
|
|
81
|
+
else "unknown"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
arch = case RbConfig::CONFIG["host_cpu"]
|
|
85
|
+
when /arm64|aarch64/i then "arm64"
|
|
86
|
+
when /x86_64|amd64/i then "x64"
|
|
87
|
+
else "unknown"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
"#{os}-#{arch}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def faveod_platform
|
|
94
|
+
case platform
|
|
95
|
+
when "darwin-arm64" then "macos-arm64"
|
|
96
|
+
when "darwin-x64" then "macos-x64"
|
|
97
|
+
when "linux-x64" then "linux-x64"
|
|
98
|
+
when "linux-arm64" then "linux-arm64"
|
|
99
|
+
else platform
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def dylib_ext
|
|
104
|
+
case RbConfig::CONFIG["host_os"]
|
|
105
|
+
when /darwin/i then ".dylib"
|
|
106
|
+
else ".so"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parser_dir
|
|
111
|
+
File.expand_path("~/.textbringer/parsers/#{platform}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parser_installed?(language)
|
|
115
|
+
# c-sharp -> csharp の変換
|
|
116
|
+
lang_name = language.to_s.gsub("-", "")
|
|
117
|
+
filename = "libtree-sitter-#{language}#{dylib_ext}"
|
|
118
|
+
File.exist?(File.join(parser_dir, filename))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def download_faveod_parser(language)
|
|
122
|
+
# Faveod tarball から特定の parser をインストール
|
|
123
|
+
filename = "libtree-sitter-#{language}#{dylib_ext}"
|
|
124
|
+
dest_path = File.join(parser_dir, filename)
|
|
125
|
+
|
|
126
|
+
if File.exist?(dest_path)
|
|
127
|
+
puts "#{language}: already installed"
|
|
128
|
+
return true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
url = "https://github.com/Faveod/tree-sitter-parsers/releases/download/#{FAVEOD_VERSION}/tree-sitter-parsers-#{FAVEOD_VERSION.delete('v')}-#{faveod_platform}.tar.gz"
|
|
132
|
+
|
|
133
|
+
puts "Downloading #{language} from Faveod..."
|
|
134
|
+
|
|
135
|
+
Dir.mktmpdir do |tmpdir|
|
|
136
|
+
tarball = File.join(tmpdir, "parsers.tar.gz")
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
URI.open(url, "rb") do |remote|
|
|
140
|
+
File.open(tarball, "wb") { |f| f.write(remote.read) }
|
|
141
|
+
end
|
|
142
|
+
rescue OpenURI::HTTPError => e
|
|
143
|
+
puts " Error: Failed to download: #{e.message}"
|
|
144
|
+
return false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
extract_dir = File.join(tmpdir, "extracted")
|
|
148
|
+
FileUtils.mkdir_p(extract_dir)
|
|
149
|
+
system("tar", "-xzf", tarball, "-C", extract_dir, out: File::NULL, err: File::NULL)
|
|
150
|
+
|
|
151
|
+
src = Dir.glob("#{extract_dir}/**/#{filename}").first
|
|
152
|
+
if src
|
|
153
|
+
FileUtils.mkdir_p(parser_dir)
|
|
154
|
+
FileUtils.cp(src, dest_path)
|
|
155
|
+
FileUtils.chmod(0o755, dest_path)
|
|
156
|
+
puts " -> #{dest_path}"
|
|
157
|
+
true
|
|
158
|
+
else
|
|
159
|
+
puts " Error: #{language} not found in tarball"
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_parser(language)
|
|
166
|
+
lang_sym = language.to_s.gsub("-", "_").to_sym
|
|
167
|
+
info = BUILD_PARSERS[lang_sym]
|
|
168
|
+
|
|
169
|
+
unless info
|
|
170
|
+
puts "Error: Unknown build-required language: #{language}"
|
|
171
|
+
puts "Available: #{BUILD_PARSERS.keys.join(', ')}"
|
|
172
|
+
return false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
filename = "libtree-sitter-#{language}#{dylib_ext}"
|
|
176
|
+
dest_path = File.join(parser_dir, filename)
|
|
177
|
+
|
|
178
|
+
if File.exist?(dest_path)
|
|
179
|
+
puts "#{language}: already installed"
|
|
180
|
+
return true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
puts "Building #{language} parser..."
|
|
184
|
+
puts " Repository: #{info[:repo]}"
|
|
185
|
+
|
|
186
|
+
Dir.mktmpdir do |tmpdir|
|
|
187
|
+
repo_dir = File.join(tmpdir, "repo")
|
|
188
|
+
|
|
189
|
+
puts " Cloning..."
|
|
190
|
+
if info[:commit]
|
|
191
|
+
# 特定コミットが必要な場合は shallow clone できない
|
|
192
|
+
_, status = Open3.capture2e("git", "clone", "-b", info[:branch],
|
|
193
|
+
"https://github.com/#{info[:repo]}.git", repo_dir)
|
|
194
|
+
unless status.success?
|
|
195
|
+
puts " Error: Failed to clone repository"
|
|
196
|
+
return false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
puts " Checking out #{info[:commit]}..."
|
|
200
|
+
_, status = Open3.capture2e("git", "-C", repo_dir, "checkout", info[:commit])
|
|
201
|
+
unless status.success?
|
|
202
|
+
puts " Error: Failed to checkout commit #{info[:commit]}"
|
|
203
|
+
return false
|
|
204
|
+
end
|
|
205
|
+
else
|
|
206
|
+
_, status = Open3.capture2e("git", "clone", "--depth", "1", "-b", info[:branch],
|
|
207
|
+
"https://github.com/#{info[:repo]}.git", repo_dir)
|
|
208
|
+
unless status.success?
|
|
209
|
+
puts " Error: Failed to clone repository"
|
|
210
|
+
return false
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
src_dir = info[:subdir] ? File.join(repo_dir, info[:subdir]) : repo_dir
|
|
215
|
+
build_cmd = info[:build_cmd].call(src_dir, dest_path)
|
|
216
|
+
|
|
217
|
+
puts " Building..."
|
|
218
|
+
FileUtils.mkdir_p(parser_dir)
|
|
219
|
+
output, status = Open3.capture2e(build_cmd)
|
|
220
|
+
|
|
221
|
+
unless status.success?
|
|
222
|
+
puts " Error: Build failed"
|
|
223
|
+
puts output
|
|
224
|
+
return false
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
FileUtils.chmod(0o755, dest_path)
|
|
228
|
+
puts " -> #{dest_path}"
|
|
229
|
+
true
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_parser(language)
|
|
234
|
+
lang = language.to_s
|
|
235
|
+
|
|
236
|
+
# Faveod に含まれるか
|
|
237
|
+
if FAVEOD_PARSERS.include?(lang)
|
|
238
|
+
download_faveod_parser(lang)
|
|
239
|
+
# ビルドが必要か
|
|
240
|
+
elsif BUILD_PARSERS.key?(lang.gsub("-", "_").to_sym)
|
|
241
|
+
build_parser(lang)
|
|
242
|
+
else
|
|
243
|
+
puts "Error: Unknown language: #{language}"
|
|
244
|
+
puts ""
|
|
245
|
+
puts "Faveod prebuilt: #{FAVEOD_PARSERS.join(', ')}"
|
|
246
|
+
puts "Build required: #{BUILD_PARSERS.keys.join(', ')}"
|
|
247
|
+
false
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def list_parsers
|
|
252
|
+
puts "=== Faveod Prebuilt Parsers ==="
|
|
253
|
+
FAVEOD_PARSERS.each do |lang|
|
|
254
|
+
status = parser_installed?(lang) ? "✓" : " "
|
|
255
|
+
puts " [#{status}] #{lang}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
puts ""
|
|
259
|
+
puts "=== Build-required Parsers ==="
|
|
260
|
+
BUILD_PARSERS.keys.sort.each do |lang|
|
|
261
|
+
status = parser_installed?(lang) ? "✓" : " "
|
|
262
|
+
puts " [#{status}] #{lang}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
puts ""
|
|
266
|
+
puts "Use 'textbringer-tree-sitter get <lang>' to install"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def get_all
|
|
270
|
+
puts "Installing all Faveod prebuilt parsers..."
|
|
271
|
+
puts ""
|
|
272
|
+
|
|
273
|
+
url = "https://github.com/Faveod/tree-sitter-parsers/releases/download/#{FAVEOD_VERSION}/tree-sitter-parsers-#{FAVEOD_VERSION.delete('v')}-#{faveod_platform}.tar.gz"
|
|
274
|
+
|
|
275
|
+
puts "Downloading from Faveod..."
|
|
276
|
+
puts " URL: #{url}"
|
|
277
|
+
|
|
278
|
+
Dir.mktmpdir do |tmpdir|
|
|
279
|
+
tarball = File.join(tmpdir, "parsers.tar.gz")
|
|
280
|
+
|
|
281
|
+
begin
|
|
282
|
+
URI.open(url, "rb") do |remote|
|
|
283
|
+
File.open(tarball, "wb") { |f| f.write(remote.read) }
|
|
284
|
+
end
|
|
285
|
+
rescue OpenURI::HTTPError => e
|
|
286
|
+
puts " Error: Failed to download: #{e.message}"
|
|
287
|
+
return
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
extract_dir = File.join(tmpdir, "extracted")
|
|
291
|
+
FileUtils.mkdir_p(extract_dir)
|
|
292
|
+
system("tar", "-xzf", tarball, "-C", extract_dir, out: File::NULL, err: File::NULL)
|
|
293
|
+
|
|
294
|
+
FileUtils.mkdir_p(parser_dir)
|
|
295
|
+
|
|
296
|
+
Dir.glob("#{extract_dir}/**/libtree-sitter-*#{dylib_ext}").each do |src|
|
|
297
|
+
filename = File.basename(src)
|
|
298
|
+
dest = File.join(parser_dir, filename)
|
|
299
|
+
|
|
300
|
+
if File.exist?(dest)
|
|
301
|
+
puts " #{filename} -> already installed"
|
|
302
|
+
else
|
|
303
|
+
FileUtils.cp(src, dest)
|
|
304
|
+
FileUtils.chmod(0o755, dest)
|
|
305
|
+
puts " #{filename} -> OK"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
puts ""
|
|
311
|
+
puts "Done!"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def node_map_dir
|
|
315
|
+
File.expand_path("~/.textbringer/tree_sitter/node_maps")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def guess_face(node_name)
|
|
319
|
+
name = node_name.to_s.downcase
|
|
320
|
+
case name
|
|
321
|
+
when /comment/
|
|
322
|
+
:comment
|
|
323
|
+
when /string|heredoc|char_literal|template_string/
|
|
324
|
+
:string
|
|
325
|
+
when /number|integer|float|decimal|numeric/
|
|
326
|
+
:number
|
|
327
|
+
when /\b(if|else|elsif|unless|case|when|while|until|for|do|end|begin|rescue|ensure|return|yield|break|next|redo|retry|raise|class|module|def|alias|defined|super|self|nil|true|false|and|or|not|in|fn|func|function|let|const|var|import|export|from|as|try|catch|finally|throw|async|await|match|loop|struct|enum|impl|trait|pub|mut|ref|use|mod|crate|where|type|interface|package|extends|implements|static|final|abstract|native|synchronized|volatile|transient|new|this|instanceof|goto|switch|default|continue|assert|with|pass|lambda|nonlocal|global|del|except|exec|print|elif|is)\b/
|
|
328
|
+
:keyword
|
|
329
|
+
when /keyword|reserved/
|
|
330
|
+
:keyword
|
|
331
|
+
when /constant|boolean/
|
|
332
|
+
:constant
|
|
333
|
+
when /function_name|method_name|call|invocation/
|
|
334
|
+
:function_name
|
|
335
|
+
when /type|class_name|struct_name|interface_name/
|
|
336
|
+
:type
|
|
337
|
+
when /variable|identifier|name/
|
|
338
|
+
:variable
|
|
339
|
+
when /operator|binary_op|unary_op/
|
|
340
|
+
:operator
|
|
341
|
+
when /punctuation|delimiter|bracket|paren|brace/
|
|
342
|
+
:punctuation
|
|
343
|
+
when /marker|heading|header/
|
|
344
|
+
:keyword
|
|
345
|
+
else
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def generate_node_map(language)
|
|
351
|
+
lang = language.to_s.gsub("-", "_")
|
|
352
|
+
filename = "libtree-sitter-#{language}#{dylib_ext}"
|
|
353
|
+
parser_path = File.join(parser_dir, filename)
|
|
354
|
+
|
|
355
|
+
unless File.exist?(parser_path)
|
|
356
|
+
puts "Error: Parser not installed. Run 'textbringer-tree-sitter get #{language}' first."
|
|
357
|
+
return false
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
require "tree_sitter"
|
|
361
|
+
|
|
362
|
+
begin
|
|
363
|
+
ts_lang = TreeSitter::Language.load(lang, parser_path)
|
|
364
|
+
rescue => e
|
|
365
|
+
puts "Error: Failed to load parser: #{e.message}"
|
|
366
|
+
return false
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# ノードタイプを収集してヒューリスティックでマッピング
|
|
370
|
+
mappings = {}
|
|
371
|
+
unmapped = []
|
|
372
|
+
|
|
373
|
+
ts_lang.symbol_count.times do |i|
|
|
374
|
+
name = ts_lang.symbol_name(i)
|
|
375
|
+
next if name.nil? || name.empty?
|
|
376
|
+
next if name.start_with?("_") # internal nodes
|
|
377
|
+
next if name.length == 1 # single char tokens
|
|
378
|
+
next if name =~ /^[^a-zA-Z]/ # non-alphabetic
|
|
379
|
+
|
|
380
|
+
face = guess_face(name)
|
|
381
|
+
if face
|
|
382
|
+
mappings[face] ||= []
|
|
383
|
+
mappings[face] << name unless mappings[face].include?(name)
|
|
384
|
+
else
|
|
385
|
+
unmapped << name unless unmapped.include?(name)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# テンプレート生成
|
|
390
|
+
const_name = lang.upcase
|
|
391
|
+
output = <<~RUBY
|
|
392
|
+
# frozen_string_literal: true
|
|
393
|
+
|
|
394
|
+
# Auto-generated node map for #{language}
|
|
395
|
+
# Generated by: textbringer-tree-sitter get #{language} --generate-map
|
|
396
|
+
#
|
|
397
|
+
# Review and customize as needed.
|
|
398
|
+
# Uncommented mappings are heuristic guesses.
|
|
399
|
+
# Commented lines are unmapped nodes.
|
|
400
|
+
|
|
401
|
+
module Textbringer
|
|
402
|
+
module TreeSitter
|
|
403
|
+
module NodeMaps
|
|
404
|
+
#{const_name}_FEATURES = {
|
|
405
|
+
RUBY
|
|
406
|
+
|
|
407
|
+
# マッピング済みを出力
|
|
408
|
+
mappings.each do |face, nodes|
|
|
409
|
+
nodes_str = nodes.join(" ")
|
|
410
|
+
output << " #{face}: %i[#{nodes_str}],\n"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
output << " }.freeze\n\n"
|
|
414
|
+
|
|
415
|
+
# 未マッピングをコメントで出力
|
|
416
|
+
if unmapped.any?
|
|
417
|
+
output << " # Unmapped nodes (add to appropriate face if needed):\n"
|
|
418
|
+
unmapped.each do |name|
|
|
419
|
+
output << " # #{name}\n"
|
|
420
|
+
end
|
|
421
|
+
output << "\n"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
output << <<~RUBY
|
|
425
|
+
#{const_name} = #{const_name}_FEATURES.flat_map { |face, nodes|
|
|
426
|
+
nodes.map { |node| [node, face] }
|
|
427
|
+
}.to_h.freeze
|
|
428
|
+
|
|
429
|
+
# 自動登録
|
|
430
|
+
register(:#{lang}, #{const_name})
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
RUBY
|
|
435
|
+
|
|
436
|
+
# ファイルに書き出し
|
|
437
|
+
FileUtils.mkdir_p(node_map_dir)
|
|
438
|
+
dest_path = File.join(node_map_dir, "#{lang}.rb")
|
|
439
|
+
File.write(dest_path, output)
|
|
440
|
+
|
|
441
|
+
puts "Generated node map: #{dest_path}"
|
|
442
|
+
puts " Mapped: #{mappings.values.flatten.size} nodes"
|
|
443
|
+
puts " Unmapped: #{unmapped.size} nodes (commented)"
|
|
444
|
+
puts ""
|
|
445
|
+
puts "Edit the file to customize mappings, then add to ~/.textbringer.rb:"
|
|
446
|
+
puts " require '#{dest_path}'"
|
|
447
|
+
true
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def show_help
|
|
451
|
+
puts <<~HELP
|
|
452
|
+
textbringer-tree-sitter - Parser manager for textbringer-tree-sitter
|
|
453
|
+
|
|
454
|
+
Usage:
|
|
455
|
+
textbringer-tree-sitter list List available parsers
|
|
456
|
+
textbringer-tree-sitter get <lang> Download/build parser + generate node_map
|
|
457
|
+
textbringer-tree-sitter get <lang> --no-map Download/build parser only (skip node_map)
|
|
458
|
+
textbringer-tree-sitter get-all Install all Faveod prebuilt parsers
|
|
459
|
+
textbringer-tree-sitter generate-map <lang> (Re)generate node_map for installed parser
|
|
460
|
+
textbringer-tree-sitter path Show parser directory
|
|
461
|
+
|
|
462
|
+
Examples:
|
|
463
|
+
textbringer-tree-sitter get markdown Build parser + generate node_map
|
|
464
|
+
textbringer-tree-sitter get ruby Download Ruby parser + generate node_map
|
|
465
|
+
textbringer-tree-sitter get-all Install all prebuilt parsers (no node_maps)
|
|
466
|
+
HELP
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def run(args)
|
|
470
|
+
command = args[0]
|
|
471
|
+
|
|
472
|
+
case command
|
|
473
|
+
when "list"
|
|
474
|
+
list_parsers
|
|
475
|
+
when "get"
|
|
476
|
+
lang = args[1]
|
|
477
|
+
if lang.nil?
|
|
478
|
+
puts "Error: Please specify a language"
|
|
479
|
+
puts "Usage: textbringer-tree-sitter get <lang>"
|
|
480
|
+
exit 1
|
|
481
|
+
end
|
|
482
|
+
skip_map = args.include?("--no-map")
|
|
483
|
+
success = get_parser(lang)
|
|
484
|
+
if success && !skip_map
|
|
485
|
+
puts ""
|
|
486
|
+
generate_node_map(lang)
|
|
487
|
+
end
|
|
488
|
+
exit(success ? 0 : 1)
|
|
489
|
+
when "get-all"
|
|
490
|
+
get_all
|
|
491
|
+
when "generate-map"
|
|
492
|
+
lang = args[1]
|
|
493
|
+
if lang.nil?
|
|
494
|
+
puts "Error: Please specify a language"
|
|
495
|
+
puts "Usage: textbringer-tree-sitter generate-map <lang>"
|
|
496
|
+
exit 1
|
|
497
|
+
end
|
|
498
|
+
success = generate_node_map(lang)
|
|
499
|
+
exit(success ? 0 : 1)
|
|
500
|
+
when "path"
|
|
501
|
+
puts parser_dir
|
|
502
|
+
when "help", "--help", "-h", nil
|
|
503
|
+
show_help
|
|
504
|
+
else
|
|
505
|
+
puts "Unknown command: #{command}"
|
|
506
|
+
show_help
|
|
507
|
+
exit 1
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
TextbringerTreeSitterCLI.run(ARGV)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# gem install 時にプリビルド済み parser を自動ダウンロード
|
|
5
|
+
# Faveod/tree-sitter-parsers から tarball を取得して展開
|
|
6
|
+
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "open-uri"
|
|
9
|
+
require "rbconfig"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
|
|
12
|
+
def platform
|
|
13
|
+
os = case RbConfig::CONFIG["host_os"]
|
|
14
|
+
when /darwin/i then "darwin"
|
|
15
|
+
when /linux/i then "linux"
|
|
16
|
+
else "unknown"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
arch = case RbConfig::CONFIG["host_cpu"]
|
|
20
|
+
when /arm64|aarch64/i then "arm64"
|
|
21
|
+
when /x86_64|amd64/i then "x64"
|
|
22
|
+
else "unknown"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
"#{os}-#{arch}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def faveod_platform
|
|
29
|
+
case platform
|
|
30
|
+
when "darwin-arm64" then "macos-arm64"
|
|
31
|
+
when "darwin-x64" then "macos-x64"
|
|
32
|
+
when "linux-x64" then "linux-x64"
|
|
33
|
+
when "linux-arm64" then "linux-arm64"
|
|
34
|
+
else platform
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dylib_ext
|
|
39
|
+
case RbConfig::CONFIG["host_os"]
|
|
40
|
+
when /darwin/i then ".dylib"
|
|
41
|
+
else ".so"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
PARSER_DIR = File.expand_path("~/.textbringer/parsers/#{platform}")
|
|
46
|
+
FAVEOD_VERSION = "v4.11"
|
|
47
|
+
|
|
48
|
+
# 自動インストールする言語
|
|
49
|
+
DEFAULT_PARSERS = %w[ruby python javascript json bash]
|
|
50
|
+
|
|
51
|
+
def download_and_extract_parsers
|
|
52
|
+
url = "https://github.com/Faveod/tree-sitter-parsers/releases/download/#{FAVEOD_VERSION}/tree-sitter-parsers-#{FAVEOD_VERSION.delete('v')}-#{faveod_platform}.tar.gz"
|
|
53
|
+
|
|
54
|
+
puts " Downloading parsers from Faveod..."
|
|
55
|
+
puts " URL: #{url}"
|
|
56
|
+
|
|
57
|
+
Dir.mktmpdir do |tmpdir|
|
|
58
|
+
tarball = File.join(tmpdir, "parsers.tar.gz")
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
URI.open(url, "rb") do |remote|
|
|
62
|
+
File.open(tarball, "wb") do |local|
|
|
63
|
+
local.write(remote.read)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue OpenURI::HTTPError => e
|
|
67
|
+
puts " Error: Failed to download: #{e.message}"
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 展開
|
|
72
|
+
extract_dir = File.join(tmpdir, "extracted")
|
|
73
|
+
FileUtils.mkdir_p(extract_dir)
|
|
74
|
+
|
|
75
|
+
system("tar", "-xzf", tarball, "-C", extract_dir)
|
|
76
|
+
|
|
77
|
+
# parser ファイルを探してコピー
|
|
78
|
+
Dir.glob("#{extract_dir}/**/libtree-sitter-*#{dylib_ext}").each do |src|
|
|
79
|
+
filename = File.basename(src)
|
|
80
|
+
# libtree-sitter-{lang}.dylib の形式から lang を抽出
|
|
81
|
+
lang = filename.sub(/^libtree-sitter-/, "").sub(/#{Regexp.escape(dylib_ext)}$/, "")
|
|
82
|
+
|
|
83
|
+
if DEFAULT_PARSERS.include?(lang)
|
|
84
|
+
dest = File.join(PARSER_DIR, filename)
|
|
85
|
+
unless File.exist?(dest)
|
|
86
|
+
FileUtils.cp(src, dest)
|
|
87
|
+
FileUtils.chmod(0o755, dest)
|
|
88
|
+
puts " #{lang} -> OK"
|
|
89
|
+
else
|
|
90
|
+
puts " #{lang} -> already installed"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
puts ""
|
|
100
|
+
puts "=" * 60
|
|
101
|
+
puts "textbringer-tree-sitter: Installing default parsers"
|
|
102
|
+
puts "=" * 60
|
|
103
|
+
puts "Platform: #{platform}"
|
|
104
|
+
puts "Directory: #{PARSER_DIR}"
|
|
105
|
+
puts ""
|
|
106
|
+
|
|
107
|
+
FileUtils.mkdir_p(PARSER_DIR)
|
|
108
|
+
download_and_extract_parsers
|
|
109
|
+
|
|
110
|
+
puts ""
|
|
111
|
+
puts "For additional parsers (HCL, YAML, Go, etc.):"
|
|
112
|
+
puts " $ textbringer-tree-sitter get hcl"
|
|
113
|
+
puts " $ textbringer-tree-sitter list"
|
|
114
|
+
puts "=" * 60
|
|
115
|
+
puts ""
|
|
116
|
+
|
|
117
|
+
# extconf.rb は Makefile を生成する必要がある
|
|
118
|
+
File.write("Makefile", <<~MAKEFILE)
|
|
119
|
+
all:
|
|
120
|
+
\t@:
|
|
121
|
+
install:
|
|
122
|
+
\t@:
|
|
123
|
+
clean:
|
|
124
|
+
\t@:
|
|
125
|
+
MAKEFILE
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textbringer
|
|
4
|
+
module TreeSitter
|
|
5
|
+
module NodeMaps
|
|
6
|
+
BASH_FEATURES = {
|
|
7
|
+
comment: %i[comment],
|
|
8
|
+
string: %i[
|
|
9
|
+
string
|
|
10
|
+
raw_string
|
|
11
|
+
heredoc_body
|
|
12
|
+
heredoc_start
|
|
13
|
+
ansi_c_string
|
|
14
|
+
],
|
|
15
|
+
keyword: %i[
|
|
16
|
+
if
|
|
17
|
+
then
|
|
18
|
+
else
|
|
19
|
+
elif
|
|
20
|
+
fi
|
|
21
|
+
for
|
|
22
|
+
while
|
|
23
|
+
until
|
|
24
|
+
do
|
|
25
|
+
done
|
|
26
|
+
case
|
|
27
|
+
esac
|
|
28
|
+
in
|
|
29
|
+
function
|
|
30
|
+
select
|
|
31
|
+
time
|
|
32
|
+
coproc
|
|
33
|
+
],
|
|
34
|
+
number: %i[],
|
|
35
|
+
constant: %i[],
|
|
36
|
+
function_name: %i[function_definition],
|
|
37
|
+
variable: %i[
|
|
38
|
+
variable_name
|
|
39
|
+
special_variable_name
|
|
40
|
+
],
|
|
41
|
+
type: %i[],
|
|
42
|
+
operator: %i[
|
|
43
|
+
file_redirect
|
|
44
|
+
heredoc_redirect
|
|
45
|
+
herestring_redirect
|
|
46
|
+
],
|
|
47
|
+
punctuation: %i[],
|
|
48
|
+
builtin: %i[],
|
|
49
|
+
property: %i[]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
BASH = BASH_FEATURES.flat_map { |face, nodes|
|
|
53
|
+
nodes.map { |node| [node, face] }
|
|
54
|
+
}.to_h.freeze
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|