Skip to content

Commit

Permalink
feat: implement syntax highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
claby2 committed Feb 6, 2021
1 parent 4eec0df commit c130705
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 20 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PROJECT = make-tui
BUILD_DIR ?= build
APP_SOURCES = parser.go \
APP_SOURCES = highlighter.go \
parser.go \
render.go \
search.go \
target.go \
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/claby2/make-tui
go 1.14

require (
github.com/alecthomas/chroma v0.8.2
github.com/gizak/termui/v3 v3.1.0
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 // indirect
Expand Down
33 changes: 33 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
Expand All @@ -12,3 +31,17 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyh
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
89 changes: 89 additions & 0 deletions highlighter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"log"
"math"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
)

var c = chroma.MustParseColour

// ColorMap associates termui color names with a respective chroma color
var ColorMap = map[string]chroma.Colour{
"red": c("#ff0000"),
"blue": c("#0000ff"),
"black": c("#000000"),
"cyan": c("#00ffff"),
"yellow": c("#ffff00"),
"white": c("#ffffff"),
"green": c("#00ff00"),
"magenta": c("#ff00ff"),
}

// Highlighter helps facilitate makefile syntax highlighting
type Highlighter struct {
style *chroma.Style
}

// NewHighlighter constructs a Highlighter and sets the style based on the given styleName
func NewHighlighter(styleName string) *Highlighter {
style := styles.Get(styleName)
if style == nil {
style = styles.Fallback
}
return &Highlighter{style}
}

// GetHighlightedContent iterates through the given content slice and returns it with inline style annotations
func (highlighter *Highlighter) GetHighlightedContent(content []string) []string {
lexer := lexers.Get("Base Makefile")

var highlightedContent []string
currentLine := ""
for _, line := range content {
iterator, err := lexer.Tokenise(nil, line)
if err != nil {
log.Fatal(err)
}
for token := iterator(); token != chroma.EOF; token = iterator() {
if token.Value == "\n" {
highlightedContent = append(highlightedContent, currentLine)
currentLine = ""
continue
}
entry := highlighter.style.Get(token.Type)
fg := "clear"
if entry.Colour.IsSet() {
fg = approximateColor(entry.Colour)
}
style := "fg:" + fg
if entry.Bold == chroma.Yes {
style += ",mod:bold"
} else if entry.Underline == chroma.Yes {
style += ",mod:underline"
}
currentLine += "[" + token.Value + "](" + style + ")"
}
if currentLine != "" {
highlightedContent = append(highlightedContent, currentLine)
currentLine = ""
}
}
return highlightedContent
}

func approximateColor(color chroma.Colour) string {
lowestDistance := math.MaxFloat64
var bestColor string
for colorName, c := range ColorMap {
distance := color.Distance(c)
if distance < lowestDistance {
lowestDistance = distance
bestColor = colorName
}
}
return bestColor
}
43 changes: 43 additions & 0 deletions highlighter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"testing"
)

