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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +76 -0
  3. data/LICENSE.txt +13 -0
  4. data/README.md +125 -0
  5. data/Rakefile +12 -0
  6. data/exe/textbringer-tree-sitter +513 -0
  7. data/ext/textbringer_tree_sitter/extconf.rb +125 -0
  8. data/lib/textbringer/tree_sitter/node_maps/bash.rb +57 -0
  9. data/lib/textbringer/tree_sitter/node_maps/c.rb +64 -0
  10. data/lib/textbringer/tree_sitter/node_maps/cobol.rb +94 -0
  11. data/lib/textbringer/tree_sitter/node_maps/csharp.rb +107 -0
  12. data/lib/textbringer/tree_sitter/node_maps/groovy.rb +77 -0
  13. data/lib/textbringer/tree_sitter/node_maps/haml.rb +34 -0
  14. data/lib/textbringer/tree_sitter/node_maps/hcl.rb +53 -0
  15. data/lib/textbringer/tree_sitter/node_maps/html.rb +33 -0
  16. data/lib/textbringer/tree_sitter/node_maps/java.rb +98 -0
  17. data/lib/textbringer/tree_sitter/node_maps/javascript.rb +82 -0
  18. data/lib/textbringer/tree_sitter/node_maps/json.rb +31 -0
  19. data/lib/textbringer/tree_sitter/node_maps/pascal.rb +102 -0
  20. data/lib/textbringer/tree_sitter/node_maps/php.rb +100 -0
  21. data/lib/textbringer/tree_sitter/node_maps/python.rb +72 -0
  22. data/lib/textbringer/tree_sitter/node_maps/ruby.rb +82 -0
  23. data/lib/textbringer/tree_sitter/node_maps/rust.rb +81 -0
  24. data/lib/textbringer/tree_sitter/node_maps/yaml.rb +45 -0
  25. data/lib/textbringer/tree_sitter/node_maps.rb +78 -0
  26. data/lib/textbringer/tree_sitter/version.rb +7 -0
  27. data/lib/textbringer/tree_sitter_adapter.rb +166 -0
  28. data/lib/textbringer/tree_sitter_config.rb +91 -0
  29. data/lib/textbringer_plugin.rb +141 -0
  30. data/parsers/darwin-arm64/.gitkeep +0 -0
  31. data/parsers/darwin-x64/.gitkeep +0 -0
  32. data/parsers/linux-arm64/.gitkeep +0 -0
  33. data/parsers/linux-x64/.gitkeep +0 -0
  34. data/scripts/build_parsers.sh +77 -0
  35. data/scripts/test_parser.rb +223 -0
  36. 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