package main import ( "fmt" "os" "sort" "strings" sitter "github.com/tree-sitter/go-tree-sitter" ts_go "github.com/tree-sitter/tree-sitter-go/bindings/go" ) // Sample Go source to highlight const source = ` package main func main () { println("Hello" + 5) } ` type Highlight struct { StartRow uint // 0-indexed line number StartCol uint // 0-indexed column (bytes) EndRow uint EndCol uint Capture string } // Theme maps capture names to ANSI escape codes. // In your editor you'd use lipgloss styles instead. var theme = map[string]string{ "keyword": "\033[1;35m", // bold magenta "keyword.type": "\033[1;35m", "keyword.function": "\033[1;35m", "keyword.return": "\033[1;35m", "keyword.coroutine": "\033[1;35m", "keyword.repeat": "\033[1;35m", "keyword.import": "\033[1;35m", "keyword.conditional": "\033[1;35m", "type": "\033[33m", // yellow "type.builtin": "\033[33m", "type.definition": "\033[1;33m", // bold yellow "function": "\033[1;34m", // bold blue "function.call": "\033[34m", // blue "function.method": "\033[34m", "function.method.call": "\033[34m", "function.builtin": "\033[1;31m", "variable": "\033[37m", // white "variable.parameter": "\033[3;37m", // italic white "variable.member": "\033[37m", "constant": "\033[1;36m", // bold cyan "constant.builtin": "\033[1;36m", "string": "\033[32m", // green "string.escape": "\033[1;32m", "number": "\033[36m", // cyan "number.float": "\033[36m", "boolean": "\033[36m", "operator": "\033[93m", // bright yellow "comment": "\033[2;37m", // dim "comment.documentation": "\033[2;37m", "module": "\033[35m", // magenta "label": "\033[33m", "property": "\033[37m", "constructor": "\033[1;33m", "punctuation.delimiter": "\033[37m", "punctuation.bracket": "\033[37m", } const reset = "\033[0m" func main() { code := []byte(source) lines := strings.Split(source, "\n") // --- Step 1: Parse --- lang := sitter.NewLanguage(ts_go.Language()) parser := sitter.NewParser() defer parser.Close() parser.SetLanguage(lang) tree := parser.Parse(code, nil) defer tree.Close() root := tree.RootNode() // --- Step 2: Load query from highlights.scm --- queryBytes, err := os.ReadFile("queries/go/highlights.scm") if err != nil { fmt.Fprintf(os.Stderr, "Failed to read highlights.scm: %v\n", err) return } query, queryErr := sitter.NewQuery(lang, string(queryBytes)) if queryErr != nil { fmt.Fprintf(os.Stderr, "Query error: %v\n", queryErr) return } defer query.Close() // --- Step 3: Run query --- cursor := sitter.NewQueryCursor() defer cursor.Close() captures := cursor.Captures(query, root, code) var highlights []Highlight for match, captureIdx := captures.Next(); match != nil; match, captureIdx = captures.Next() { capture := match.Captures[captureIdx] captureName := query.CaptureNames()[capture.Index] // Skip @spell — it's a nvim spellcheck hint, not a highlight if captureName == "spell" { continue } node := capture.Node start := node.StartPosition() end := node.EndPosition() highlights = append(highlights, Highlight{ StartRow: start.Row, StartCol: start.Column, EndRow: end.Row, EndCol: end.Column, Capture: captureName, }) } // --- Step 4: Show captures with positions --- fmt.Println("=== Captures (row:col → row:col) ===") for _, h := range highlights { // Extract text for display using the source lines text := extractText(lines, h) fmt.Printf(" %d:%-2d → %d:%-2d @%-22s %q\n", h.StartRow, h.StartCol, h.EndRow, h.EndCol, h.Capture, text) } fmt.Println() // --- Step 5: Render with colors using row:col positions --- // Build a per-line map of column ranges to capture names. // Sort so wider (less specific) ranges come first — last writer wins. sort.Slice(highlights, func(i, j int) bool { if highlights[i].StartRow == highlights[j].StartRow { if highlights[i].StartCol == highlights[j].StartCol { // Wider range first so more specific overwrites it if highlights[i].EndRow == highlights[j].EndRow { return highlights[i].EndCol > highlights[j].EndCol } return highlights[i].EndRow > highlights[j].EndRow } return highlights[i].StartCol < highlights[j].StartCol } return highlights[i].StartRow < highlights[j].StartRow }) // captureAt[row][col] = capture name (last writer wins) captureAt := make(map[uint]map[uint]string) for _, h := range highlights { for row := h.StartRow; row <= h.EndRow; row++ { if captureAt[row] == nil { captureAt[row] = make(map[uint]string) } startCol := uint(0) if row == h.StartRow { startCol = h.StartCol } endCol := uint(len(lines[row])) if row == h.EndRow { endCol = h.EndCol } for col := startCol; col < endCol; col++ { captureAt[row][col] = h.Capture } } } fmt.Println("=== Colored output ===") printColored(lines, captureAt) // ===================================================================== // INCREMENTAL PARSING DEMO // ===================================================================== // When a user types in your editor, you don't re-parse the whole file. // Instead you: // 1. Tell the OLD tree what changed (tree.Edit) // 2. Parse the new source, passing the old tree // 3. Tree-sitter reuses unchanged nodes and only re-parses the edit // 4. Use ChangedRanges to know which lines need re-highlighting // // This is O(edit size + log(file size)) instead of O(file size). fmt.Println("\n========================================") fmt.Println("=== INCREMENTAL PARSE DEMO ===") fmt.Println("========================================") fmt.Println() // Simulate: user changes "Hello" → "Goodbye" on row 4 // Before: println("Hello" + 5) // After: println("Goodbye" + 5) oldSource := source newSource := strings.Replace(oldSource, `"Hello"`, `"Goodbye"`, 1) // Find where the edit happened (in a real editor you already know this // from the keystroke — you don't need to search for it) editStart := strings.Index(oldSource, `"Hello"`) oldEnd := editStart + len(`"Hello"`) newEnd := editStart + len(`"Goodbye"`) // Convert byte offset to row:col for the InputEdit editStartPoint := byteToPoint(oldSource, uint(editStart)) oldEndPoint := byteToPoint(oldSource, uint(oldEnd)) newEndPoint := byteToPoint(newSource, uint(newEnd)) fmt.Printf("Edit: replaced %q → %q\n", "Hello", "Goodbye") fmt.Printf(" at byte %d, row %d col %d\n", editStart, editStartPoint.Row, editStartPoint.Column) fmt.Println() // Step 1: Tell the old tree what changed tree.Edit(&sitter.InputEdit{ StartByte: uint(editStart), OldEndByte: uint(oldEnd), NewEndByte: uint(newEnd), StartPosition: editStartPoint, OldEndPosition: oldEndPoint, NewEndPosition: newEndPoint, }) // Step 2: Parse the new source, passing the old (edited) tree. // Tree-sitter will REUSE all nodes that weren't affected by the edit // and only re-parse the region around the change. newCode := []byte(newSource) newTree := parser.Parse(newCode, tree) defer newTree.Close() // Step 3: See exactly which ranges changed changedRanges := newTree.ChangedRanges(tree) fmt.Printf("Changed ranges: %d\n", len(changedRanges)) for i, r := range changedRanges { fmt.Printf(" range %d: row %d:%d → row %d:%d\n", i, r.StartPoint.Row, r.StartPoint.Column, r.EndPoint.Row, r.EndPoint.Column) } fmt.Println() // In your editor, you'd ONLY re-run the highlight query on the changed // ranges (using cursor.SetByteRange or cursor.SetPointRange), then // update just those lines in your display. Everything else stays cached. // For this demo, let's re-highlight the full new tree to show the result newRoot := newTree.RootNode() newLines := strings.Split(newSource, "\n") cursor2 := sitter.NewQueryCursor() defer cursor2.Close() newCaptures := cursor2.Captures(query, newRoot, newCode) var newHighlights []Highlight for match, captureIdx := newCaptures.Next(); match != nil; match, captureIdx = newCaptures.Next() { capture := match.Captures[captureIdx] captureName := query.CaptureNames()[capture.Index] if captureName == "spell" { continue } node := capture.Node start := node.StartPosition() end := node.EndPosition() newHighlights = append(newHighlights, Highlight{ StartRow: start.Row, StartCol: start.Column, EndRow: end.Row, EndCol: end.Column, Capture: captureName, }) } sort.Slice(newHighlights, func(i, j int) bool { if newHighlights[i].StartRow == newHighlights[j].StartRow { if newHighlights[i].StartCol == newHighlights[j].StartCol { if newHighlights[i].EndRow == newHighlights[j].EndRow { return newHighlights[i].EndCol > newHighlights[j].EndCol } return newHighlights[i].EndRow > newHighlights[j].EndRow } return newHighlights[i].StartCol < newHighlights[j].StartCol } return newHighlights[i].StartRow < newHighlights[j].StartRow }) newCaptureAt := make(map[uint]map[uint]string) for _, h := range newHighlights { for row := h.StartRow; row <= h.EndRow; row++ { if newCaptureAt[row] == nil { newCaptureAt[row] = make(map[uint]string) } startCol := uint(0) if row == h.StartRow { startCol = h.StartCol } endCol := uint(len(newLines[row])) if row == h.EndRow { endCol = h.EndCol } for col := startCol; col < endCol; col++ { newCaptureAt[row][col] = h.Capture } } } fmt.Println("=== After edit (colored output) ===") printColored(newLines, newCaptureAt) } // printColored renders source lines with ANSI colors based on the capture map. func printColored(lines []string, captureAt map[uint]map[uint]string) { for row, line := range lines { currentCapture := "" for col := uint(0); col < uint(len(line)); col++ { cap := "" if rowMap, ok := captureAt[uint(row)]; ok { cap = rowMap[col] } if cap != currentCapture { if currentCapture != "" { fmt.Print(reset) } if color, ok := theme[cap]; ok { fmt.Print(color) } currentCapture = cap } fmt.Print(string(line[col])) } if currentCapture != "" { fmt.Print(reset) } fmt.Println() } } // byteToPoint converts a byte offset into a row:col Point. func byteToPoint(src string, offset uint) sitter.Point { row := uint(0) col := uint(0) for i := range offset { if src[i] == '\n' { row++ col = 0 } else { col++ } } return sitter.NewPoint(row, col) } // extractText pulls the highlighted text from source lines using row:col positions. func extractText(lines []string, h Highlight) string { if h.StartRow == h.EndRow { line := lines[h.StartRow] end := min(h.EndCol, uint(len(line))) return line[h.StartCol:end] } // Multi-line highlight (rare, but possible for block comments etc.) var result string for row := h.StartRow; row <= h.EndRow; row++ { line := lines[row] start := uint(0) if row == h.StartRow { start = h.StartCol } end := uint(len(line)) if row == h.EndRow { end = h.EndCol } if result != "" { result += "\n" } result += line[start:end] } return result }