func TestGetHighlightedContent(t *testing.T) {
content := []string{
"PROJECT = make-tui",
"BUILD_DIR ?= build",
"APP_SOURCES = parser.go \\",
" render.go \\",
" search.go \\",
" target.go \\",
" main.go \\",
"",
"build: $(APP_SOURCES)",
" go build -o $(BUILD_DIR)/make-tui $(APP_SOURCES)",
".PHONY: build",
"",
"run: $(APP_SOURCES)",
" go run $(APP_SOURCES)",
".PHONY: run",
"",
"test:",
" go test ./...",
".PHONY: test",
"",
"clean:",
" rm -rf $(BUILD_DIR)",
".PHONY: clean",
}
expected := len(content)
highlighter := NewHighlighter("emacs")
highlightedContent := highlighter.GetHighlightedContent(content)

result := len(highlightedContent)

if result != expected {
t.Errorf("expected highlighted content %d, received %d", expected, result)
}

}
2 changes: 1 addition & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (parsedContent *ParsedContent) Parse() {
// Handle multiline comments
if inMultilineComment {
inTarget = false
inMultilineComment = line[len(line)-1:] == "\\"
inMultilineComment = len(line) > 0 && line[len(line)-1:] == "\\"
// If currently in multiline comment, the entire line is commented
continue
} else {
Expand Down
23 changes: 12 additions & 11 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ func Render(content *ParsedContent) {
target := NewTarget(0, len(content.rules), content.rules)
target.Rows = getTargets(content.rules)

highlighter := NewHighlighter("vim")

dependencyWidget := widgets.NewParagraph()
dependencyWidget.Title = "Dependencies"
dependencyWidget.Text = getDependency(content.rules, target.Index)

contentWidget := widgets.NewParagraph()
contentWidget.Title = content.filePath
contentWidget.Text = getHighlightedContent(content.content, content.rules, termHeight, target.Index)
contentWidget.Text = getContent(content.content, highlighter, content.rules, termHeight, target.Index)

grid := ui.NewGrid()
grid.SetRect(0, 0, termWidth, termHeight)
Expand Down Expand Up @@ -113,13 +115,14 @@ func Render(content *ParsedContent) {
}
target.Index = target.SelectedRow
dependencyWidget.Text = getDependency(content.rules, target.Index)
contentWidget.Text = getHighlightedContent(content.content, content.rules, termHeight, target.Index)
contentWidget.Text = getContent(content.content, highlighter, content.rules, termHeight, target.Index)
ui.Render(grid)
}

ui.Close()
if run && target.Name != "" {
cmd := exec.Command("make", "-f"+content.filePath, target.Name)
targetName := target.GetName()
if run && targetName != "" {
cmd := exec.Command("make", "-f"+content.filePath, targetName)
stdout, _ := cmd.StdoutPipe()
Check(cmd.Start)

Expand Down Expand Up @@ -153,23 +156,21 @@ func getDependency(rules []Rule, index int) string {
return ""
}

func getHighlightedContent(content []string, rules []Rule, termHeight, index int) string {
func getContent(content []string, highlighter *Highlighter, rules []Rule, termHeight, index int) string {
contentCopy := append([]string(nil), content...)
highlightedContent := highlighter.GetHighlightedContent(contentCopy)
firstLine := 0
if index < len(rules) {
lineNumber := rules[index].lineNumber
numberOfCommands := len(rules[index].commands)

if len(contentCopy) > termHeight-1 {
if len(content) > termHeight-1 {
firstLine = lineNumber
}

// Highlight rule (including commands)
for i := lineNumber; i <= lineNumber+numberOfCommands; i++ {
contentCopy[i] = "[" + contentCopy[i] + "](fg:black,bg:white,mod:bold)"
}
highlightedContent[lineNumber] = "[" + content[lineNumber] + "](fg:black,bg:white,mod:bold)"
}
return strings.ReplaceAll(strings.Join(contentCopy[firstLine:], "\n"), "\t", strings.Repeat(" ", 4))
return strings.ReplaceAll(strings.Join(highlightedContent[firstLine:], "\n"), "\t", strings.Repeat(" ", 4))
}

func replaceTabs(content []string) []string {
Expand Down
2 changes: 1 addition & 1 deletion search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Search struct {
content string
}

// NewSearch constructs a Search and sets active to false and content to an empty strin`
// NewSearch constructs a Search and sets active to false and content to an empty string
func NewSearch() *Search {
return &Search{active: false, content: ""}
}
Expand Down
11 changes: 5 additions & 6 deletions target.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type Target struct {

Search *Search
Index int
Name string
targets []string
numberOfRules int
}
Expand All @@ -22,15 +21,10 @@ func NewTarget(index, numberOfRules int, rules []Rule) *Target {
for _, rule := range rules {
targets = append(targets, rule.target)
}
var name string
if len(targets) > 0 {
name = targets[0]
}
target := &Target{
List: widgets.NewList(),
Index: index,
numberOfRules: numberOfRules,
Name: name,
targets: targets,
}

Expand All @@ -56,6 +50,11 @@ func (target *Target) FindTarget(goalTargetName string) int {
return -1
}

// GetName returns the name of the currently selected target
func (target *Target) GetName() string {
return target.targets[target.Index]
}

// SetRect sets the rectangle for the target widget for rendering
func (target *Target) SetRect(x1, y1, x2, y2 int) {
target.List.SetRect(x1, y1, x2, y2)
Expand Down

0 comments on commit c130705

Please sign in to comment.