From e4462f9674fbd60c5832603bd6490d8c616e977b Mon Sep 17 00:00:00 2001 From: EnumDev Date: Mon, 4 May 2026 21:33:35 +0300 Subject: [PATCH] Add simple syntax highlighting --- config/config.yml | 2 +- config/styles/classic.yml | 8 +++ config/styles/default.yml | 11 +++- config/syntax/yaml.yml | 15 ++++++ src/buffer.go | 38 +++++++++++++ src/command.go | 46 ++++++++++++++++ src/highlighting.go | 109 ++++++++++++++++++++++++++++++++++++++ src/main.go | 3 ++ src/style.go | 10 +++- src/top_menu.go | 6 +++ 10 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 config/syntax/yaml.yml create mode 100644 src/highlighting.go diff --git a/config/config.yml b/config/config.yml index 4ffa822..c6c4b44 100644 --- a/config/config.yml +++ b/config/config.yml @@ -6,5 +6,5 @@ selected_style_fallback: "default-fallback" # Style for 8-color capable terminal show_top_menu: true show_line_index: true extend_line_index: false # Extend line index to the bottom of the screen -buffer_info_message: "File: %f Cursor: (%x, %y, %p) Chars: %c" +buffer_info_message: "File: %f, Filetype: %t, Cursor: (%x, %y, %p), Chars: %c" tab_indentation: 4 # Length of tab characters diff --git a/config/styles/classic.yml b/config/styles/classic.yml index 88c4ec0..aa02fdf 100644 --- a/config/styles/classic.yml +++ b/config/styles/classic.yml @@ -19,3 +19,11 @@ colors: message_bar_fg: "black" # Message bar text color input_bar_bg: "245" # Input bar background color input_bar_fg: "black" # Input bar text color + + # Syntax highlighting + syntax_comment: "darkgray" + syntax_keyword: "lightgreen" + syntax_identifier: "yellow" + syntax_constant: "purple" + syntax_variable: "yellow" + syntax_string: "purple" diff --git a/config/styles/default.yml b/config/styles/default.yml index a2a7955..9c6009d 100644 --- a/config/styles/default.yml +++ b/config/styles/default.yml @@ -5,6 +5,7 @@ style_type: "256-color" # Colors colors: + # Main colors buffer_area_bg: "234" # Buffer area background color buffer_area_fg: "white" # Buffer area text color buffer_area_sel: "243" # Buffer area selected text and cursor background color @@ -18,4 +19,12 @@ colors: message_bar_bg: "236" # Message bar background color message_bar_fg: "white" # Message bar text color input_bar_bg: "236" # Input bar background color - input_bar_fg: "white" # Input bar text color \ No newline at end of file + input_bar_fg: "white" # Input bar text color + + # Syntax highlighting + syntax_comment: "gray" + syntax_keyword: "gold" + syntax_identifier: "purple" + syntax_constant: "teal" + syntax_variable: "purple" + syntax_string: "blue" diff --git a/config/syntax/yaml.yml b/config/syntax/yaml.yml new file mode 100644 index 0000000..0610b05 --- /dev/null +++ b/config/syntax/yaml.yml @@ -0,0 +1,15 @@ +filetype: yaml +filenames: ".y[a]?ml$" + +rules: + - type: string # Strings + regex: '(["''])(.*?)(["''])' + - type: comment + regex: "#.*" + - type: identifier # Keys + regex: '^[[:blank:]]*(-.)?([a-z0-9\._\-])+:' + multiline: true + - type: constant # Number values + regex: '\d+(\.\d+)?' + - type: constant # True/false values + regex: '\b(YES|yes|Y|y|ON|on|TRUE|True|true|NO|no|N|n|OFF|off|FALSE|False|false)\b' diff --git a/src/buffer.go b/src/buffer.go index 2956dd8..7e70468 100644 --- a/src/buffer.go +++ b/src/buffer.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "slices" "strings" "typer/runestring" @@ -21,6 +22,7 @@ type Buffer struct { Selection *Selection canSave bool + filetype string filename string } @@ -55,14 +57,43 @@ func drawBuffer(window *Window) { bufferX, bufferY, _, _ := window.GetTextAreaDimensions() + parsedSyntaxes, err := HighlightString(string(buffer.GetContentsAsString()), buffer.filetype) + if err != nil { + window.PrintMessage(fmt.Sprintf("Could not parse regular expression in '%s' syntax: %s", buffer.filetype, err)) + } + + i := -1 for lineIndex, line := range buffer.Contents { for runeIndex, r := range append(line, ' ') { + i++ drawPosition := Position{runeIndex, lineIndex} if x-buffer.Offset.X >= bufferX && y-buffer.Offset.Y >= bufferY { // Default style style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg) + // Check for syntax highlighting + for _, parsedSyntax := range parsedSyntaxes { + if i >= parsedSyntax.StartIndex && i < parsedSyntax.EndIndex { + switch parsedSyntax.Type { + case "comment": + style = style.Foreground(CurrentStyle.SyntaxComment) + case "keyword": + style = style.Foreground(CurrentStyle.SyntaxKeyword) + case "identifier": + style = style.Foreground(CurrentStyle.SyntaxIdentifier) + case "constant": + style = style.Foreground(CurrentStyle.SyntaxConstant) + case "variable": + style = style.Foreground(CurrentStyle.SyntaxVariable) + case "string": + style = style.Foreground(CurrentStyle.SyntaxString) + } + + break + } + } + // Change background if under cursor if buffer.CursorPos.Equals(runeIndex, lineIndex) { style = style.Background(CurrentStyle.BufferAreaSel) @@ -135,6 +166,13 @@ func (buffer *Buffer) Load() error { buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y]) } + // Set buffer filetype + for _, syntax := range AvailableSyntaxes { + if ok, _ := regexp.MatchString(syntax.Filenames, buffer.filename); ok { + buffer.filetype = syntax.Filetype + } + } + return nil } diff --git a/src/command.go b/src/command.go index 6a90eb7..30ca9e5 100644 --- a/src/command.go +++ b/src/command.go @@ -418,6 +418,51 @@ func initCommands() { }, } + setFiletypeCmd := Command{ + cmd: "set-filetype", + run: func(window *Window, args ...string) { + if len(args) >= 1 { + input := args[0] + + if input == "" { + return + } + + if strings.ToLower(input) == "none" { + window.CurrentBuffer.filetype = "" + window.PrintMessage("Setting filetype to 'none'") + return + } else if _, ok := AvailableSyntaxes[input]; !ok { + window.PrintMessage(fmt.Sprintf("Could not set filetype to '%s'", input)) + return + } + + window.CurrentBuffer.filetype = input + window.PrintMessage(fmt.Sprintf("Setting filetype to '%s'", input)) + + return + } + + inputChannel := RequestInput(window, "Filetype to switch to:", "") + go func() { + input := <-inputChannel + + if input == "" { + return + } + + if _, ok := AvailableSyntaxes[input]; !ok { + window.PrintMessage(fmt.Sprintf("Could not set filetype to '%s'", input)) + return + } + + window.CurrentBuffer.filetype = input + window.PrintMessage(fmt.Sprintf("Setting filetype to '%s'", input)) + + }() + }, + } + menuFileCmd := Command{ cmd: "menu-file", run: func(window *Window, args ...string) { @@ -521,6 +566,7 @@ func initCommands() { commands["toggle-top-bar"] = &toggleTopBar commands["toggle-line-index"] = &toggleLineIndex commands["set-style"] = &setStyleCmd + commands["set-filetype"] = &setFiletypeCmd commands["menu-file"] = &menuFileCmd commands["menu-edit"] = &menuEditCmd commands["menu-buffers"] = &menuBuffersCmd diff --git a/src/highlighting.go b/src/highlighting.go new file mode 100644 index 0000000..1f35971 --- /dev/null +++ b/src/highlighting.go @@ -0,0 +1,109 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "regexp" + + "gopkg.in/yaml.v3" +) + +type SyntaxRule struct { + Type string `yaml:"type"` + Regex string `yaml:"regex"` + Multiline bool `yaml:"multiline"` +} + +type Syntax struct { + Filetype string `yaml:"filetype"` + Filenames string `yaml:"filenames"` + Rules []SyntaxRule `yaml:"rules"` +} + +type ParsedSyntax struct { + StartIndex int + EndIndex int + Type string +} + +var AvailableSyntaxes map[string]Syntax = make(map[string]Syntax) + +func ReadSyntaxHighlighters() { + // Get syntax directory path + syntaxDirPath := GetConfigPath("syntax") + + // Ensure directory exists at path + if stat, err := os.Stat(syntaxDirPath); syntaxDirPath == "" || err != nil || !stat.IsDir() { + return + } + + // Get directory entries + entries, err := os.ReadDir(syntaxDirPath) + if err != nil { + log.Fatalf("Could not read syntax directory: %s", err) + } + + // Read entries in directory + for _, entry := range entries { + entryPath := filepath.Join(syntaxDirPath, entry.Name()) + + data, err := os.ReadFile(entryPath) + if err != nil { + log.Fatalf("Could not read syntax file (%s): %s", entryPath, err) + } + + syntax := Syntax{} + err = yaml.Unmarshal(data, &syntax) + if err != nil { + log.Fatalf("Could not read syntax file (%s): %s", entryPath, err) + } + + if _, ok := AvailableSyntaxes[syntax.Filetype]; !ok { + AvailableSyntaxes[syntax.Filetype] = syntax + } + } +} + +func HighlightString(s string, filetype string) (parsedSyntaxes []ParsedSyntax, err error) { + // Get syntax for filetype + syntax, ok := AvailableSyntaxes[filetype] + if !ok { + return nil, nil + } + + for _, rule := range syntax.Rules { + + regex := rule.Regex + if rule.Multiline { + regex = "(?m)" + rule.Regex + } + r, err := regexp.Compile(regex) + if err != nil { + return nil, err + } + + matches := r.FindAllStringIndex(s, -1) + for _, match := range matches { + skip := false + for _, parsedSyntax := range parsedSyntaxes { + if (match[0] >= parsedSyntax.StartIndex && match[0] < parsedSyntax.EndIndex) || (match[1] >= parsedSyntax.StartIndex && match[1] < parsedSyntax.EndIndex) { + skip = true + } + } + if skip { + continue + } + + parsedSyntax := ParsedSyntax{ + StartIndex: match[0], + EndIndex: match[1], + Type: rule.Type, + } + + parsedSyntaxes = append(parsedSyntaxes, parsedSyntax) + } + } + + return parsedSyntaxes, nil +} diff --git a/src/main.go b/src/main.go index 8593f65..11b3429 100644 --- a/src/main.go +++ b/src/main.go @@ -23,6 +23,9 @@ func main() { // Read styles directory readStyles() + // Read syntax directory + ReadSyntaxHighlighters() + // Initialize commands initCommands() diff --git a/src/style.go b/src/style.go index 62274d0..03d1244 100644 --- a/src/style.go +++ b/src/style.go @@ -20,7 +20,7 @@ type TyperStyle struct { Description string StyleType string - // Colors + // Main Colors BufferAreaBg tcell.Color `name:"buffer_area_bg"` BufferAreaFg tcell.Color `name:"buffer_area_fg"` BufferAreaSel tcell.Color `name:"buffer_area_sel"` @@ -35,6 +35,14 @@ type TyperStyle struct { MessageBarFg tcell.Color `name:"message_bar_fg"` InputBarBg tcell.Color `name:"input_bar_bg"` InputBarFg tcell.Color `name:"input_bar_fg"` + + // Syntax highlighting + SyntaxComment tcell.Color `name:"syntax_comment"` + SyntaxKeyword tcell.Color `name:"syntax_keyword"` + SyntaxIdentifier tcell.Color `name:"syntax_identifier"` + SyntaxConstant tcell.Color `name:"syntax_constant"` + SyntaxVariable tcell.Color `name:"syntax_variable"` + SyntaxString tcell.Color `name:"syntax_string"` } type typerStyleYaml struct { diff --git a/src/top_menu.go b/src/top_menu.go index d980d27..a24fc5f 100644 --- a/src/top_menu.go +++ b/src/top_menu.go @@ -134,6 +134,7 @@ func drawTopMenu(window *Window) { func getBufferInfoMsg(window *Window) string { pathToFile := "Not set" filename := "Not set" + filetype := "Not set" if window.CurrentBuffer.filename != "" { pathToFile = window.CurrentBuffer.filename } @@ -141,6 +142,10 @@ func getBufferInfoMsg(window *Window) string { filename = filepath.Base(window.CurrentBuffer.filename) } + if window.CurrentBuffer.filetype != "" { + filetype = window.CurrentBuffer.filetype + } + contents := window.CurrentBuffer.GetContentsAsString() chars := len(contents) words := len(strings.Fields(string(contents))) @@ -150,6 +155,7 @@ func getBufferInfoMsg(window *Window) string { ret = strings.ReplaceAll(ret, "\n", " ") ret = strings.ReplaceAll(ret, "%F", pathToFile) ret = strings.ReplaceAll(ret, "%f", filename) + ret = strings.ReplaceAll(ret, "%t", filetype) ret = strings.ReplaceAll(ret, "%x", strconv.Itoa(window.CurrentBuffer.CursorPos.X+1)) ret = strings.ReplaceAll(ret, "%y", strconv.Itoa(window.CurrentBuffer.CursorPos.Y+1)) ret = strings.ReplaceAll(ret, "%p", strconv.Itoa(window.CurrentBuffer.PositionToAbsolutePosition(window.CurrentBuffer.CursorPos)+1))