diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 20:22:09 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 20:22:09 +0100 |
| commit | 5a8dbc6347b3541e84fe669b22c17ad3b715e258 (patch) | |
| tree | b148c450939688caaaeb4adac6f2faa1eaffe649 /syntax.go | |
| download | qwe-editor-5a8dbc6347b3541e84fe669b22c17ad3b715e258.tar.gz | |
Engage!
Diffstat (limited to 'syntax.go')
| -rw-r--r-- | syntax.go | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/syntax.go b/syntax.go new file mode 100644 index 0000000..7a68277 --- /dev/null +++ b/syntax.go @@ -0,0 +1,269 @@ +package main + +// Syntax highlighting using tree-sitter. It parses the buffer content, executes +// queries to find semantic tokens, and maps those tokens to theme colors. + +import ( + "context" + "fmt" + + sitter "github.com/mitjafelicijan/go-tree-sitter" + "github.com/mitjafelicijan/go-tree-sitter/bash" + "github.com/mitjafelicijan/go-tree-sitter/c" + "github.com/mitjafelicijan/go-tree-sitter/cpp" + "github.com/mitjafelicijan/go-tree-sitter/css" + "github.com/mitjafelicijan/go-tree-sitter/dockerfile" + "github.com/mitjafelicijan/go-tree-sitter/golang" + "github.com/mitjafelicijan/go-tree-sitter/html" + "github.com/mitjafelicijan/go-tree-sitter/javascript" + "github.com/mitjafelicijan/go-tree-sitter/lua" + markdown "github.com/mitjafelicijan/go-tree-sitter/markdown/tree-sitter-markdown" + "github.com/mitjafelicijan/go-tree-sitter/php" + "github.com/mitjafelicijan/go-tree-sitter/python" + "github.com/mitjafelicijan/go-tree-sitter/sql" + "github.com/mitjafelicijan/go-tree-sitter/typescript/tsx" + "github.com/mitjafelicijan/go-tree-sitter/typescript/typescript" + "github.com/nsf/termbox-go" +) + +// SyntaxHighlighter manages the tree-sitter parser, tree, and calculated highlights for a buffer. +type SyntaxHighlighter struct { + Parser *sitter.Parser + Tree *sitter.Tree + Lang *sitter.Language + Query *sitter.Query + Language string + Highlights map[int]map[int]termbox.Attribute // Cached colors: Line -> Col -> termbox.Attribute + Log func(string, string) // Debug logging function. +} + +// NewSyntaxHighlighter initializes a parser for the given file type. +func NewSyntaxHighlighter(fileType string, log func(string, string)) *SyntaxHighlighter { + parser := sitter.NewParser() + var lang *sitter.Language + var langName string + + // Map internal FileType names to tree-sitter languages. + switch fileType { + case "C": + lang = c.GetLanguage() + langName = "c" + case "C++": + lang = cpp.GetLanguage() + langName = "cpp" + case "Go": + lang = golang.GetLanguage() + langName = "go" + case "JavaScript": + lang = javascript.GetLanguage() + langName = "javascript" + case "TypeScript": + lang = typescript.GetLanguage() + langName = "typescript" + case "TSX": + lang = tsx.GetLanguage() + langName = "tsx" + case "Python": + lang = python.GetLanguage() + langName = "python" + case "Bash": + lang = bash.GetLanguage() + langName = "bash" + case "CSS": + lang = css.GetLanguage() + langName = "css" + case "Dockerfile": + lang = dockerfile.GetLanguage() + langName = "dockerfile" + case "HTML": + lang = html.GetLanguage() + langName = "html" + case "Lua": + lang = lua.GetLanguage() + langName = "lua" + case "Markdown": + lang = markdown.GetLanguage() + langName = "markdown" + case "PHP": + lang = php.GetLanguage() + langName = "php" + case "SQL": + lang = sql.GetLanguage() + langName = "sql" + default: + return nil + } + + parser.SetLanguage(lang) + s := &SyntaxHighlighter{ + Parser: parser, + Lang: lang, + Language: langName, + Highlights: make(map[int]map[int]termbox.Attribute), + Log: log, + } + + // Load the tree-sitter query file (.scm) for this language. + queryPath := fmt.Sprintf("queries/%s.scm", langName) + s.LoadQuery(queryPath) + + return s +} + +// LoadQuery reads and compiles a tree-sitter query from the embedded filesystem. +func (s *SyntaxHighlighter) LoadQuery(path string) { + if s.Log != nil { + s.Log("TS", fmt.Sprintf("Loading query for %s", path)) + } + + content, err := QueriesFS.ReadFile(path) + if err != nil { + if s.Log != nil { + s.Log("TS", fmt.Sprintf("LoadQuery failed to read %s: %v", path, err)) + } + return + } + + q, err := sitter.NewQuery(content, s.Lang) + if err == nil { + s.Query = q + } else if s.Log != nil { + s.Log("TS", fmt.Sprintf("LoadQuery failed to compile query for %s: %v", path, err)) + } +} + +// Parse runs a full parse of the content and updates the highlight cache. +func (s *SyntaxHighlighter) Parse(content []byte) { + if s.Parser == nil { + return + } + tree, _ := s.Parser.ParseCtx(context.Background(), nil, content) + s.Tree = tree + s.updateHighlights(content) +} + +// Reparse is a wrapper around Parse (used for batch updates). +func (s *SyntaxHighlighter) Reparse(content []byte) { + s.Parse(content) +} + +// Edit is a placeholder for incremental parsing (currently does a full reparse). +func (s *SyntaxHighlighter) Edit(edit sitter.EditInput, newContent []byte) { + s.Reparse(newContent) +} + +// updateHighlights executes the tree-sitter query on the syntax tree and populates the highlight cache. +func (s *SyntaxHighlighter) updateHighlights(source []byte) { + // Always clear previous highlights to prevent ghosting. + s.Highlights = make(map[int]map[int]termbox.Attribute) + + if s.Tree == nil || s.Query == nil { + return + } + + qc := sitter.NewQueryCursor() + qc.Exec(s.Query, s.Tree.RootNode()) + + for { + m, ok := qc.NextMatch() + if !ok { + break + } + + for _, c := range m.Captures { + // Find the theme attribute for the capture name (e.g., "function", "keyword"). + captureName := s.Query.CaptureNameForId(c.Index) + attr := getTermboxAttr(captureName) + + startRow := int(c.Node.StartPoint().Row) + startCol := int(c.Node.StartPoint().Column) + endRow := int(c.Node.EndPoint().Row) + endCol := int(c.Node.EndPoint().Column) + + // Map the capture span to line/column color attributes. + for r := startRow; r <= endRow; r++ { + if _, ok := s.Highlights[r]; !ok { + s.Highlights[r] = make(map[int]termbox.Attribute) + } + + cStart := 0 + if r == startRow { + cStart = startCol + } + + cEnd := -1 + if r == endRow { + cEnd = endCol + } + + limit := cEnd + if limit == -1 { + limit = 1000 // A reasonable overflow limit for whole lines. + } + + for col := cStart; col < limit; col++ { + s.Highlights[r][col] = attr + } + } + } + } +} + +// getTermboxAttr maps a tree-sitter capture name to a color name from our theme. +func getTermboxAttr(captureName string) termbox.Attribute { + var cn ColorName + switch captureName { + case "function": + cn = ColorTSFunction + case "tag": + cn = ColorTSTag + case "attribute": + cn = ColorTSAttribute + case "constant": + cn = ColorTSConstant + case "variable": + cn = ColorTSVariable + case "type": + cn = ColorTSType + case "string": + cn = ColorTSString + case "keyword": + cn = ColorTSKeyword + case "comment": + cn = ColorTSComment + case "number": + cn = ColorTSNumber + case "boolean": + cn = ColorTSBoolean + case "null": + cn = ColorTSNull + case "property": + cn = ColorTSProperty + default: + return termbox.ColorDefault + } + + fg, _ := GetThemeColor(cn) + return fg +} + +// Highlight returns a slice of attributes for each character in a line. +func (s *SyntaxHighlighter) Highlight(lineIdx int, lineContent []rune) []termbox.Attribute { + attrs := make([]termbox.Attribute, len(lineContent)) + // Fill with default foreground color first. + defaultFg, _ := GetThemeColor(ColorDefault) + for i := range attrs { + attrs[i] = defaultFg + } + + // Apply cached highlights if they exist for this line. + if lineHighlights, ok := s.Highlights[lineIdx]; ok { + for col, color := range lineHighlights { + if col < len(attrs) { + attrs[col] = color + } + } + } + + return attrs +} |
