1
0
mirror of https://github.com/go-task/task.git synced 2025-04-19 23:02:17 +03:00
task/taskfile/snippet.go
Pete Davison ffeb3bcc3f
refactor: executor functional options (#2085)
* refactor: executor functional options

* refactor: minor tidy up of list code

* fix: WithVersionCheck missing from call to NewExecutor

* feat: docstrings for structs with functional options

* refactor: prefix the functional options with the name of the struct they belong to
2025-03-10 20:38:25 +00:00

168 lines
4.6 KiB
Go

package taskfile
import (
"bytes"
"embed"
"fmt"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color"
)
//go:embed themes/*.xml
var embedded embed.FS
const (
lineIndicator = ">"
columnIndicator = "^"
)
func init() {
r, err := embedded.Open("themes/task.xml")
if err != nil {
panic(err)
}
style, err := chroma.NewXMLStyle(r)
if err != nil {
panic(err)
}
styles.Register(style)
}
type (
// SnippetOption is a function that configures a [Snippet].
SnippetOption func(*Snippet)
// A Snippet is a syntax highlighted snippet of a Taskfile with optional
// padding and a line and column indicator.
Snippet struct {
linesRaw []string
linesHighlighted []string
start int
end int
line int
column int
padding int
noIndicators bool
}
)
// NewSnippet creates a new [Snippet] from a byte slice and a line and column
// number. The line and column numbers should be 1-indexed. For example, the
// first character in the file would be 1:1 (line 1, column 1). The padding
// determines the number of lines to include before and after the chosen line.
func NewSnippet(b []byte, opts ...SnippetOption) *Snippet {
snippet := &Snippet{}
for _, opt := range opts {
opt(snippet)
}
// Syntax highlight the input and split it into lines
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.Write(b)
}
linesRaw := strings.Split(string(b), "\n")
linesHighlighted := strings.Split(buf.String(), "\n")
// Work out the start and end lines of the snippet
snippet.start = max(snippet.line-snippet.padding, 1)
snippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1)
snippet.linesRaw = linesRaw[snippet.start-1 : snippet.end]
snippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end]
return snippet
}
// Options loops through the given [SnippetOption] functions and applies them
// to the [Snippet].
func (s *Snippet) Options(opts ...SnippetOption) {
for _, opt := range opts {
opt(s)
}
}
// SnippetWithLine specifies the line number that the [Snippet] should center
// around and point to.
func SnippetWithLine(line int) SnippetOption {
return func(snippet *Snippet) {
snippet.line = line
}
}
// SnippetWithColumn specifies the column number that the [Snippet] should
// point to.
func SnippetWithColumn(column int) SnippetOption {
return func(snippet *Snippet) {
snippet.column = column
}
}
// SnippetWithPadding specifies the number of lines to include before and after
// the selected line in the [Snippet].
func SnippetWithPadding(padding int) SnippetOption {
return func(snippet *Snippet) {
snippet.padding = padding
}
}
// SnippetWithNoIndicators specifies that the [Snippet] should not include line
// or column indicators.
func SnippetWithNoIndicators() SnippetOption {
return func(snippet *Snippet) {
snippet.noIndicators = true
}
}
func (s *Snippet) String() string {
buf := &bytes.Buffer{}
maxLineNumberDigits := digits(s.end)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator))
columnSpacer := strings.Repeat(" ", max(s.column-1, 0))
// Loop over each line in the snippet
for i, lineHighlighted := range s.linesHighlighted {
if i > 0 {
fmt.Fprintln(buf)
}
currentLine := s.start + i
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
// If this is a padding line or indicators are disabled, print it as normal
if currentLine != s.line || s.noIndicators {
fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, lineHighlighted)
continue
}
// Otherwise, print the line with indicators
fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, lineHighlighted)
// Only print the column indicator if the column is in bounds
if s.column > 0 && s.column <= len(s.linesRaw[i]) {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
}
// If there are lines, but no line is selected, print the column indicator under all the lines
if len(s.linesHighlighted) > 0 && s.line == 0 && s.column > 0 {
fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))
}
return buf.String()
}
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}