363 lines
No EOL
12 KiB
Go
Executable file
363 lines
No EOL
12 KiB
Go
Executable file
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()
|
||
} |