Skip to content

Commit

Permalink
Show syntax lexer name in file view/blame (go-gitea#21814)
Browse files Browse the repository at this point in the history
Show which Chroma Lexer is used to highlight the file in the file
header. It's useful for development to see what was detected, and I
think it's not bad info to have for the user:

<img width="233" alt="Screenshot 2022-11-14 at 22 31 16"
src="https://user-images.githubusercontent.com/115237/201770854-44933dfc-70a4-487c-8457-1bb3cc43ea62.png">
<img width="226" alt="Screenshot 2022-11-14 at 22 36 06"
src="https://user-images.githubusercontent.com/115237/201770856-9260ce6f-6c0f-442c-92b5-201e5b113188.png">
<img width="194" alt="Screenshot 2022-11-14 at 22 36 26"
src="https://user-images.githubusercontent.com/115237/201770857-6f56591b-80ea-42cc-8ea5-21b9156c018b.png">

Also, I improved the way this header overflows on small screens:

<img width="354" alt="Screenshot 2022-11-14 at 22 44 36"
src="https://user-images.githubusercontent.com/115237/201774828-2ddbcde1-da15-403f-bf7a-6248449fa2c5.png">

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
  • Loading branch information
5 people authored and fsologureng committed Nov 22, 2022
1 parent 82110fe commit 1b2470c
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 72 deletions.
36 changes: 25 additions & 11 deletions modules/highlight/highlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"

"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
Expand Down Expand Up @@ -56,18 +57,18 @@ func NewContext() {
})
}

// Code returns a HTML version of code string with chroma syntax highlighting classes
func Code(fileName, language, code string) string {
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
func Code(fileName, language, code string) (string, string) {
NewContext()

// diff view newline will be passed as empty, change to literal '\n' so it can be copied
// preserve literal newline in blame view
if code == "" || code == "\n" {
return "\n"
return "\n", ""
}

if len(code) > sizeLimit {
return code
return code, ""
}

var lexer chroma.Lexer
Expand Down Expand Up @@ -103,7 +104,10 @@ func Code(fileName, language, code string) string {
}
cache.Add(fileName, lexer)
}
return CodeFromLexer(lexer, code)

lexerName := formatLexerName(lexer.Config().Name)

return CodeFromLexer(lexer, code), lexerName
}

// CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes
Expand Down Expand Up @@ -134,12 +138,12 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
return strings.TrimSuffix(htmlbuf.String(), "\n")
}

// File returns a slice of chroma syntax highlighted HTML lines of code
func File(fileName, language string, code []byte) ([]string, error) {
// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
func File(fileName, language string, code []byte) ([]string, string, error) {
NewContext()

if len(code) > sizeLimit {
return PlainText(code), nil
return PlainText(code), "", nil
}

formatter := html.New(html.WithClasses(true),
Expand Down Expand Up @@ -172,9 +176,11 @@ func File(fileName, language string, code []byte) ([]string, error) {
}
}

lexerName := formatLexerName(lexer.Config().Name)

iterator, err := lexer.Tokenise(nil, string(code))
if err != nil {
return nil, fmt.Errorf("can't tokenize code: %w", err)
return nil, "", fmt.Errorf("can't tokenize code: %w", err)
}

tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
Expand All @@ -185,13 +191,13 @@ func File(fileName, language string, code []byte) ([]string, error) {
iterator = chroma.Literator(tokens...)
err = formatter.Format(htmlBuf, styles.GitHub, iterator)
if err != nil {
return nil, fmt.Errorf("can't format code: %w", err)
return nil, "", fmt.Errorf("can't format code: %w", err)
}
lines = append(lines, htmlBuf.String())
htmlBuf.Reset()
}

return lines, nil
return lines, lexerName, nil
}

// PlainText returns non-highlighted HTML for code
Expand All @@ -212,3 +218,11 @@ func PlainText(code []byte) []string {
}
return m
}

