1package main
2
3// Syntax highlighting using tree-sitter. It parses the buffer content, executes
4// queries to find semantic tokens, and maps those tokens to theme colors.
5
6import (
7 "context"
8 "fmt"
9
10 sitter "github.com/mitjafelicijan/go-tree-sitter"
11 "github.com/mitjafelicijan/go-tree-sitter/bash"
12 "github.com/mitjafelicijan/go-tree-sitter/c"
13 "github.com/mitjafelicijan/go-tree-sitter/cpp"
14 "github.com/mitjafelicijan/go-tree-sitter/css"
15 "github.com/mitjafelicijan/go-tree-sitter/dockerfile"
16 "github.com/mitjafelicijan/go-tree-sitter/golang"
17 "github.com/mitjafelicijan/go-tree-sitter/html"
18 "github.com/mitjafelicijan/go-tree-sitter/javascript"
19 "github.com/mitjafelicijan/go-tree-sitter/lua"
20 markdown "github.com/mitjafelicijan/go-tree-sitter/markdown/tree-sitter-markdown"
21 "github.com/mitjafelicijan/go-tree-sitter/php"
22 "github.com/mitjafelicijan/go-tree-sitter/python"
23 "github.com/mitjafelicijan/go-tree-sitter/sql"
24 "github.com/mitjafelicijan/go-tree-sitter/typescript/tsx"
25 "github.com/mitjafelicijan/go-tree-sitter/typescript/typescript"
26 "github.com/nsf/termbox-go"
27)
28
29// SyntaxHighlighter manages the tree-sitter parser, tree, and calculated highlights for a buffer.
30type SyntaxHighlighter struct {
31 Parser *sitter.Parser
32 Tree *sitter.Tree
33 Lang *sitter.Language
34 Query *sitter.Query
35 Language string
36 Highlights map[int]map[int]termbox.Attribute // Cached colors: Line -> Col -> termbox.Attribute
37 Log func(string, string) // Debug logging function.
38}
39
40// NewSyntaxHighlighter initializes a parser for the given file type.
41func NewSyntaxHighlighter(fileType string, log func(string, string)) *SyntaxHighlighter {
42 parser := sitter.NewParser()
43 var lang *sitter.Language
44 var langName string
45
46 // Map internal FileType names to tree-sitter languages.
47 switch fileType {
48 case "C":
49 lang = c.GetLanguage()
50 langName = "c"
51 case "C++":
52 lang = cpp.GetLanguage()
53 langName = "cpp"
54 case "Go":
55 lang = golang.GetLanguage()
56 langName = "go"
57 case "JavaScript":
58 lang = javascript.GetLanguage()
59 langName = "javascript"
60 case "TypeScript":
61 lang = typescript.GetLanguage()
62 langName = "typescript"
63 case "TSX":
64 lang = tsx.GetLanguage()
65 langName = "tsx"
66 case "Python":
67 lang = python.GetLanguage()
68 langName = "python"
69 case "Bash":
70 lang = bash.GetLanguage()
71 langName = "bash"
72 case "CSS":
73 lang = css.GetLanguage()
74 langName = "css"
75 case "Dockerfile":
76 lang = dockerfile.GetLanguage()
77 langName = "dockerfile"
78 case "HTML":
79 lang = html.GetLanguage()
80 langName = "html"
81 case "Lua":
82 lang = lua.GetLanguage()
83 langName = "lua"
84 case "Markdown":
85 lang = markdown.GetLanguage()
86 langName = "markdown"
87 case "PHP":
88 lang = php.GetLanguage()
89 langName = "php"
90 case "SQL":
91 lang = sql.GetLanguage()
92 langName = "sql"
93 default:
94 return nil
95 }
96
97 parser.SetLanguage(lang)
98 s := &SyntaxHighlighter{
99 Parser: parser,
100 Lang: lang,
101 Language: langName,
102 Highlights: make(map[int]map[int]termbox.Attribute),
103 Log: log,
104 }
105
106 // Load the tree-sitter query file (.scm) for this language.
107 queryPath := fmt.Sprintf("queries/%s.scm", langName)
108 s.LoadQuery(queryPath)
109
110 return s
111}
112
113// LoadQuery reads and compiles a tree-sitter query from the embedded filesystem.
114func (s *SyntaxHighlighter) LoadQuery(path string) {
115 if s.Log != nil {
116 s.Log("TS", fmt.Sprintf("Loading query for %s", path))
117 }
118
119 content, err := QueriesFS.ReadFile(path)
120 if err != nil {
121 if s.Log != nil {
122 s.Log("TS", fmt.Sprintf("LoadQuery failed to read %s: %v", path, err))
123 }
124 return
125 }
126
127 q, err := sitter.NewQuery(content, s.Lang)
128 if err == nil {
129 s.Query = q
130 } else if s.Log != nil {
131 s.Log("TS", fmt.Sprintf("LoadQuery failed to compile query for %s: %v", path, err))
132 }
133}
134
135// Parse runs a full parse of the content and updates the highlight cache.
136func (s *SyntaxHighlighter) Parse(content []byte) {
137 if s.Parser == nil {
138 return
139 }
140 tree, _ := s.Parser.ParseCtx(context.Background(), nil, content)
141 s.Tree = tree
142 s.updateHighlights(content)
143}
144
145// Reparse is a wrapper around Parse (used for batch updates).
146func (s *SyntaxHighlighter) Reparse(content []byte) {
147 s.Parse(content)
148}
149
150// Edit is a placeholder for incremental parsing (currently does a full reparse).
151func (s *SyntaxHighlighter) Edit(edit sitter.EditInput, newContent []byte) {
152 s.Reparse(newContent)
153}
154
155// updateHighlights executes the tree-sitter query on the syntax tree and populates the highlight cache.
156func (s *SyntaxHighlighter) updateHighlights(source []byte) {
157 // Always clear previous highlights to prevent ghosting.
158 s.Highlights = make(map[int]map[int]termbox.Attribute)
159
160 if s.Tree == nil || s.Query == nil {
161 return
162 }
163
164 qc := sitter.NewQueryCursor()
165 qc.Exec(s.Query, s.Tree.RootNode())
166
167 for {
168 m, ok := qc.NextMatch()
169 if !ok {
170 break
171 }
172
173 for _, c := range m.Captures {
174 // Find the theme attribute for the capture name (e.g., "function", "keyword").
175 captureName := s.Query.CaptureNameForId(c.Index)
176 attr := getTermboxAttr(captureName)
177
178 startRow := int(c.Node.StartPoint().Row)
179 startCol := int(c.Node.StartPoint().Column)
180 endRow := int(c.Node.EndPoint().Row)
181 endCol := int(c.Node.EndPoint().Column)
182
183 // Map the capture span to line/column color attributes.
184 for r := startRow; r <= endRow; r++ {
185 if _, ok := s.Highlights[r]; !ok {
186 s.Highlights[r] = make(map[int]termbox.Attribute)
187 }
188
189 cStart := 0
190 if r == startRow {
191 cStart = startCol
192 }
193
194 cEnd := -1
195 if r == endRow {
196 cEnd = endCol
197 }
198
199 limit := cEnd
200 if limit == -1 {
201 limit = 1000 // A reasonable overflow limit for whole lines.
202 }
203
204 for col := cStart; col < limit; col++ {
205 s.Highlights[r][col] = attr
206 }
207 }
208 }
209 }
210}
211
212// getTermboxAttr maps a tree-sitter capture name to a color name from our theme.
213func getTermboxAttr(captureName string) termbox.Attribute {
214 var cn ColorName
215 switch captureName {
216 case "function":
217 cn = ColorTSFunction
218 case "tag":
219 cn = ColorTSTag
220 case "attribute":
221 cn = ColorTSAttribute
222 case "constant":
223 cn = ColorTSConstant
224 case "variable":
225 cn = ColorTSVariable
226 case "type":
227 cn = ColorTSType
228 case "string":
229 cn = ColorTSString
230 case "keyword":
231 cn = ColorTSKeyword
232 case "comment":
233 cn = ColorTSComment
234 case "number":
235 cn = ColorTSNumber
236 case "boolean":
237 cn = ColorTSBoolean
238 case "null":
239 cn = ColorTSNull
240 case "property":
241 cn = ColorTSProperty
242 default:
243 return termbox.ColorDefault
244 }
245
246 fg, _ := GetThemeColor(cn)
247 return fg
248}
249
250// Highlight returns a slice of attributes for each character in a line.
251func (s *SyntaxHighlighter) Highlight(lineIdx int, lineContent []rune) []termbox.Attribute {
252 attrs := make([]termbox.Attribute, len(lineContent))
253 // Fill with default foreground color first.
254 defaultFg, _ := GetThemeColor(ColorDefault)
255 for i := range attrs {
256 attrs[i] = defaultFg
257 }
258
259 // Apply cached highlights if they exist for this line.
260 if lineHighlights, ok := s.Highlights[lineIdx]; ok {
261 for col, color := range lineHighlights {
262 if col < len(attrs) {
263 attrs[col] = color
264 }
265 }
266 }
267
268 return attrs
269}