Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show syntax lexer name in file view/blame #21814

Merged
merged 16 commits into from
Nov 19, 2022
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.ToTitleCase(name)
}
47 changes: 31 additions & 16 deletions modules/highlight/highlight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,43 @@ func TestFile(t *testing.T) {
name string
code string
want []string
lexer string
}{
{
name: "empty.py",
code: "",
want: lines(""),
name: "empty.py",
code: "",
want: lines(""),
lexer: "Python",
},
{
name: "tags.txt",
code: "<>",
want: lines("&lt;&gt;"),
name: "empty.js",
code: "",
want: lines(""),
lexer: "Javascript",
},
{
name: "tags.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
name: "tags.txt",
code: "<>",
want: lines("&lt;&gt;"),
lexer: "Plaintext",
},
{
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.py",
code: "<>",
want: lines(`<span class="o">&lt;</span><span class="o">&gt;</span>`),
lexer: "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`),
name: "eol-no.py",
code: "a=1",
want: lines(`<span class="n">a</span><span class="o">=</span><span class="mi">1</span>`),
lexer: "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`),
lexer: "Python",
},
{
name: "eol-newline2.py",
Expand All @@ -54,6 +66,7 @@ func TestFile(t *testing.T) {
\n
`,
),
lexer: "Python",
},
{
name: "empty-line-with-space.py",
Expand All @@ -73,17 +86,19 @@ c=2
\n
<span class="n">c</span><span class="o">=</span><span class="mi">2</span>`,
),
lexer: "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.lexer, 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
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,7 @@ 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)
if err != nil {
log.Error("highlight.File failed, fallback to plain text: %v", err)
fileContent = highlight.PlainText(buf)
Expand All @@ -581,6 +581,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
}
ctx.Data["EscapeStatus"] = status
ctx.Data["FileContent"] = fileContent
ctx.Data["LexerName"] = lexerName
ctx.Data["LineEscapeStatus"] = statuses
}
if !isLFSFile {
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" .}}
silverwind marked this conversation as resolved.
Show resolved Hide resolved
</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>
30 changes: 4 additions & 26 deletions templates/repo/view_file.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down