Skip to content

Commit eec1c71

Browse files
silverwinddelvhlafrikslunnyjolheiser
authored
Show syntax lexer name in file view/blame (#21814)
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 <[email protected]> Co-authored-by: Lauris BH <[email protected]> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: John Olheiser <[email protected]>
1 parent 044c754 commit eec1c71

File tree

11 files changed

+132
-72
lines changed

11 files changed

+132
-72
lines changed

modules/highlight/highlight.go

+25-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"code.gitea.io/gitea/modules/analyze"
1919
"code.gitea.io/gitea/modules/log"
2020
"code.gitea.io/gitea/modules/setting"
21+
"code.gitea.io/gitea/modules/util"
2122

2223
"github.com/alecthomas/chroma/v2"
2324
"github.com/alecthomas/chroma/v2/formatters/html"
@@ -56,18 +57,18 @@ func NewContext() {
5657
})
5758
}
5859

59-
// Code returns a HTML version of code string with chroma syntax highlighting classes
60-
func Code(fileName, language, code string) string {
60+
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
61+
func Code(fileName, language, code string) (string, string) {
6162
NewContext()
6263

6364
// diff view newline will be passed as empty, change to literal '\n' so it can be copied
6465
// preserve literal newline in blame view
6566
if code == "" || code == "\n" {
66-
return "\n"
67+
return "\n", ""
6768
}
6869

6970
if len(code) > sizeLimit {
70-
return code
71+
return code, ""
7172
}
7273

7374
var lexer chroma.Lexer
@@ -103,7 +104,10 @@ func Code(fileName, language, code string) string {
103104
}
104105
cache.Add(fileName, lexer)
105106
}
106-
return CodeFromLexer(lexer, code)
107+
108+
lexerName := formatLexerName(lexer.Config().Name)
109+
110+
return CodeFromLexer(lexer, code), lexerName
107111
}
108112

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

137-
// File returns a slice of chroma syntax highlighted HTML lines of code
138-
func File(fileName, language string, code []byte) ([]string, error) {
141+
// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
142+
func File(fileName, language string, code []byte) ([]string, string, error) {
139143
NewContext()
140144

141145
if len(code) > sizeLimit {
142-
return PlainText(code), nil
146+
return PlainText(code), "", nil
143147
}
144148

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

179+
lexerName := formatLexerName(lexer.Config().Name)
180+
175181
iterator, err := lexer.Tokenise(nil, string(code))
176182
if err != nil {
177-
return nil, fmt.Errorf("can't tokenize code: %w", err)
183+
return nil, "", fmt.Errorf("can't tokenize code: %w", err)
178184
}
179185

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

194-
return lines, nil
200+
return lines, lexerName, nil
195201
}
196202

197203
// PlainText returns non-highlighted HTML for code
@@ -212,3 +218,11 @@ func PlainText(code []byte) []string {
212218
}
213219
return m
214220
}
221+
222+
func formatLexerName(name string) string {
223+
if name == "fallback" {
224+
return "Plaintext"
225+
}
226+
227+
return util.ToTitleCaseNoLower(name)
228+
}

modules/highlight/highlight_test.go

