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,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
|