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,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree_sitter_config"
4
+ require_relative "tree_sitter/node_maps"
5
+
6
+ module Textbringer
7
+ module TreeSitterAdapter
8
+ # Emacs 風の 4 段階レベル
9
+ HIGHLIGHT_LEVELS = [
10
+ %i[comment string], # Level 1: 最小限
11
+ %i[keyword type constant], # Level 2: 基本
12
+ %i[function_name variable number], # Level 3: 標準(デフォルト)
13
+ %i[operator punctuation builtin] # Level 4: 全部
14
+ ].freeze
15
+
16
+ module ClassMethods
17
+ def use_tree_sitter(language)
18
+ @tree_sitter_language = language
19
+
20
+ # prepend を使って既存の custom_highlight より優先させる
21
+ prepend InstanceMethods
22
+
23
+ define_method(:tree_sitter_language) do
24
+ language
25
+ end
26
+ end
27
+
28
+ attr_reader :tree_sitter_language
29
+ end
30
+
31
+ def self.debug?
32
+ ENV["TEXTBRINGER_TREE_SITTER_DEBUG"] == "1"
33
+ end
34
+
35
+ module InstanceMethods
36
+ def custom_highlight(window)
37
+ window.instance_variable_set(:@highlight_on, {})
38
+ window.instance_variable_set(:@highlight_off, {})
39
+
40
+ return unless can_highlight?
41
+
42
+ parser = get_parser
43
+ return unless parser
44
+
45
+ buffer = window.buffer
46
+ # textbringer 本体と同じロジック: base_pos を基準にする
47
+ base_pos = buffer.point_min
48
+ buffer_text = buffer.to_s
49
+ tree = parser.parse_string(nil, buffer_text)
50
+ return unless tree
51
+
52
+ if TreeSitterAdapter.debug?
53
+ File.open("/tmp/tree_sitter_debug.log", "a") do |f|
54
+ f.puts "[#{Time.now}] custom_highlight"
55
+ f.puts " base_pos=#{base_pos} buffer.bytesize=#{buffer_text.bytesize}"
56
+ end
57
+ end
58
+
59
+ highlight_on = {}
60
+ highlight_off = {}
61
+
62
+ visit_node(tree.root_node) do |node, start_byte, end_byte|
63
+ face = node_type_to_face(node.type.to_sym)
64
+ next unless face
65
+
66
+ attrs = Face[face]&.attributes
67
+ if attrs
68
+ # base_pos + offset でバッファ内の絶対位置を計算
69
+ highlight_on[base_pos + start_byte] = attrs
70
+ highlight_off[base_pos + end_byte] = attrs
71
+
72
+ if TreeSitterAdapter.debug? && highlight_on.size <= 5
73
+ File.open("/tmp/tree_sitter_debug.log", "a") do |f|
74
+ f.puts " #{node.type} pos=#{base_pos + start_byte}-#{base_pos + end_byte} face=#{face}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ if TreeSitterAdapter.debug?
81
+ File.open("/tmp/tree_sitter_debug.log", "a") do |f|
82
+ f.puts " total_highlights=#{highlight_on.size}"
83
+ end
84
+ end
85
+
86
+ window.instance_variable_set(:@highlight_on, highlight_on)
87
+ window.instance_variable_set(:@highlight_off, highlight_off)
88
+ end
89
+
90
+ private
91
+
92
+ def can_highlight?
93
+ # textbringer 本体と同じチェック: @@has_colors を使う
94
+ return false unless Window.class_variable_get(:@@has_colors)
95
+ return false if CONFIG[:syntax_highlight] == false
96
+
97
+ true
98
+ end
99
+
100
+ def get_parser
101
+ @parser ||= begin
102
+ return nil unless TreeSitterConfig.parser_available?(tree_sitter_language)
103
+ return nil unless defined?(::TreeSitter)
104
+
105
+ parser_path = TreeSitterConfig.parser_path(tree_sitter_language)
106
+ language = ::TreeSitter::Language.load(
107
+ tree_sitter_language.to_s,
108
+ parser_path
109
+ )
110
+
111
+ parser = ::TreeSitter::Parser.new
112
+ parser.language = language
113
+ parser
114
+ rescue LoadError, ::TreeSitter::TreeSitterError, ::TreeSitter::LanguageLoadError
115
+ nil
116
+ end
117
+ end
118
+
119
+ def visit_node(node, &block)
120
+ block.call(node, node.start_byte, node.end_byte)
121
+
122
+ node.child_count.times do |i|
123
+ child = node.child(i)
124
+ visit_node(child, &block) if child
125
+ end
126
+ end
127
+
128
+ def node_type_to_face(node_type)
129
+ node_map = TreeSitter::NodeMaps.for(tree_sitter_language)
130
+ return nil unless node_map
131
+
132
+ face = node_map[node_type]
133
+ return nil unless face
134
+ return nil unless enabled_faces.include?(face)
135
+
136
+ face
137
+ end
138
+
139
+ def enabled_faces
140
+ # カスタム feature 設定が優先
141
+ if CONFIG[:tree_sitter_enabled_features]
142
+ return CONFIG[:tree_sitter_enabled_features]
143
+ end
144
+
145
+ # レベルベースの制御
146
+ level = CONFIG[:tree_sitter_highlight_level] || 3
147
+ HIGHLIGHT_LEVELS.take(level).flatten
148
+ end
149
+ end
150
+ end
151
+
152
+ # Window モンキーパッチ
153
+ class Window
154
+ unless method_defined?(:original_highlight)
155
+ alias_method :original_highlight, :highlight
156
+
157
+ def highlight
158
+ if @buffer&.mode.respond_to?(:custom_highlight)
159
+ @buffer.mode.custom_highlight(self)
160
+ else
161
+ original_highlight
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Textbringer
6
+ module TreeSitterConfig
7
+ class << self
8
+ def platform
9
+ os = case RbConfig::CONFIG["host_os"]
10
+ when /darwin/i then "darwin"
11
+ when /linux/i then "linux"
12
+ else "unknown"
13
+ end
14
+
15
+ arch = case RbConfig::CONFIG["host_cpu"]
16
+ when /arm64|aarch64/i then "arm64"
17
+ when /x86_64|amd64/i then "x64"
18
+ else "unknown"
19
+ end
20
+
21
+ "#{os}-#{arch}"
22
+ end
23
+
24
+ def dylib_ext
25
+ case RbConfig::CONFIG["host_os"]
26
+ when /darwin/i then ".dylib"
27
+ else ".so"
28
+ end
29
+ end
30
+
31
+ # Parser を探索するディレクトリのリスト(優先順位順)
32
+ def parser_search_paths
33
+ paths = []
34
+
35
+ # 1. CONFIG で指定されたカスタムパス(最優先)
36
+ if defined?(CONFIG) && CONFIG[:tree_sitter_parser_dir]
37
+ paths << CONFIG[:tree_sitter_parser_dir]
38
+ end
39
+
40
+ # 2. ~/.textbringer/parsers/{platform}(ユーザー共通)
41
+ paths << File.expand_path("~/.textbringer/parsers/#{platform}")
42
+
43
+ # 3. gem 内の parsers/{platform}(デフォルト)
44
+ paths << File.expand_path("../../../parsers/#{platform}", __FILE__)
45
+
46
+ paths
47
+ end
48
+
49
+ # 後方互換性のため、最初に見つかったパスを返す
50
+ def parser_dir
51
+ parser_search_paths.find { |path| Dir.exist?(path) } || parser_search_paths.last
52
+ end
53
+
54
+ def parser_path(language)
55
+ filename = "libtree-sitter-#{language}#{dylib_ext}"
56
+
57
+ # 検索パスから parser を探す
58
+ parser_search_paths.each do |dir|
59
+ path = File.join(dir, filename)
60
+ return path if File.exist?(path)
61
+ end
62
+
63
+ # 見つからない場合はデフォルトパスを返す
64
+ File.join(parser_search_paths.last, filename)
65
+ end
66
+
67
+ def parser_available?(language)
68
+ filename = "libtree-sitter-#{language}#{dylib_ext}"
69
+
70
+ parser_search_paths.any? do |dir|
71
+ File.exist?(File.join(dir, filename))
72
+ end
73
+ end
74
+
75
+ def define_default_faces
76
+ Face.define(:comment, foreground: "green")
77
+ Face.define(:string, foreground: "cyan")
78
+ Face.define(:keyword, foreground: "yellow")
79
+ Face.define(:number, foreground: "magenta")
80
+ Face.define(:constant, foreground: "magenta")
81
+ Face.define(:function_name, foreground: "blue")
82
+ Face.define(:type, foreground: "blue")
83
+ Face.define(:variable, foreground: "white")
84
+ Face.define(:operator, foreground: "white")
85
+ Face.define(:punctuation, foreground: "white")
86
+ Face.define(:builtin, foreground: "cyan")
87
+ Face.define(:property, foreground: "cyan")
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Textbringer plugin エントリポイント
4
+ # Textbringer がプラグインを自動ロードする際に読み込まれる
5
+
6
+ # tree_sitter gem を先に require して namespace 衝突を防ぐ
7
+ # (Textbringer::TreeSitter より先に ::TreeSitter を定義)
8
+ begin
9
+ require "tree_sitter"
10
+ rescue LoadError
11
+ # tree_sitter gem がない場合は無視(parser なしで動作)
12
+ end
13
+
14
+ require "textbringer/tree_sitter/version"
15
+ require "textbringer/tree_sitter_config"
16
+ require "textbringer/tree_sitter/node_maps"
17
+ require "textbringer/tree_sitter_adapter"
18
+
19
+ # デフォルトの Face を定義
20
+ Textbringer::TreeSitterConfig.define_default_faces
21
+
22
+ # ユーザー定義の node_maps を読み込む
23
+ # ~/.textbringer/tree_sitter/node_maps/*.rb
24
+ user_node_maps_dir = File.expand_path("~/.textbringer/tree_sitter/node_maps")
25
+ if Dir.exist?(user_node_maps_dir)
26
+ Dir.glob(File.join(user_node_maps_dir, "*.rb")).sort.each do |file|
27
+ begin
28
+ require file
29
+ rescue => e
30
+ warn "textbringer-tree-sitter: Failed to load #{file}: #{e.message}"
31
+ end
32
+ end
33
+ end
34
+
35
+ # 既存 Mode → 言語 のマッピング
36
+ MODE_LANGUAGE_MAP = {
37
+ "RubyMode" => :ruby,
38
+ "CMode" => :c,
39
+ "JavaScriptMode" => :javascript,
40
+ "PythonMode" => :python,
41
+ "RustMode" => :rust,
42
+ "BashMode" => :bash,
43
+ "ShMode" => :bash,
44
+ "HCLMode" => :hcl,
45
+ "TerraformMode" => :hcl,
46
+ "JSONMode" => :json,
47
+ "YAMLMode" => :yaml,
48
+ "HTMLMode" => :html,
49
+ "JavaMode" => :java,
50
+ "PHPMode" => :php,
51
+ "MarkdownMode" => :markdown,
52
+ "GoMode" => :go,
53
+ "TypeScriptMode" => :typescript,
54
+ "TSXMode" => :tsx,
55
+ "SQLMode" => :sql,
56
+ }.freeze
57
+
58
+ # 言語 → ファイルパターン(自動 Mode 生成用)
59
+ LANGUAGE_FILE_PATTERNS = {
60
+ markdown: /\.(md|markdown|mkd|mkdn)$/i,
61
+ hcl: /\.(tf|tfvars|hcl)$/i,
62
+ go: /\.go$/i,
63
+ typescript: /\.ts$/i,
64
+ tsx: /\.tsx$/i,
65
+ sql: /\.sql$/i,
66
+ yaml: /\.(ya?ml)$/i,
67
+ json: /\.json$/i,
68
+ python: /\.py$/i,
69
+ rust: /\.rs$/i,
70
+ java: /\.java$/i,
71
+ c: /\.[ch]$/i,
72
+ javascript: /\.m?js$/i,
73
+ bash: /\.(sh|bash)$/i,
74
+ php: /\.php$/i,
75
+ html: /\.html?$/i,
76
+ }.freeze
77
+
78
+ # parser + node_map がある言語で、Mode がなければ自動生成
79
+ Textbringer::TreeSitter::NodeMaps.available_languages.each do |language|
80
+ next unless Textbringer::TreeSitterConfig.parser_available?(language)
81
+
82
+ # 既存の Mode を探す
83
+ mode_name = MODE_LANGUAGE_MAP.key(language)&.to_s ||
84
+ "#{language.to_s.split('_').map(&:capitalize).join}Mode"
85
+
86
+ unless Textbringer.const_defined?(mode_name)
87
+ # Mode が存在しない場合は自動生成
88
+ pattern = LANGUAGE_FILE_PATTERNS[language]
89
+ if pattern
90
+ # class_eval で名前付きクラスを定義(inherited フック対策)
91
+ Textbringer.class_eval <<~RUBY, __FILE__, __LINE__ + 1
92
+ class #{mode_name} < ProgrammingMode
93
+ self.file_name_pattern = #{pattern.inspect}
94
+ end
95
+ RUBY
96
+ end
97
+ end
98
+ end
99
+
100
+ # デバッグログ用
101
+ def tree_sitter_debug(msg)
102
+ return unless ENV["TEXTBRINGER_TREE_SITTER_DEBUG"] == "1"
103
+ File.open("/tmp/tree_sitter_plugin.log", "a") { |f| f.puts "[#{Time.now}] #{msg}" }
104
+ end
105
+
106
+ # 利用可能な parser と node_map がある Mode に tree-sitter を有効化
107
+ tree_sitter_debug "=== Enabling tree-sitter on modes ==="
108
+ tree_sitter_debug "available_languages: #{Textbringer::TreeSitter::NodeMaps.available_languages.inspect}"
109
+
110
+ MODE_LANGUAGE_MAP.each do |mode_name, language|
111
+ next unless language
112
+
113
+ parser_available = Textbringer::TreeSitterConfig.parser_available?(language)
114
+ node_map = Textbringer::TreeSitter::NodeMaps.for(language)
115
+
116
+ tree_sitter_debug "#{mode_name} (#{language}): parser=#{parser_available}, node_map=#{!!node_map}"
117
+
118
+ next unless parser_available
119
+ next unless node_map
120
+
121
+ begin
122
+ mode_class = Textbringer.const_get(mode_name)
123
+ tree_sitter_debug " found mode_class: #{mode_class}"
124
+
125
+ # 既に tree-sitter が設定されていればスキップ
126
+ if mode_class.respond_to?(:tree_sitter_language) && mode_class.tree_sitter_language
127
+ tree_sitter_debug " already has tree_sitter_language: #{mode_class.tree_sitter_language}"
128
+ next
129
+ end
130
+
131
+ # TreeSitterAdapter を extend して use_tree_sitter を呼ぶ
132
+ mode_class.extend(Textbringer::TreeSitterAdapter::ClassMethods)
133
+ mode_class.use_tree_sitter(language)
134
+ tree_sitter_debug " enabled tree-sitter for #{mode_name}"
135
+ rescue NameError => e
136
+ tree_sitter_debug " NameError: #{e.message}"
137
+ # Mode が存在しない場合は無視
138
+ end
139
+ end
140
+
141
+ tree_sitter_debug "=== Done ==="
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Parser ビルドスクリプト
5
+ # Usage: ./scripts/build_parsers.sh
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
9
+
10
+ # プラットフォーム判定
11
+ if [[ "$(uname)" == "Darwin" ]]; then
12
+ if [[ "$(uname -m)" == "arm64" ]]; then
13
+ PLATFORM="darwin-arm64"
14
+ EXT=".dylib"
15
+ else
16
+ PLATFORM="darwin-x64"
17
+ EXT=".dylib"
18
+ fi
19
+ else
20
+ if [[ "$(uname -m)" == "aarch64" ]]; then
21
+ PLATFORM="linux-arm64"
22
+ EXT=".so"
23
+ else
24
+ PLATFORM="linux-x64"
25
+ EXT=".so"
26
+ fi
27
+ fi
28
+
29
+ PARSER_DIR="$PROJECT_DIR/parsers/$PLATFORM"
30
+ mkdir -p "$PARSER_DIR"
31
+
32
+ echo "Building parsers for $PLATFORM..."
33
+
34
+ # 一時ディレクトリ
35
+ TMP_DIR=$(mktemp -d)
36
+ trap "rm -rf $TMP_DIR" EXIT
37
+
38
+ cd "$TMP_DIR"
39
+
40
+ # HCL parser をビルド
41
+ echo "Building HCL parser..."
42
+ git clone --depth 1 https://github.com/mitchellh/tree-sitter-hcl.git
43
+ cd tree-sitter-hcl
44
+
45
+ if [[ "$EXT" == ".dylib" ]]; then
46
+ c++ -shared -fPIC -O2 -std=c++14 -Isrc src/parser.c src/scanner.cc -o "libtree-sitter-hcl$EXT"
47
+ else
48
+ c++ -shared -fPIC -O2 -std=c++14 -Isrc src/parser.c src/scanner.cc -o "libtree-sitter-hcl$EXT"
49
+ fi
50
+
51
+ cp "libtree-sitter-hcl$EXT" "$PARSER_DIR/"
52
+ echo "HCL parser installed to $PARSER_DIR/libtree-sitter-hcl$EXT"
53
+
54
+ # Ruby parser はプリビルド版をダウンロード
55
+ echo "Downloading Ruby parser..."
56
+ cd "$TMP_DIR"
57
+
58
+ RUBY_PARSER_URL="https://github.com/Faveod/tree-sitter-parsers/releases/download/v0.1.0/libtree-sitter-ruby-$PLATFORM$EXT"
59
+ if curl -fsSL -o "libtree-sitter-ruby$EXT" "$RUBY_PARSER_URL" 2>/dev/null; then
60
+ cp "libtree-sitter-ruby$EXT" "$PARSER_DIR/"
61
+ echo "Ruby parser installed to $PARSER_DIR/libtree-sitter-ruby$EXT"
62
+ else
63
+ echo "Warning: Could not download Ruby parser. Building from source..."
64
+ git clone --depth 1 https://github.com/tree-sitter/tree-sitter-ruby.git
65
+ cd tree-sitter-ruby
66
+ if [[ "$EXT" == ".dylib" ]]; then
67
+ cc -shared -fPIC -O2 -Isrc src/parser.c src/scanner.c -o "libtree-sitter-ruby$EXT"
68
+ else
69
+ cc -shared -fPIC -O2 -Isrc src/parser.c src/scanner.c -o "libtree-sitter-ruby$EXT"
70
+ fi
71
+ cp "libtree-sitter-ruby$EXT" "$PARSER_DIR/"
72
+ echo "Ruby parser installed to $PARSER_DIR/libtree-sitter-ruby$EXT"
73
+ fi
74
+
75
+ echo ""
76
+ echo "Done! Parsers installed to $PARSER_DIR"
77
+ ls -la "$PARSER_DIR"
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Parser 動作確認スクリプト
5
+ # Usage: bundle exec ruby scripts/test_parser.rb [language] [file]
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+
9
+ require "tree_sitter"
10
+ require "textbringer/tree_sitter_config"
11
+ require "textbringer/tree_sitter/node_maps"
12
+
13
+ # Textbringer モック
14
+ module Textbringer
15
+ CONFIG = { colors: true }
16
+
17
+ class Face
18
+ @faces = {}
19
+
20
+ class << self
21
+ def [](name)
22
+ @faces[name]
23
+ end
24
+
25
+ def define(name, **attrs)
26
+ @faces[name] = new(name, attrs)
27
+ end
28
+ end
29
+
30
+ attr_reader :name, :attributes
31
+
32
+ def initialize(name, attrs)
33
+ @name = name
34
+ @attributes = attrs
35
+ end
36
+ end
37
+ end
38
+
39
+ Textbringer::TreeSitterConfig.define_default_faces
40
+
41
+ def colorize(text, face)
42
+ colors = {
43
+ comment: "\e[32m", # green
44
+ string: "\e[36m", # cyan
45
+ keyword: "\e[33m", # yellow
46
+ number: "\e[35m", # magenta
47
+ constant: "\e[35m", # magenta
48
+ function_name: "\e[34m", # blue
49
+ type: "\e[34m", # blue
50
+ variable: "\e[37m", # white
51
+ builtin: "\e[36m", # cyan
52
+ property: "\e[36m" # cyan
53
+ }
54
+ reset = "\e[0m"
55
+
56
+ color = colors[face] || ""
57
+ "#{color}#{text}#{reset}"
58
+ end
59
+
60
+ def collect_highlights(node, node_map, code)
61
+ highlights = []
62
+ visit_node(node, node_map, code, highlights)
63
+ highlights.sort_by { |h| [h[:start], -h[:end]] }
64
+ end
65
+
66
+ def visit_node(node, node_map, code, highlights)
67
+ node_type = node.type.to_sym
68
+ face = node_map[node_type]
69
+
70
+ if face
71
+ highlights << {
72
+ type: node_type,
73
+ face: face,
74
+ start: node.start_byte,
75
+ end: node.end_byte,
76
+ text: code[node.start_byte...node.end_byte]
77
+ }
78
+ end
79
+
80
+ node.child_count.times do |i|
81
+ child = node.child(i)
82
+ visit_node(child, node_map, code, highlights) if child
83
+ end
84
+ end
85
+
86
+ def print_highlighted_code(code, highlights)
87
+ # 単純化のため、行単位でハイライト表示
88
+ puts "\n=== Highlighted Code ==="
89
+ highlights.each do |h|
90
+ next if h[:text].include?("\n") # 複数行はスキップ
91
+
92
+ puts "#{colorize(h[:text], h[:face])} (#{h[:face]})"
93
+ end
94
+ end
95
+
96
+ def main
97
+ language = (ARGV[0] || "ruby").to_sym
98
+ file_path = ARGV[1]
99
+
100
+ # Parser の存在確認
101
+ unless Textbringer::TreeSitterConfig.parser_available?(language)
102
+ puts "Parser not found for #{language}"
103
+ puts "Parser path: #{Textbringer::TreeSitterConfig.parser_path(language)}"
104
+ puts "\nAvailable languages with NodeMaps:"
105
+ Textbringer::TreeSitter::NodeMaps.available_languages.each do |lang|
106
+ available = Textbringer::TreeSitterConfig.parser_available?(lang)
107
+ status = available ? "✓" : "✗"
108
+ puts " #{status} #{lang}"
109
+ end
110
+ exit 1
111
+ end
112
+
113
+ # NodeMap の存在確認
114
+ node_map = Textbringer::TreeSitter::NodeMaps.for(language)
115
+ unless node_map
116
+ puts "NodeMap not found for #{language}"
117
+ exit 1
118
+ end
119
+
120
+ # コードを読み込み
121
+ code = if file_path
122
+ File.read(file_path)
123
+ else
124
+ # サンプルコード
125
+ case language
126
+ when :ruby
127
+ <<~RUBY
128
+ # Sample Ruby code
129
+ def hello(name)
130
+ puts "Hello, \#{name}!"
131
+ end
132
+
133
+ class Greeter
134
+ GREETING = "Hi"
135
+
136
+ def greet
137
+ hello("World")
138
+ end
139
+ end
140
+ RUBY
141
+ when :hcl
142
+ <<~HCL
143
+ # Sample HCL code
144
+ resource "aws_instance" "example" {
145
+ ami = "ami-12345678"
146
+ instance_type = "t2.micro"
147
+ count = 3
148
+
149
+ tags = {
150
+ Name = "HelloWorld"
151
+ }
152
+
153
+ accounts = [for account in local.accounts : replace(account, "x", "")]
154
+ }
155
+ HCL
156
+ when :python
157
+ <<~PYTHON
158
+ # Sample Python code
159
+ def hello(name):
160
+ print(f"Hello, {name}!")
161
+
162
+ class Greeter:
163
+ def __init__(self):
164
+ self.count = 0
165
+
166
+ def greet(self):
167
+ hello("World")
168
+ PYTHON
169
+ when :javascript
170
+ <<~JS
171
+ // Sample JavaScript code
172
+ function hello(name) {
173
+ console.log(`Hello, ${name}!`);
174
+ }
175
+
176
+ class Greeter {
177
+ constructor() {
178
+ this.count = 0;
179
+ }
180
+
181
+ greet() {
182
+ hello("World");
183
+ }
184
+ }
185
+ JS
186
+ else
187
+ puts "No sample code for #{language}. Please provide a file."
188
+ exit 1
189
+ end
190
+ end
191
+
192
+ # パース
193
+ parser_path = Textbringer::TreeSitterConfig.parser_path(language)
194
+ ts_language = TreeSitter::Language.load(language.to_s, parser_path)
195
+ parser = TreeSitter::Parser.new
196
+ parser.language = ts_language
197
+ tree = parser.parse_string(nil, code)
198
+
199
+ puts "=== Source Code ==="
200
+ puts code
201
+ puts
202
+
203
+ # ハイライト情報を収集
204
+ highlights = collect_highlights(tree.root_node, node_map, code)
205
+
206
+ puts "=== Highlight Info ==="
207
+ puts "Language: #{language}"
208
+ puts "Total highlights: #{highlights.size}"
209
+ puts
210
+
211
+ # ハイライト一覧
212
+ puts "=== Token Details ==="
213
+ highlights.each do |h|
214
+ text = h[:text].gsub("\n", "\\n")
215
+ text = text[0..40] + "..." if text.length > 40
216
+ puts "#{h[:face].to_s.ljust(15)} | #{h[:type].to_s.ljust(20)} | #{text}"
217
+ end
218
+
219
+ # カラー出力(単一行トークンのみ)
220
+ print_highlighted_code(code, highlights)
221
+ end
222
+
223
+ main