+40-19
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,52 @@ func lines(s string) []string {
1717

1818
func TestFile(t *testing.T) {
1919
tests := []struct {
20-
name string
21-
code string
22-
want []string
20+
name string
21+
code string
22+
want []string
23+
lexerName string
2324
}{
2425
{
25-
name: "empty.py",
26-
code: "",
27-
want: lines(""),
26+
name: "empty.py",
27+
code: "",
28+
want: lines(""),
29+
lexerName: "Python",
2830
},
2931
{
30-
name: "tags.txt",
31-
code: "<>",
32-
want: lines("&lt;&gt;"),
32+
name: "empty.js",
33+
code: "",
34+
want: lines(""),
35+
lexerName: "JavaScript",
3336
},
3437
{
35-
name: "tags.py",
36-
code: "<>",
37-
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
38+
name: "empty.yaml",
39+
code: "",
40+
want: lines(""),
41+
lexerName: "YAML",
3842
},
3943
{
40-
name: "eol-no.py",
41-
code: "a=1",
42-
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>`),
44+
name: "tags.txt",
45+
code: "<>",
46+
want: lines("&lt;&gt;"),
47+
lexerName: "Plaintext",
4348
},
4449
{
45-
name: "eol-newline1.py",
46-
code: "a=1\n",
47-
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n`),
50+
name: "tags.py",
51+
code: "<>",
52+
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
53+
lexerName: "Python",
54+
},
55+
{
56+
name: "eol-no.py",
57+
code: "a=1",
58+
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>`),
59+
lexerName: "Python",
60+
},
61+
{
62+
name: "eol-newline1.py",
63+
code: "a=1\n",
64+
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>\n`),
65+
lexerName: "Python",
4866
},
4967
{
5068
name: "eol-newline2.py",
@@ -54,6 +72,7 @@ func TestFile(t *testing.T) {
5472
\n
5573
`,
5674
),
75+
lexerName: "Python",
5776
},
5877
{
5978
name: "empty-line-with-space.py",
@@ -73,17 +92,19 @@ c=2
7392
\n
7493
<span class="n">c</span><span class="o">=</span><span class="mi">2</span>`,
7594
),
95+
lexerName: "Python",
7696
},
7797
}
7898

7999
for _, tt := range tests {
80100
t.Run(tt.name, func(t *testing.T) {
81-
out, err := File(tt.name, "", []byte(tt.code))
101+
out, lexerName, err := File(tt.name, "", []byte(tt.code))
82102
assert.NoError(t, err)
83103
expected := strings.Join(tt.want, "\n")
84104
actual := strings.Join(out, "\n")
85105
assert.Equal(t, strings.Count(actual, "<span"), strings.Count(actual, "</span>"))
86106
assert.EqualValues(t, expected, actual)
107+
assert.Equal(t, tt.lexerName, lexerName)
87108
})
88109
}
89110
}

modules/indexer/code/search.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
9494
lineNumbers[i] = startLineNum + i
9595
index += len(line)
9696
}
97+
98+
highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
99+
97100
return &Result{
98101
RepoID: result.RepoID,
99102
Filename: result.Filename,
@@ -102,7 +105,7 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
102105
Language: result.Language,
103106
Color: result.Color,
104107
LineNumbers: lineNumbers,
105-
FormattedLines: highlight.Code(result.Filename, "", formattedLinesBuffer.String()),
108+
FormattedLines: highlighted,
106109
}, nil
107110
}
108111

modules/util/util.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,21 @@ func ToUpperASCII(s string) string {
186186
return string(b)
187187
}
188188

189-
var titleCaser = cases.Title(language.English)
189+
var (
190+
titleCaser = cases.Title(language.English)
191+
titleCaserNoLower = cases.Title(language.English, cases.NoLower)
192+
)
190193

191194
// ToTitleCase returns s with all english words capitalized
192195
func ToTitleCase(s string) string {
193196
return titleCaser.String(s)
194197
}
195198

199+
// ToTitleCaseNoLower returns s with all english words capitalized without lowercasing
200+
func ToTitleCaseNoLower(s string) string {
201+
return titleCaserNoLower.String(s)
202+
}
203+
196204
var (
197205
whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$")
198206
leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])")

