Gim/internal/syntax/registry.go
2026-04-07 11:01:07 -07:00

184 lines
5.1 KiB
Go

package syntax
import (
"fmt"
"strings"
sitter "github.com/tree-sitter/go-tree-sitter"
ts_go "github.com/tree-sitter/tree-sitter-go/bindings/go"
ts_js "github.com/tree-sitter/tree-sitter-javascript/bindings/go"
)
type languagePack struct {
// languagePack.id is the stable registry identifier (for example, "go").
id string
// languagePack.filetypes are normalized aliases resolved from buffer filetype.
filetypes []string
// languagePack.extensions are normalized filename extensions (for example, ".go").
extensions []string
// languagePack.newLanguage constructs the tree-sitter language handle.
newLanguage func() *sitter.Language
// languagePack.loadQuery returns highlights query source for this language.
loadQuery func() ([]byte, error)
}
// resolvedLanguage stores compiled runtime assets for one language.
//
// Instances are cached in languageRegistry.compiledByLang and reused by all
// buffers that resolve to the same language id.
type resolvedLanguage struct {
id string
language *sitter.Language
query *sitter.Query
}
// languageRegistry maps buffer metadata to language packs and lazily compiles
// tree-sitter language/query assets.
type languageRegistry struct {
packs []languagePack
byFiletype map[string]languagePack
byExtension map[string]languagePack
compiledByLang map[string]*resolvedLanguage
}
// newLanguageRegistry constructs the default in-process language registry.
//
// It registers built-in packs and prepares lookup maps for filetype and
// extension resolution.
func newLanguageRegistry() *languageRegistry {
r := &languageRegistry{
packs: []languagePack{},
byFiletype: map[string]languagePack{},
byExtension: map[string]languagePack{},
compiledByLang: map[string]*resolvedLanguage{},
}
r.register(languagePack{
id: "go",
filetypes: []string{"go", "golang"},
extensions: []string{".go"},
newLanguage: func() *sitter.Language { return sitter.NewLanguage(ts_go.Language()) },
loadQuery: loadGoHighlightsQuery,
})
r.register(languagePack{
id: "javascript",
filetypes: []string{"javascript", "js", "jsx"},
extensions: []string{".js", ".mjs", ".cjs", ".jsx"},
newLanguage: func() *sitter.Language { return sitter.NewLanguage(ts_js.Language()) },
loadQuery: loadJavaScriptHighlightsQuery,
})
r.register(languagePack{
id: "gomod",
filetypes: []string{"gomod"},
extensions: []string{".mod"},
newLanguage: func() *sitter.Language { return sitter.NewLanguage(ts_js.Language()) },
loadQuery: loadJavaScriptHighlightsQuery,
})
return r
}
// register adds a language pack and indexes it by normalized keys.
func (r *languageRegistry) register(pack languagePack) {
r.packs = append(r.packs, pack)
for _, ft := range pack.filetypes {
n := normalizeKey(ft)
if n != "" {
r.byFiletype[n] = pack
}
}
for _, ext := range pack.extensions {
n := normalizeExtension(ext)
if n != "" {
r.byExtension[n] = pack
}
}
}
// resolve returns compiled language/query assets for a buffer identity.
//
// Resolution is filetype-first, extension-second. Results are compiled once
// per language id and cached in compiledByLang.
func (r *languageRegistry) resolve(filetype, filename string) (*resolvedLanguage, bool, error) {
pack, ok := r.resolvePack(filetype, filename)
if !ok {
return nil, false, nil
}
if cached, ok := r.compiledByLang[pack.id]; ok {
return cached, true, nil
}
lang := pack.newLanguage()
if lang == nil {
return nil, false, fmt.Errorf("language %q did not provide a language handle", pack.id)
}
qBytes, err := pack.loadQuery()
if err != nil {
return nil, false, fmt.Errorf("load query for %q: %w", pack.id, err)
}
q, qErr := sitter.NewQuery(lang, string(qBytes))
if qErr != nil {
return nil, false, fmt.Errorf("compile query for %q: %w", pack.id, qErr)
}
resolved := &resolvedLanguage{id: pack.id, language: lang, query: q}
r.compiledByLang[pack.id] = resolved
return resolved, true, nil
}
// resolvePack finds a registered language pack using normalized buffer
// metadata without compiling queries.
func (r *languageRegistry) resolvePack(filetype, filename string) (languagePack, bool) {
if p, ok := r.byFiletype[normalizeKey(filetype)]; ok {
return p, true
}
if p, ok := r.byExtension[extensionOf(filename)]; ok {
return p, true
}
return languagePack{}, false
}
// normalizeKey canonicalizes filetype-like keys for registry lookups.
func normalizeKey(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
s = strings.TrimPrefix(s, ".")
return s
}
// normalizeExtension canonicalizes extension keys and guarantees a leading
// dot for non-empty values.
func normalizeExtension(ext string) string {
ext = strings.TrimSpace(strings.ToLower(ext))
if ext == "" {
return ""
}
if !strings.HasPrefix(ext, ".") {
ext = "." + ext
}
return ext
}
// extensionOf extracts a normalized extension from a filename.
// Returns empty string when no usable extension is present.
func extensionOf(filename string) string {
name := strings.TrimSpace(strings.ToLower(filename))
if name == "" {
return ""
}
i := strings.LastIndex(name, ".")
if i <= 0 || i == len(name)-1 {
return ""
}
return name[i:]
}