From eec1c718806797b21ba5f6c1ceddf711e9d4801a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 19 Nov 2022 12:08:06 +0100 Subject: [PATCH] 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: Screenshot 2022-11-14 at 22 31 16 Screenshot 2022-11-14 at 22 36 06 Screenshot 2022-11-14 at 22 36 26 Also, I improved the way this header overflows on small screens: Screenshot 2022-11-14 at 22 44 36 Co-authored-by: delvh Co-authored-by: Lauris BH Co-authored-by: Lunny Xiao Co-authored-by: John Olheiser --- modules/highlight/highlight.go | 36 ++++++++++++------ modules/highlight/highlight_test.go | 59 +++++++++++++++++++---------- modules/indexer/code/search.go | 5 ++- modules/util/util.go | 10 ++++- routers/web/repo/blame.go | 13 ++++++- routers/web/repo/view.go | 3 +- services/gitdiff/gitdiff.go | 3 +- services/gitdiff/highlightdiff.go | 4 +- templates/repo/blame.tmpl | 13 ++----- templates/repo/file_info.tmpl | 28 ++++++++++++++ templates/repo/view_file.tmpl | 30 ++------------- 11 files changed, 132 insertions(+), 72 deletions(-) create mode 100644 templates/repo/file_info.tmpl diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index 65ed74b0199..3836c058808 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -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) +} diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index 8f83f4a2f61..18997537461 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -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(`<>`), + name: "empty.yaml", + code: "", + want: lines(""), + lexerName: "YAML", }, { - name: "eol-no.py", - code: "a=1", - want: lines(`a=1`), + name: "tags.txt", + code: "<>", + want: lines("<>"), + lexerName: "Plaintext", }, { - name: "eol-newline1.py", - code: "a=1\n", - want: lines(`a=1\n`), + name: "tags.py", + code: "<>", + want: lines(`<>`), + lexerName: "Python", + }, + { + name: "eol-no.py", + code: "a=1", + want: lines(`a=1`), + lexerName: "Python", + }, + { + name: "eol-newline1.py", + code: "a=1\n", + want: lines(`a=1\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 c=2`, ), + 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, "")) assert.EqualValues(t, expected, actual) + assert.Equal(t, tt.lexerName, lexerName) }) } } diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index bb7715bafcd..df255fa8758 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -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 } diff --git a/modules/util/util.go b/modules/util/util.go index be60fe4b4bb..6df47ca5682 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -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])") diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 64a6f0ec538..52d5c3c6e62 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -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 } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e7aca048192..7500dbb34bb 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -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) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 3c8c5c81a56..d24c0ac0826 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -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)} } diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go index 4ceada4d7ec..727827232d6 100644 --- a/services/gitdiff/highlightdiff.go +++ b/services/gitdiff/highlightdiff.go @@ -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) diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index b697573d24e..e4a10ee57dd 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -1,14 +1,9 @@
-

-
-
-
- {{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}} -
-
{{FileSize .FileSize}}
-
+

+
+ {{template "repo/file_info" .}}
-
+
{{.locale.Tr "repo.file_raw"}} {{if not .IsViewCommit}} diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl new file mode 100644 index 00000000000..90a831fb8e8 --- /dev/null +++ b/templates/repo/file_info.tmpl @@ -0,0 +1,28 @@ +
+ {{if .FileIsSymlink}} +
+ {{.locale.Tr "repo.symbolic_link"}} +
+ {{end}} + {{if .NumLinesSet}}{{/* Explicit attribute needed to show 0 line changes */}} +
+ {{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}} +
+ {{end}} + {{if .FileSize}} +
+ {{FileSize .FileSize}}{{if .IsLFSFile}} ({{.locale.Tr "repo.stored_lfs"}}){{end}} +
+ {{end}} + {{if .LFSLock}} +
+ {{svg "octicon-lock" 16 "mr-2"}} + {{.LFSLockOwner}} +
+ {{end}} + {{if .LexerName}} +
+ {{.LexerName}} +
+ {{end}} +
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 60d2a812def..321600a9975 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -6,38 +6,16 @@
{{end}} -

-
+

+
{{if .ReadmeInList}} {{svg "octicon-book" 16 "mr-3"}} {{.FileName}} {{else}} -
- {{if .FileIsSymlink}} -
- {{.locale.Tr "repo.symbolic_link"}} -
- {{end}} - {{if .NumLinesSet}} -
- {{.NumLines}} {{.locale.TrN .NumLines "repo.line" "repo.lines"}} -
- {{end}} - {{if .FileSize}} -
- {{FileSize .FileSize}}{{if .IsLFSFile}} ({{.locale.Tr "repo.stored_lfs"}}){{end}} -
- {{end}} - {{if .LFSLock}} -
- {{svg "octicon-lock" 16 "mr-2"}} - {{.LFSLockOwner}} -
- {{end}} -
+ {{template "repo/file_info" .}} {{end}}
-
+
{{if .HasSourceRenderedToggle}}