func formatLexerName(name string) string {
if name == "fallback" {
return "Plaintext"
}

return util.ToTitleCaseNoLower(name)
}
59 changes: 40 additions & 19 deletions modules/highlight/highlight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,52 @@ func lines(s string) []string {

func TestFile(t *testing.T) {
tests := []struct {
name string
code string
want []string
name string
code string
want []string
lexerName string
}{
{
name: "empty.py",
code: "",
want: lines(""),
name: "empty.py",
code: "",
want: lines(""),
lexerName: "Python",
},
{
name: "tags.txt",
code: "<>",
want: lines("&lt;&gt;"),
name: "empty.js",
code: "",
want: lines(""),
lexerName: "JavaScript",
},
{
name: "tags.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
name: "empty.yaml",
code: "",
want: lines(""),
lexerName: "YAML",
},
{
name: "eol-no.py",
code: "a=1",
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>`),
name: "tags.txt",
code: "<>",
want: lines("&lt;&gt;"),
lexerName: "Plaintext",
},
{
name: "eol-newline1.py",
code: "a=1\n",
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n`),
name: "tags.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
lexerName: "Python",
},
{
name: "eol-no.py",
code: "a=1",
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>`),
lexerName: "Python",
},
{
name: "eol-newline1.py",
code: "a=1\n",
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n`),
lexerName: "Python",
},
{
name: "eol-newline2.py",
Expand All @@ -54,6 +72,7 @@ func TestFile(t *testing.T) {
\n
`,
),
lexerName: "Python",
},
{
name: "empty-line-with-space.py",
Expand All @@ -73,17 +92,19 @@ c=2
\n
<span class="n">c</span><span class="o">=</span><span class="mi">2</span>`,
),
lexerName: "Python",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out, err := File(tt.name, "", []byte(tt.code))
out, lexerName, err := File(tt.name, "", []byte(tt.code))
assert.NoError(t, err)
expected := strings.Join(tt.want, "\n")
actual := strings.Join(out, "\n")
assert.Equal(t, strings.Count(actual, "<span"), strings.Count(actual, "</span>"))
assert.EqualValues(t, expected, actual)
assert.Equal(t, tt.lexerName, lexerName)
})
}
}
Expand Down
5 changes: 4 additions & 1 deletion modules/indexer/code/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
lineNumbers[i] = startLineNum + i
index += len(line)
}

highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())

return &Result{
RepoID: result.RepoID,
Filename: result.Filename,
Expand All @@ -102,7 +105,7 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
Language: result.Language,
Color: result.Color,
LineNumbers: lineNumbers,
FormattedLines: highlight.Code(result.Filename, "", formattedLinesBuffer.String()),
FormattedLines: highlighted,
}, nil
}

Expand Down
10 changes: 9 additions & 1 deletion modules/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,21 @@ func ToUpperASCII(s string) string {
return string(b)
}

var titleCaser = cases.Title(language.English)
var (
titleCaser = cases.Title(language.English)
titleCaserNoLower = cases.Title(language.English, cases.NoLower)
)

// ToTitleCase returns s with all english words capitalized
func ToTitleCase(s string) string {
return titleCaser.String(s)
}

// ToTitleCaseNoLower returns s with all english words capitalized without lowercasing
func ToTitleCaseNoLower(s string) string {
return titleCaserNoLower.String(s)
}

