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}