grimoir/syntax.go
2026-07-04 13:04:33 +03:00

363 lines
No EOL
12 KiB
Go
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"regexp"
"strings"
"sync"
"github.com/charmbracelet/lipgloss"
)
// === Цвета подсветки синтаксиса ===
var (
colorKeyword = lipgloss.NewStyle().Foreground(lipgloss.Color("207")).Bold(true)
colorString = lipgloss.NewStyle().Foreground(lipgloss.Color("106"))
colorComment = lipgloss.NewStyle().Foreground(lipgloss.Color("59")).Italic(true)
colorNumber = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
colorFunc = lipgloss.NewStyle().Foreground(lipgloss.Color("81")).Bold(true)
)
// LanguageConfig описывает правила подсветки для одного языка
type LanguageConfig struct {
Keywords []string
LineCommentPrefix string
StringDelimiters []string
HasNumbers bool
CaseInsensitive bool
VarPrefix string
}
var languageConfigs = map[string]LanguageConfig{
"go": {
Keywords: []string{"func", "package", "import", "var", "const", "type", "struct", "interface", "if", "else", "for", "switch", "case", "default", "return", "break", "continue", "defer", "go", "chan", "select", "map", "range", "nil", "true", "false"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, "`"},
HasNumbers: true,
},
"py": {
Keywords: []string{"def", "class", "if", "else", "elif", "for", "while", "import", "from", "return", "pass", "break", "continue", "lambda", "try", "except", "finally", "with", "as", "True", "False", "None", "and", "or", "not", "in", "is", "raise", "yield", "global", "nonlocal", "del", "assert"},
LineCommentPrefix: "#",
StringDelimiters: []string{`"`, `'`},
HasNumbers: true,
},
"js": {
Keywords: []string{"function", "const", "let", "var", "if", "else", "for", "while", "switch", "case", "default", "return", "break", "continue", "async", "await", "import", "export", "class", "extends", "new", "this", "super", "true", "false", "null", "undefined", "typeof", "instanceof", "void", "delete", "throw", "try", "catch", "finally"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, `'`, "`"},
HasNumbers: true,
},
"ts": {
Keywords: []string{"function", "const", "let", "var", "if", "else", "for", "while", "switch", "case", "default", "return", "break", "continue", "async", "await", "import", "export", "class", "extends", "new", "this", "super", "true", "false", "null", "undefined", "type", "interface", "enum", "implements", "namespace", "abstract", "readonly", "public", "private", "protected"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, `'`, "`"},
HasNumbers: true,
},
"rs": {
Keywords: []string{"fn", "let", "mut", "const", "static", "if", "else", "match", "for", "while", "loop", "break", "continue", "return", "pub", "mod", "use", "struct", "enum", "trait", "impl", "unsafe", "async", "await", "move", "ref", "self", "Self", "super", "crate", "true", "false"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"c": {
Keywords: []string{"int", "char", "void", "float", "double", "long", "short", "unsigned", "signed", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "struct", "union", "enum", "typedef", "static", "const", "extern", "volatile", "sizeof", "NULL"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"cpp": {
Keywords: []string{"int", "char", "void", "float", "double", "long", "short", "unsigned", "signed", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "struct", "union", "enum", "typedef", "static", "const", "extern", "volatile", "class", "public", "private", "protected", "virtual", "override", "new", "delete", "nullptr", "true", "false", "auto", "template", "typename", "namespace", "using", "this"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"java": {
Keywords: []string{"class", "interface", "extends", "implements", "public", "private", "protected", "static", "final", "void", "int", "boolean", "char", "double", "float", "long", "short", "byte", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "new", "this", "super", "null", "true", "false", "try", "catch", "finally", "throw", "throws", "import", "package", "abstract", "synchronized"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"sql": {
Keywords: []string{"SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "CREATE", "TABLE", "DROP", "ALTER", "ADD", "COLUMN", "INDEX", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "ON", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", "AS", "DISTINCT", "COUNT", "SUM", "AVG", "MAX", "MIN", "NULL", "IS", "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "CONSTRAINT", "UNIQUE", "DEFAULT"},
LineCommentPrefix: "--",
StringDelimiters: []string{`'`},
HasNumbers: true,
CaseInsensitive: true,
},
"sh": {
Keywords: []string{"if", "then", "else", "elif", "fi", "for", "do", "done", "while", "case", "esac", "function", "return", "export", "local", "readonly", "unset", "shift", "echo", "exit", "break", "continue", "true", "false"},
LineCommentPrefix: "#",
StringDelimiters: []string{`"`, `'`},
HasNumbers: false,
VarPrefix: "$",
},
}
// === Кеш скомпилированных regexp ===
// compiledLangPatterns — скомпилированные паттерны для одного языка
type compiledLangPatterns struct {
strings []*regexp.Regexp // по одному на каждый StringDelimiter
keywords []*regexp.Regexp // по одному на каждое ключевое слово
numbers *regexp.Regexp // nil если !HasNumbers
varPattern *regexp.Regexp // nil если VarPrefix == ""
}
var (
patternCache = map[string]*compiledLangPatterns{}
patternCacheMu sync.RWMutex
)
// getPatterns возвращает скомпилированные паттерны для языка (из кеша или компилирует)
func getPatterns(fileType string, cfg LanguageConfig) *compiledLangPatterns {
patternCacheMu.RLock()
if p, ok := patternCache[fileType]; ok {
patternCacheMu.RUnlock()
return p
}
patternCacheMu.RUnlock()
// Компилируем один раз
p := &compiledLangPatterns{}
for _, delim := range cfg.StringDelimiters {
var re *regexp.Regexp
switch delim {
case `"`:
re = regexp.MustCompile(`"(?:[^"\\]|\\.)*"`)
case `'`:
re = regexp.MustCompile(`'(?:[^'\\]|\\.)*'`)
case "`":
re = regexp.MustCompile("`[^`]*`")
}
p.strings = append(p.strings, re)
}
for _, kw := range cfg.Keywords {
var re *regexp.Regexp
if cfg.CaseInsensitive {
re = regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(kw) + `\b`)
} else {
re = regexp.MustCompile(`\b` + regexp.QuoteMeta(kw) + `\b`)
}
p.keywords = append(p.keywords, re)
}
if cfg.HasNumbers {
p.numbers = regexp.MustCompile(`\b\d+(\.\d+)?\b`)
}
if cfg.VarPrefix != "" {
p.varPattern = regexp.MustCompile(regexp.QuoteMeta(cfg.VarPrefix) + `\w+`)
}
patternCacheMu.Lock()
patternCache[fileType] = p
patternCacheMu.Unlock()
return p
}
// === Кеш подсвеченных строк ===
// highlightCacheKey — ключ кеша подсветки
type highlightCacheKey struct {
line string
fileType string
}
var (
highlightCache = map[highlightCacheKey]string{}
highlightCacheMu sync.RWMutex
// Ограничиваем размер кеша — не кешируем больше N строк
highlightCacheMax = 2000
)
// HighlightLine подсвечивает одну строку кода
func HighlightLine(line, fileType string) string {
cfg, ok := languageConfigs[fileType]
if !ok {
return line
}
// Проверяем кеш
key := highlightCacheKey{line, fileType}
highlightCacheMu.RLock()
if cached, ok := highlightCache[key]; ok {
highlightCacheMu.RUnlock()
return cached
}
highlightCacheMu.RUnlock()
// Вычисляем
patterns := getPatterns(fileType, cfg)
tokens := tokenizeWithPatterns(line, cfg, patterns)
result := renderTokens(tokens, cfg)
// Сохраняем в кеш
highlightCacheMu.Lock()
if len(highlightCache) < highlightCacheMax {
highlightCache[key] = result
}
highlightCacheMu.Unlock()
return result
}
// InvalidateHighlightCache сбрасывает кеш подсветки (вызывается при изменении файла)
func InvalidateHighlightCache() {
highlightCacheMu.Lock()
highlightCache = map[highlightCacheKey]string{}
highlightCacheMu.Unlock()
}
// tokenKind — тип токена
type tokenKind int
const (
tokenPlain tokenKind = iota
tokenKeyword
tokenString
tokenComment
tokenNumber
tokenVar
)
// token — фрагмент строки с типом
type token struct {
text string
kind tokenKind
}
// tokenizeWithPatterns разбивает строку на токены используя предкомпилированные regexp
func tokenizeWithPatterns(line string, cfg LanguageConfig, p *compiledLangPatterns) []token {
if line == "" {
return nil
}
// Находим комментарий
commentStart := -1
if cfg.LineCommentPrefix != "" {
commentStart = strings.Index(line, cfg.LineCommentPrefix)
}
code := line
comment := ""
if commentStart >= 0 {
code = line[:commentStart]
comment = line[commentStart:]
}
n := len(code)
kinds := make([]tokenKind, n)
// Строки
for _, re := range p.strings {
if re == nil {
continue
}
for _, loc := range re.FindAllStringIndex(code, -1) {
already := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] != tokenPlain {
already = true
break
}
}
if !already {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenString
}
}
}
}
// Ключевые слова
for _, re := range p.keywords {
for _, loc := range re.FindAllStringIndex(code, -1) {
inString := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] == tokenString {
inString = true
break
}
}
if !inString {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenKeyword
}
}
}
}
// Числа
if p.numbers != nil {
for _, loc := range p.numbers.FindAllStringIndex(code, -1) {
skip := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] != tokenPlain {
skip = true
break
}
}
if !skip {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenNumber
}
}
}
}
// Переменные
if p.varPattern != nil {
for _, loc := range p.varPattern.FindAllStringIndex(code, -1) {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenVar
}
}
}
// Склеиваем в токены
var tokens []token
i := 0
for i < n {
k := kinds[i]
j := i + 1
for j < n && kinds[j] == k {
j++
}
tokens = append(tokens, token{text: code[i:j], kind: k})
i = j
}
if comment != "" {
tokens = append(tokens, token{text: comment, kind: tokenComment})
}
return tokens
}
// renderTokens применяет lipgloss стили к токенам
func renderTokens(tokens []token, cfg LanguageConfig) string {
var sb strings.Builder
for _, t := range tokens {
switch t.kind {
case tokenKeyword:
text := t.text
if cfg.CaseInsensitive {
text = strings.ToUpper(text)
}
sb.WriteString(colorKeyword.Render(text))
case tokenString:
sb.WriteString(colorString.Render(t.text))
case tokenComment:
sb.WriteString(colorComment.Render(t.text))
case tokenNumber:
sb.WriteString(colorNumber.Render(t.text))
case tokenVar:
sb.WriteString(colorFunc.Render(t.text))
default:
sb.WriteString(t.text)
}
}
return sb.String()
}