Tree-sitter による Go ソースのカラーリング試験
ワイド文字を処理していません
1文字が 1 バイトを超える文字は文字化けする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
/* 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 } |