routers/web/repo/blame.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ func RefBlame(ctx *context.Context) {
100100
ctx.Data["FileName"] = blob.Name()
101101

102102
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
103+
ctx.Data["NumLinesSet"] = true
104+
103105
if err != nil {
104106
ctx.NotFound("GetBlobLineCount", err)
105107
return
@@ -237,6 +239,8 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
237239
rows := make([]*blameRow, 0)
238240
escapeStatus := &charset.EscapeStatus{}
239241

242+
var lexerName string
243+
240244
i := 0
241245
commitCnt := 0
242246
for _, part := range blameParts {
@@ -278,7 +282,13 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
278282
line += "\n"
279283
}
280284
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
281-
line = highlight.Code(fileName, language, line)
285+
line, lexerNameForLine := highlight.Code(fileName, language, line)
286+
287+
// set lexer name to the first detected lexer. this is certainly suboptimal and
288+
// we should instead highlight the whole file at once
289+
if lexerName == "" {
290+
lexerName = lexerNameForLine
291+
}
282292

283293
br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
284294
br.Code = gotemplate.HTML(line)
@@ -290,4 +300,5 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
290300
ctx.Data["EscapeStatus"] = escapeStatus
291301
ctx.Data["BlameRows"] = rows
292302
ctx.Data["CommitCnt"] = commitCnt
303+
ctx.Data["LexerName"] = lexerName
293304
}

routers/web/repo/view.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
568568
language = ""
569569
}
570570
}
571-
fileContent, err := highlight.File(blob.Name(), language, buf)
571+
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
572+
ctx.Data["LexerName"] = lexerName
572573
if err != nil {
573574
log.Error("highlight.File failed, fallback to plain text: %v", err)
574575
fileContent = highlight.PlainText(buf)

services/gitdiff/gitdiff.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) Dif
280280

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

services/gitdiff/highlightdiff.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB
9191
hcd.collectUsedRunes(codeA)
9292
hcd.collectUsedRunes(codeB)
9393

94-
highlightCodeA := highlight.Code(filename, language, codeA)
95-
highlightCodeB := highlight.Code(filename, language, codeB)
94+
highlightCodeA, _ := highlight.Code(filename, language, codeA)
95+
highlightCodeB, _ := highlight.Code(filename, language, codeB)
9696

9797
highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
9898
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)

templates/repo/blame.tmpl

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
<div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
2-
<h4 class="file-header ui top attached header df ac sb">
3-
<div class="file-header-left df ac">
4-
<div class="file-info text grey normal mono">
5-
<div class="file-info-entry">
6-
{{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}}
7-
</div>
8-
<div class="file-info-entry">{{FileSize .FileSize}}</div>
9-
</div>
2+
<h4 class="file-header ui top attached header df ac sb fw">
3+
<div class="file-header-left df ac py-3 pr-4">
4+
{{template "repo/file_info" .}}
105
</div>
11-
<div class="file-header-right file-actions df ac">
6+
<div class="file-header-right file-actions df ac fw">
127
<div class="ui buttons">
138
<a class="ui tiny button" href="{{$.RawFileLink}}">{{.locale.Tr "repo.file_raw"}}</a>
149
{{if not .IsViewCommit}}

templates/repo/file_info.tmpl

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<div class="file-info text grey normal mono">
2+
{{if .FileIsSymlink}}
3+
<div class="file-info-entry">
4+
{{.locale.Tr "repo.symbolic_link"}}
5+
</div>
6+
{{end}}
7+
{{if .NumLinesSet}}{{/* Explicit attribute needed to show 0 line changes */}}
8+
<div class="file-info-entry">
9+
{{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}}
10+
</div>
11+
{{end}}
12+
{{if .FileSize}}
13+
<div class="file-info-entry">
14+
{{FileSize .FileSize}}{{if .IsLFSFile}} ({{.locale.Tr "repo.stored_lfs"}}){{end}}
15+
</div>
16+
{{end}}
17+
{{if .LFSLock}}
18+
<div class="file-info-entry ui tooltip" data-content="{{.LFSLockHint}}">
19+
{{svg "octicon-lock" 16 "mr-2"}}
20+
<a href="{{.LFSLockOwnerHomeLink}}">{{.LFSLockOwner}}</a>
21+
</div>
22+
{{end}}
23+
{{if .LexerName}}
24+
<div class="file-info-entry">
25+
{{.LexerName}}
26+
</div>
27+
{{end}}
28+
</div>

0 commit comments

Comments
 (0)