184 lines
5.1 KiB
Go
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:]
|
|
}
|