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>tags/v1.19.0-rc0
@@ -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" | |||
@@ -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 | |||
@@ -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 | |||
@@ -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), | |||
@@ -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()) | |||
@@ -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 | |||
@@ -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) | |||
} |
@@ -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("<>"), | |||
name: "empty.js", | |||
code: "", | |||
want: lines(""), | |||
lexerName: "JavaScript", | |||
}, | |||
{ | |||
name: "tags.py", | |||
code: "<>", | |||
want: lines(`<span class="o"><</span><span class="o">></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("<>"), | |||
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"><</span><span class="o">></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", | |||
@@ -54,6 +72,7 @@ func TestFile(t *testing.T) { | |||
\n | |||
`, | |||
), | |||
lexerName: "Python", | |||
}, | |||
{ | |||
name: "empty-line-with-space.py", | |||
@@ -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) | |||
}) | |||
} | |||
} |
@@ -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, | |||
@@ -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 | |||
} | |||
@@ -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])") |
@@ -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 | |||
@@ -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 { | |||
@@ -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) | |||
@@ -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 | |||
} |
@@ -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) |
@@ -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)} | |||
} | |||
@@ -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) |
@@ -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}} |
@@ -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> |
@@ -6,38 +6,16 @@ | |||
</div> | |||
</div> | |||
{{end}} | |||
<h4 class="file-header ui top attached header df ac sb"> | |||
<div class="file-header-left df ac pr-4"> | |||
<h4 class="file-header ui top attached header df ac sb fw"> | |||
<div class="file-header-left df ac py-3 pr-4"> | |||
{{if .ReadmeInList}} | |||
{{svg "octicon-book" 16 "mr-3"}} | |||
<strong>{{.FileName}}</strong> | |||
{{else}} | |||
<div class="file-info text grey normal mono"> | |||
{{if .FileIsSymlink}} | |||
<div class="file-info-entry"> | |||
{{.locale.Tr "repo.symbolic_link"}} | |||
</div> | |||
{{end}} | |||
{{if .NumLinesSet}} | |||
<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}} | |||
</div> | |||
{{template "repo/file_info" .}} | |||
{{end}} | |||
</div> | |||
<div class="file-header-right file-actions df ac"> | |||
<div class="file-header-right file-actions df ac fw"> | |||
{{if .HasSourceRenderedToggle}} | |||
<div class="ui compact icon buttons two-toggle-buttons"> | |||
<a href="{{$.Link}}?display=source" class="ui mini basic button tooltip {{if .IsDisplayingSource}}active{{end}}" data-content="{{.locale.Tr "repo.file_view_source"}}" data-position="bottom center">{{svg "octicon-code" 15}}</a> |