/*
Colorize test Go source by Tree-sitter
Not process wide character
Error on if letter larger than 1 byte
Support url at comment and interpreted_string_literal
2024-12-18
*/
package main
import (
"bytes"
"fmt"
"regexp"
"sort"
sitter "github.com/smacker/go-tree-sitter"
"github.com/smacker/go-tree-sitter/golang"
)
// ノードの種類に応じた色マッピング
var nodeColors = map[string]string{
"interpreted_string_literal": "\033[1;33m", // 黄色
"comment": "\033[1;34m", // 青
"url": "\033[1;36m", // シアン
"package": "\033[1;32m", // 緑
"identifier": "\033[1;31m", // 赤
"string": "\033[1;35m", // 紫
"number": "\033[1;37m", // 白
"int_literal": "\033[0;33m", // 茶色
"slice_type": "\033[0;34m", // 濃い青
"default": "\033[0m", // リセット
}
// URL 検出用の正規表現
var urlRegex = regexp.MustCompile(`https?://[^\s]+`)
// イベント構造体
type Event struct {
Row uint32 // 行番号 (0-based)
Column uint32 // 列番号 (0-based) byte column
EventType string // イベントの種類 ("start" or "end")
Color string // 対応する色
NodeType string // ノードの種類
}
func main() {
sourceCode := []byte(`
package main
/*
コメント例
https://example.com
*/
func main() {
message := "Hello, Tree-sitter!" // コメント
fmt.Println(message)
}
`)
// Tree-sitter パーサーのセットアップ
parser := sitter.NewParser()
parser.SetLanguage(golang.GetLanguage()) // Go 言語用パーサー
// 構文解析
tree := parser.Parse(nil, sourceCode) // 非推奨コード
rootNode := tree.RootNode()
// fmt.Printf("rootNode: %v\n--------\n", rootNode)
// パース結果を位置データに変換
events := parseToPositionData(sourceCode, rootNode)
// fmt.Printf("events: %v\n--------\n", events)
// 行、列でソート
// 行、列で比較し、"end" イベントを優先する
sort.Slice(events, func(i, j int) bool {
if events[i].Row == events[j].Row {
if events[i].Column == events[j].Column {
return events[i].EventType == "end" // "end" を優先
}
return events[i].Column < events[j].Column
}
return events[i].Row < events[j].Row
})
fmt.Printf("sorted events: %v\n--------\n", events)
// スイープラインでソースコードに色付け
colorizeSource(sourceCode, events)
}
func colorizeSource(source []byte, events []Event) {
currentColor := "\033[0m" // 初期はリセット色
eventIndex := 0 // 現在処理中のイベントインデックス
for row, line := range splitLines(source) {
coloredLine := ""
for col := 0; col < len(line); col++ {
// 現在の行と列に該当するイベントを処理
for eventIndex < len(events) && events[eventIndex].Row == uint32(row) && events[eventIndex].Column == uint32(col) {
if events[eventIndex].EventType == "start" {
pushColor(currentColor)
currentColor = events[eventIndex].Color
} else if events[eventIndex].EventType == "end" {
// currentColor = "\033[0m" // リセット色
currentColor = popColor()
}
eventIndex++
}
// 現在の文字に色を付ける
coloredLine += currentColor + string(line[col])
}
// 行の終了後にリセット
fmt.Printf("%s\033[0m", coloredLine)
// fmt.Printf("%s", coloredLine)
}
}
// ソースコードを行ごとに分割
// セパレーターは "\n" は残す
func splitLines(source []byte) [][]byte {
return bytes.SplitAfter(source, []byte("\n"))
}
// 構文木を走査してコールバックを実行
func walk(node *sitter.Node, source []byte, callback func(node *sitter.Node)) {
if node == nil {
return
}
// 現在のノードでコールバックを実行
callback(node)
// 子ノードを再帰的に処理
for i := 0; i < int(node.ChildCount()); i++ {
walk(node.Child(i), source, callback)
}
}
// パース結果を位置データに変換
func parseToPositionData(source []byte, node *sitter.Node) []Event {
var events []Event
walk(node, source, func(currentNode *sitter.Node) {
nodeType := currentNode.Type()
color, err := getColor(nodeType)
if err != nil {
return
}
startPoint := currentNode.StartPoint()
endPoint := currentNode.EndPoint()
// コメントや文字列リテラル内にURLがある場合
// Tree-sitterの構文木をさらに詳細に解析する方法もあるけれどやめ
if nodeType == "comment" || nodeType == "interpreted_string_literal" {
text := currentNode.Content(source)
matches := urlRegex.FindAllStringIndex(text, -1)
for _, match := range matches {
urlStart := match[0]
urlEnd := match[1]
startRow, startCol := calculatePosition(text, currentNode, urlStart)
endRow, endCol := calculatePosition(text, currentNode, urlEnd)
events = append(events, Event{
Row: startRow,
Column: startCol,
EventType: "start",
Color: nodeColors["url"],
NodeType: "url",
}, Event{
Row: endRow,
Column: endCol,
EventType: "end",
Color: nodeColors["url"],
NodeType: "url",
})
}
}
events = append(events, Event{
Row: startPoint.Row,
Column: startPoint.Column,
EventType: "start",
Color: color,
NodeType: nodeType,
})
events = append(events, Event{
Row: endPoint.Row,
Column: endPoint.Column,
EventType: "end",
Color: color,
NodeType: nodeType,
})
})
return events
}
// ノード内の相対位置を計算して絶対位置に変換
func calculatePosition(text string, node *sitter.Node, byteIndex int) (uint32, uint32) {
lines := bytes.Split([]byte(text[:byteIndex]), []byte("\n"))
row := node.StartPoint().Row + uint32(len(lines)-1)
col := uint32(len(lines[len(lines)-1]))
if len(lines) == 1 {
col += node.StartPoint().Column
}
return row, col
}
// ノードの種類に応じた色を取得
func getColor(nodeType string) (string, error) {
if color, ok := nodeColors[nodeType]; ok {
return color, nil
}
return nodeColors["default"], fmt.Errorf("color not found for node type: %s", nodeType)
}
// スイープラインの処理でネストした色付けを処理
var colorStack = []string{}
func pushColor(color string) {
colorStack = append(colorStack, color)
}
func popColor() string {
if len(colorStack) == 0 {
return "\033[0m" // リセット色
}
color := colorStack[len(colorStack)-1] // スタックの末尾を取得
colorStack = colorStack[:len(colorStack)-1] // スタックから末尾を削除
return color
}