var (
whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$")
leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])")
Expand Down
13 changes: 12 additions & 1 deletion routers/web/repo/blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ func RefBlame(ctx *context.Context) {
ctx.Data["FileName"] = blob.Name()

ctx.Data["NumLines"], err = blob.GetBlobLineCount()
ctx.Data["NumLinesSet"] = true

if err != nil {
ctx.NotFound("GetBlobLineCount", err)
return
Expand Down Expand Up @@ -237,6 +239,8 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
rows := make([]*blameRow, 0)
escapeStatus := &charset.EscapeStatus{}

var lexerName string

i := 0
commitCnt := 0
for _, part := range blameParts {
Expand Down Expand Up @@ -278,7 +282,13 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
line += "\n"
}
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
line = highlight.Code(fileName, language, line)
line, lexerNameForLine := highlight.Code(fileName, language, line)

// set lexer name to the first detected lexer. this is certainly suboptimal and
// we should instead highlight the whole file at once
if lexerName == "" {
lexerName = lexerNameForLine
}

br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
br.Code = gotemplate.HTML(line)
Expand All @@ -290,4 +300,5 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
ctx.Data["EscapeStatus"] = escapeStatus
ctx.Data["BlameRows"] = rows
ctx.Data["CommitCnt"] = commitCnt
ctx.Data["LexerName"] = lexerName
}
3 changes: 2 additions & 1 deletion routers/web/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
language = ""
}
}
fileContent, err := highlight.File(blob.Name(), language, buf)
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
ctx.Data["LexerName"] = lexerName
if err != nil {
log.Error("highlight.File failed, fallback to plain text: %v", err)
fileContent = highlight.PlainText(buf)
Expand Down
3 changes: 2 additions & 1 deletion services/gitdiff/gitdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) Dif

// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
status, content := charset.EscapeControlHTML(highlight.Code(fileName, language, code), locale)
highlighted, _ := highlight.Code(fileName, language, code)
status, content := charset.EscapeControlHTML(highlighted, locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}

Expand Down
4 changes: 2 additions & 2 deletions services/gitdiff/highlightdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB
hcd.collectUsedRunes(codeA)
hcd.collectUsedRunes(codeB)

highlightCodeA := highlight.Code(filename, language, codeA)
highlightCodeB := highlight.Code(filename, language, codeB)
highlightCodeA, _ := highlight.Code(filename, language, codeA)
highlightCodeB, _ := highlight.Code(filename, language, codeB)

highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
Expand Down
13 changes: 4 additions & 9 deletions templates/repo/blame.tmpl
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
<div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
<h4 class="file-header ui top attached header df ac sb">
<div class="file-header-left df ac">
<div class="file-info text grey normal mono">
<div class="file-info-entry">
{{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}}
</div>
<div class="file-info-entry">{{FileSize .FileSize}}</div>
</div>
<h4 class="file-header ui top attached header df ac sb fw">
<div class="file-header-left df ac py-3 pr-4">
{{template "repo/file_info" .}}
</div>
<div class="file-header-right file-actions df ac">
<div class="file-header-right file-actions df ac fw">
<div class="ui buttons">
<a class="ui tiny button" href="{{$.RawFileLink}}">{{.locale.Tr "repo.file_raw"}}</a>
{{if not .IsViewCommit}}
Expand Down
28 changes: 28 additions & 0 deletions templates/repo/file_info.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="file-info text grey normal mono">
{{if .FileIsSymlink}}
<div class="file-info-entry">
{{.locale.Tr "repo.symbolic_link"}}
</div>
{{end}}
{{if .NumLinesSet}}{{/* Explicit attribute needed to show 0 line changes */}}
<div class="file-info-entry">
{{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}}
</div>
{{end}}
{{if .FileSize}}
<div class="file-info-entry">
{{FileSize .FileSize}}{{if .IsLFSFile}} ({{.locale.Tr "repo.stored_lfs"}}){{end}}
</div>
{{end}}
{{if .LFSLock}}
<div class="file-info-entry ui tooltip" data-content="{{.LFSLockHint}}">
{{svg "octicon-lock" 16 "mr-2"}}
<a href="{{.LFSLockOwnerHomeLink}}">{{.LFSLockOwner}}</a>
</div>
{{end}}
{{if .LexerName}}
<div class="file-info-entry">
{{.LexerName}}
</div>
{{end}}
</div>
Loading

0 comments on commit 1b2470c

Please sign in to comment.