Download
181 lines 5 KiBGoModified 2026-10-18 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
package highlight

import (
	"cmp"
	"path/filepath"
	"slices"
	"sort"
	"strings"
	"sync"

	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/lexers"
)

// chroma's LexerRegistry.Match tries every filename glob of every lexer,
// each again with every backup suffix: ~15,000 filepath.Match calls, about
// 8ms per lookup, and Get falls back to two of those on any unknown name.
// builtinIndex gives the same answers from hash tables: nearly all globs
// are "*.ext" or a literal name, leaving a dozen to match one by one.

// ignoredSuffixes mirrors chroma's list of backup suffixes a filename may
// carry and still match a glob ("foo.py.bak" is Python).
var ignoredSuffixes = [...]string{
	"~", ".bak", ".old", ".orig",
	".dpkg-dist", ".dpkg-old", ".ucf-dist", ".ucf-new", ".ucf-old",
	".rpmnew", ".rpmorig", ".rpmsave",
	".in",
}

// globHit is one glob that matched: its lexer's position in the registry
// and its position in that lexer's list. chroma breaks priority ties by the
// order it finds matches, so hits are replayed in this order.
type globHit struct{ lexer, glob int }

type globRule struct {
	pattern string
	hit     globHit
}

// globIndex holds one tier of globs (Filenames or AliasFilenames).
type globIndex struct {
	exact map[string][]globHit // literal names: "Makefile"
	ext   map[string][]globHit // "*.ext" globs, keyed by ".ext"
	other []globRule           // everything else: "*.[1-9]", "Makefile.*"
}

func (g *globIndex) add(pattern string, hit globHit) {
	switch {
	case !strings.ContainsAny(pattern, `*?[\`):
		g.exact[pattern] = append(g.exact[pattern], hit)
	case strings.HasPrefix(pattern, "*.") && !strings.ContainsAny(pattern[1:], `*?[\`):
		g.ext[pattern[1:]] = append(g.ext[pattern[1:]], hit)
	default:
		g.other = append(g.other, globRule{pattern, hit})
	}
}

// hits returns every glob matching name, directly or once a backup suffix
// is stripped: the literal suffix ends glob+suf, so glob+suf matches name
// exactly when name ends in suf and glob matches the rest.
func (g *globIndex) hits(name string) []globHit {
	var out []globHit
	try := func(n string) {
		out = append(out, g.exact[n]...)
		for i := range len(n) {
			if n[i] == '.' {
				out = append(out, g.ext[n[i:]]...)
			}
		}
		for _, r := range g.other {
			if ok, _ := filepath.Match(r.pattern, n); ok {
				out = append(out, r.hit)
			}
		}
	}
	try(name)
	for _, suf := range ignoredSuffixes {
		if rest, ok := strings.CutSuffix(name, suf); ok {
			try(rest)
		}
	}
	return out
}

type builtinIndex struct {
	lexers  []chroma.Lexer
	byName  map[string]chroma.Lexer
	byAlias map[string]chroma.Lexer
	tiers   [2]globIndex // Filenames, then AliasFilenames
}

var builtins = sync.OnceValue(func() *builtinIndex {
	ix := &builtinIndex{
		lexers:  lexers.GlobalLexerRegistry.Lexers,
		byName:  map[string]chroma.Lexer{},
		byAlias: map[string]chroma.Lexer{},
	}
	for t := range ix.tiers {
		ix.tiers[t] = globIndex{exact: map[string][]globHit{}, ext: map[string][]globHit{}}
	}
	for i, lx := range ix.lexers {
		cfg := lx.Config()
		ix.byName[cfg.Name] = lx
		ix.byName[strings.ToLower(cfg.Name)] = lx
		for _, a := range cfg.Aliases {
			ix.byAlias[a] = lx
			ix.byAlias[strings.ToLower(a)] = lx
		}
		for j, g := range cfg.Filenames {
			ix.tiers[0].add(g, globHit{i, j})
		}
		for j, g := range cfg.AliasFilenames {
			ix.tiers[1].add(g, globHit{i, j})
		}
	}
	return ix
})

// match is lexers.Match: the highest-priority lexer whose Filenames match,
// else whose AliasFilenames match, else nil.
func (ix *builtinIndex) match(filename string) chroma.Lexer {
	name := filepath.Base(filename)
	for t := range ix.tiers {
		hits := ix.tiers[t].hits(name)
		if len(hits) == 0 {
			continue
		}
		slices.SortFunc(hits, func(a, b globHit) int {
			return cmp.Or(cmp.Compare(a.lexer, b.lexer), cmp.Compare(a.glob, b.glob))
		})
		var matched chroma.PrioritisedLexers
		for _, h := range slices.Compact(hits) {
			matched = append(matched, ix.lexers[h.lexer])
		}
		sort.Sort(matched)
		return matched[0]
	}
	return nil
}

// get is lexers.Get: by name, by alias, then as a file extension or name.
func (ix *builtinIndex) get(name string) chroma.Lexer {
	lower := strings.ToLower(name)
	for _, lx := range []chroma.Lexer{ix.byName[name], ix.byAlias[name], ix.byName[lower], ix.byAlias[lower]} {
		if lx != nil {
			return lx
		}
	}
	var candidates chroma.PrioritisedLexers
	for _, f := range []string{"filename." + name, name} {
		if lx := ix.match(f); lx != nil {
			candidates = append(candidates, lx)
		}
	}
	if len(candidates) == 0 {
		return nil
	}
	sort.Sort(candidates)
	return candidates[0]
}

// names returns every lowercase name get resolves without wildcard
// globs: lexer names and aliases, extensions and literal filenames.
func (ix *builtinIndex) names() []string {
	var out []string
	for k := range ix.byName {
		out = append(out, strings.ToLower(k))
	}
	for k := range ix.byAlias {
		out = append(out, strings.ToLower(k))
	}
	for _, t := range ix.tiers {
		for k := range t.exact {
			out = append(out, strings.ToLower(k))
		}
		for k := range t.ext {
			out = append(out, strings.ToLower(k[1:]))
		}
	}
	return out
}