forked from gitea/gitea
		
	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 <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>
This commit is contained in:
		
							parent
							
								
									044c754ea5
								
							
						
					
					
						commit
						eec1c71880
					
				| @ -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}} | ||||
|  | ||||
							
								
								
									
										28
									
								
								templates/repo/file_info.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								templates/repo/file_info.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 silverwind
						silverwind