286 lines
10 KiB
Go
286 lines
10 KiB
Go
package main
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
|
||
"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: "$",
|
||
},
|
||
}
|
||
|
||
// tokenKind — тип токена
|
||
type tokenKind int
|
||
|
||
const (
|
||
tokenPlain tokenKind = iota
|
||
tokenKeyword // ключевое слово
|
||
tokenString // строковый литерал
|
||
tokenComment // комментарий
|
||
tokenNumber // число
|
||
tokenVar // переменная ($VAR)
|
||
)
|
||
|
||
// token — фрагмент строки с типом
|
||
type token struct {
|
||
text string
|
||
kind tokenKind
|
||
}
|
||
|
||
// HighlightLine подсвечивает одну строку кода
|
||
func HighlightLine(line, fileType string) string {
|
||
cfg, ok := languageConfigs[fileType]
|
||
if !ok {
|
||
return line
|
||
}
|
||
tokens := tokenize(line, cfg)
|
||
return renderTokens(tokens, cfg)
|
||
}
|
||
|
||
// tokenize разбивает строку на токены не применяя стили
|
||
func tokenize(line string, cfg LanguageConfig) []token {
|
||
if line == "" {
|
||
return nil
|
||
}
|
||
|
||
// Шаг 1: находим комментарий — всё после него plain (красим как comment целиком)
|
||
commentStart := -1
|
||
if cfg.LineCommentPrefix != "" {
|
||
commentStart = strings.Index(line, cfg.LineCommentPrefix)
|
||
}
|
||
|
||
code := line
|
||
comment := ""
|
||
if commentStart >= 0 {
|
||
code = line[:commentStart]
|
||
comment = line[commentStart:]
|
||
}
|
||
|
||
// Шаг 2: разбиваем code на токены через span-маппинг
|
||
// Каждая позиция байта получает тип; потом склеиваем в токены
|
||
n := len(code)
|
||
kinds := make([]tokenKind, n)
|
||
|
||
// Строковые литералы
|
||
for _, delim := range cfg.StringDelimiters {
|
||
var re *regexp.Regexp
|
||
switch delim {
|
||
case `"`:
|
||
re = regexp.MustCompile(`"(?:[^"\\]|\\.)*"`)
|
||
case `'`:
|
||
re = regexp.MustCompile(`'(?:[^'\\]|\\.)*'`)
|
||
case "`":
|
||
re = regexp.MustCompile("`[^`]*`")
|
||
}
|
||
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 _, 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`)
|
||
}
|
||
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 cfg.HasNumbers {
|
||
re := regexp.MustCompile(`\b\d+(\.\d+)?\b`)
|
||
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 {
|
||
// Не красим если уже keyword
|
||
isKW := false
|
||
for i := loc[0]; i < loc[1]; i++ {
|
||
if kinds[i] == tokenKeyword {
|
||
isKW = true
|
||
break
|
||
}
|
||
}
|
||
if !isKW {
|
||
for i := loc[0]; i < loc[1]; i++ {
|
||
kinds[i] = tokenNumber
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Переменные ($VAR)
|
||
if cfg.VarPrefix != "" {
|
||
re := regexp.MustCompile(regexp.QuoteMeta(cfg.VarPrefix) + `\w+`)
|
||
for _, loc := range re.FindAllStringIndex(code, -1) {
|
||
for i := loc[0]; i < loc[1]; i++ {
|
||
kinds[i] = tokenVar
|
||
}
|
||
}
|
||
}
|
||
|
||
// Шаг 3: склеиваем соседние байты одного типа в токены
|
||
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()
|
||
}
|