1
0
mirror of https://github.com/alecthomas/chroma.git synced 2025-03-17 20:58:08 +02:00

Pager friendly terminal formatting (#1006)

Let's say a pager, like [moar](https://github.com/walles/moar) (uses
Chroma for syntax highlighting) or `less`, shows a line in the middle of
a file.

Unless that line starts with the correct formatting for the line, the
pager would have to scan the whole file from the start to get the
coloring of this single line right.

Before this PR, lines were not guaranteed to start with formatting, but
could sometimes rely on formatting from the preceding lines.

With this change in place, lines can now stand by themselves, and paging
will work better on Chroma's output.

Missing formatting at the start of the line would happen when a token
had a linefeed in the middle.
This commit is contained in:
Johan Walles 2024-10-02 23:20:59 +02:00 committed by GitHub
parent a56e228d32
commit 876fb612a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 29 deletions

View File

@ -1,7 +1,6 @@
package formatters
import (
"fmt"
"io"
"math"
@ -257,13 +256,7 @@ func (c *indexedTTYFormatter) Format(w io.Writer, style *chroma.Style, it chroma
}
}
if clr != "" {
fmt.Fprint(w, clr)
}
fmt.Fprint(w, token.Value)
if clr != "" {
fmt.Fprintf(w, "\033[0m")
}
writeToken(w, clr, token.Value)
}
return nil
}

View File

@ -3,6 +3,7 @@ package formatters
import (
"fmt"
"io"
"regexp"
"github.com/alecthomas/chroma/v2"
)
@ -10,33 +11,66 @@ import (
// TTY16m is a true-colour terminal formatter.
var TTY16m = Register("terminal16m", chroma.FormatterFunc(trueColourFormatter))
var crOrCrLf = regexp.MustCompile(`\r?\n`)
// Print the text with the given formatting, resetting the formatting at the end
// of each line and resuming it on the next line.
//
// This way, a pager (like https://github.com/walles/moar for example) can show
// any line in the output by itself, and it will get the right formatting.
func writeToken(w io.Writer, formatting string, text string) {
if formatting == "" {
fmt.Fprint(w, text)
return
}
newlineIndices := crOrCrLf.FindAllStringIndex(text, -1)
afterLastNewline := 0
for _, indices := range newlineIndices {
newlineStart, afterNewline := indices[0], indices[1]
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:newlineStart])
fmt.Fprint(w, "\033[0m")
fmt.Fprint(w, text[newlineStart:afterNewline])
afterLastNewline = afterNewline
}
if afterLastNewline < len(text) {
// Print whatever is left after the last newline
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:])
fmt.Fprint(w, "\033[0m")
}
}
func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
style = clearBackground(style)
for token := it(); token != chroma.EOF; token = it() {
entry := style.Get(token.Type)
if !entry.IsZero() {
out := ""
if entry.Bold == chroma.Yes {
out += "\033[1m"
}
if entry.Underline == chroma.Yes {
out += "\033[4m"
}
if entry.Italic == chroma.Yes {
out += "\033[3m"
}
if entry.Colour.IsSet() {
out += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
out += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}
fmt.Fprint(w, out)
if entry.IsZero() {
fmt.Fprint(w, token.Value)
continue
}
fmt.Fprint(w, token.Value)
if !entry.IsZero() {
fmt.Fprint(w, "\033[0m")
formatting := ""
if entry.Bold == chroma.Yes {
formatting += "\033[1m"
}
if entry.Underline == chroma.Yes {
formatting += "\033[4m"
}
if entry.Italic == chroma.Yes {
formatting += "\033[3m"
}
if entry.Colour.IsSet() {
formatting += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
formatting += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}
writeToken(w, formatting, token.Value)
}
return nil
}