diff --git a/modules/charset/escape_status.go b/modules/charset/escape_status.go index fb9ebbb228..06625b29c2 100644 --- a/modules/charset/escape_status.go +++ b/modules/charset/escape_status.go @@ -10,14 +10,8 @@ type EscapeStatus struct { HasAmbiguous bool } -// Or combines two EscapeStatus structs into one representing the conjunction of the two -func (status *EscapeStatus) Or(other *EscapeStatus) *EscapeStatus { - st := status - if status == nil { - st = &EscapeStatus{} - } +func (st *EscapeStatus) Combine(other *EscapeStatus) { st.Escaped = st.Escaped || other.Escaped st.HasAmbiguous = st.HasAmbiguous || other.HasAmbiguous st.HasInvisible = st.HasInvisible || other.HasInvisible - return st } diff --git a/modules/highlight/benchmark_test.go b/modules/highlight/benchmark_test.go new file mode 100644 index 0000000000..17be819201 --- /dev/null +++ b/modules/highlight/benchmark_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package highlight + +import ( + "strings" + "testing" + + "github.com/alecthomas/chroma/v2/lexers" +) + +func BenchmarkDetectChromaLexerByFileName(b *testing.B) { + for b.Loop() { + // BenchmarkDetectChromaLexerByFileName-12 18214717 61.35 ns/op + DetectChromaLexerByFileName("a.sql", "") + } +} + +func BenchmarkDetectChromaLexerWithAnalyze(b *testing.B) { + code := []byte(strings.Repeat("SELECT * FROM table;\n", 1000)) + b.ResetTimer() + for b.Loop() { + // BenchmarkRenderCodeSlowGuess-12 87946 13310 ns/op + detectChromaLexerWithAnalyze("a", "", code) + } +} + +func BenchmarkChromaAnalyze(b *testing.B) { + code := strings.Repeat("SELECT * FROM table;\n", 1000) + b.ResetTimer() + for b.Loop() { + // comparing to detectChromaLexerWithAnalyze (go-enry), "chroma/lexers.Analyse" is very slow + // BenchmarkChromaAnalyze-12 519 2247104 ns/op + lexers.Analyse(code) + } +} + +func BenchmarkRenderCodeByLexer(b *testing.B) { + code := strings.Repeat("SELECT * FROM table;\n", 1000) + lexer := DetectChromaLexerByFileName("a.sql", "") + b.ResetTimer() + for b.Loop() { + // HINT: CODE-HIGHLIGHT-PERFORMANCE: Really slow ....... the regexp2 used by Chroma takes most of the time + // BenchmarkRenderCodeByLexer-12 22 47159038 ns/op + RenderCodeByLexer(lexer, code) + } +} diff --git a/modules/highlight/lexerdetect_test.go b/modules/highlight/lexerdetect_test.go index a06053be0c..753c949753 100644 --- a/modules/highlight/lexerdetect_test.go +++ b/modules/highlight/lexerdetect_test.go @@ -4,53 +4,11 @@ package highlight import ( - "strings" "testing" - "github.com/alecthomas/chroma/v2/lexers" "github.com/stretchr/testify/assert" ) -func BenchmarkDetectChromaLexerByFileName(b *testing.B) { - for b.Loop() { - // BenchmarkDetectChromaLexerByFileName-12 18214717 61.35 ns/op - DetectChromaLexerByFileName("a.sql", "") - } -} - -func BenchmarkDetectChromaLexerWithAnalyze(b *testing.B) { - b.StopTimer() - code := []byte(strings.Repeat("SELECT * FROM table;\n", 1000)) - b.StartTimer() - for b.Loop() { - // BenchmarkRenderCodeSlowGuess-12 87946 13310 ns/op - detectChromaLexerWithAnalyze("a", "", code) - } -} - -func BenchmarkChromaAnalyze(b *testing.B) { - b.StopTimer() - code := strings.Repeat("SELECT * FROM table;\n", 1000) - b.StartTimer() - for b.Loop() { - // comparing to detectChromaLexerWithAnalyze (go-enry), "chroma/lexers.Analyse" is very slow - // BenchmarkChromaAnalyze-12 519 2247104 ns/op - lexers.Analyse(code) - } -} - -func BenchmarkRenderCodeByLexer(b *testing.B) { - b.StopTimer() - code := strings.Repeat("SELECT * FROM table;\n", 1000) - lexer := DetectChromaLexerByFileName("a.sql", "") - b.StartTimer() - for b.Loop() { - // Really slow ....... the regexp2 used by Chroma takes most of the time - // BenchmarkRenderCodeByLexer-12 22 47159038 ns/op - RenderCodeByLexer(lexer, code) - } -} - func TestDetectChromaLexer(t *testing.T) { globalVars().highlightMapping[".my-html"] = "HTML" t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") }) diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 294bd7e1cb..e515545e08 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -288,7 +288,7 @@ func prepareMockDataUnicodeEscape(ctx *context.Context) { lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines)) for i, hl := range highlightLines { lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, ctx.Locale) - escapeStatus = escapeStatus.Or(lineEscapeStatus[i]) + escapeStatus.Combine(lineEscapeStatus[i]) } ctx.Data["HighlightLines"] = highlightLines ctx.Data["EscapeStatus"] = escapeStatus diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 764e01841e..06805b7b95 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -272,7 +272,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames line = template.HTML(util.UnsafeBytesToString(unsafeLines[i])) } br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale) - escapeStatus = escapeStatus.Or(br.EscapeStatus) + escapeStatus.Combine(br.EscapeStatus) } ctx.Data["EscapeStatus"] = escapeStatus diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index f9c43c5ca7..dac82ae7ac 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -127,7 +127,7 @@ func handleFileViewRenderSource(ctx *context.Context, attrs *attribute.Attribute statuses := make([]*charset.EscapeStatus, len(fileContent)) for i, line := range fileContent { statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) - status = status.Or(statuses[i]) + status.Combine(statuses[i]) } ctx.Data["EscapeStatus"] = status ctx.Data["FileContent"] = fileContent diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 930ef6719c..d1e3027f07 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -79,6 +79,8 @@ type DiffLine struct { Content string Comments issues_model.CommentList // related PR code comments SectionInfo *DiffLineSectionInfo + + cachedDiffInline *DiffInline } // DiffLineSectionInfo represents diff line section meta data @@ -294,14 +296,6 @@ func newDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRig } } -// escape a line's content or return
needed for copy/paste purposes -func getLineContent(content string, locale translation.Locale) DiffInline { - if len(content) > 0 { - return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)), locale) - } - return DiffInline{EscapeStatus: &charset.EscapeStatus{}, Content: "
"} -} - // DiffSection represents a section of a DiffFile. type DiffSection struct { language *diffVarMutable[string] @@ -332,8 +326,8 @@ type DiffInline struct { Content template.HTML } -// DiffInlineWithUnicodeEscape makes a DiffInline with hidden Unicode characters escaped -func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline { +// diffInlineWithUnicodeEscape makes a DiffInline with hidden Unicode characters escaped +func diffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline { status, content := charset.EscapeControlHTML(s, locale) return DiffInline{EscapeStatus: status, Content: content} } @@ -356,6 +350,13 @@ func (diffSection *DiffSection) getLineContentForRender(lineIdx int, diffLine *D } func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType, leftLine, rightLine *DiffLine, locale translation.Locale) DiffInline { + sideIdx := util.Iif(diffLineType == DiffLineDel, 0, 1) // del=left, add=right + lines := [2]*DiffLine{leftLine, rightLine} + + if lines[sideIdx] != nil && lines[sideIdx].cachedDiffInline != nil { + return *lines[sideIdx].cachedDiffInline + } + var fileLanguage string var highlightedLeftLines, highlightedRightLines map[int]template.HTML // when a "diff section" is manually prepared by ExcerptBlob, it doesn't have "file" information @@ -364,33 +365,36 @@ func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType, highlightedLeftLines, highlightedRightLines = diffSection.highlightedLeftLines.value, diffSection.highlightedRightLines.value } - var lineHTML template.HTML - hcd := newHighlightCodeDiff() if diffLineType == DiffLinePlain { - // left and right are the same, no need to do line-level diff - if leftLine != nil { - lineHTML = diffSection.getLineContentForRender(leftLine.LeftIdx, leftLine, fileLanguage, highlightedLeftLines) - } else if rightLine != nil { - lineHTML = diffSection.getLineContentForRender(rightLine.RightIdx, rightLine, fileLanguage, highlightedRightLines) - } - } else { - var diff1, diff2 template.HTML - if leftLine != nil { - diff1 = diffSection.getLineContentForRender(leftLine.LeftIdx, leftLine, fileLanguage, highlightedLeftLines) - } - if rightLine != nil { - diff2 = diffSection.getLineContentForRender(rightLine.RightIdx, rightLine, fileLanguage, highlightedRightLines) - } - if diff1 != "" && diff2 != "" { - // if only some parts of a line are changed, highlight these changed parts as "deleted/added". - lineHTML = hcd.diffLineWithHighlight(diffLineType, diff1, diff2) - } else { - // if left is empty or right is empty (a line is fully deleted or added), then we do not need to diff anymore. - // the tmpl code already adds background colors for these cases. - lineHTML = util.Iif(diffLineType == DiffLineDel, diff1, diff2) - } + // left and right are the same, no need to do line-level diff, can just pick any side + // caller always uses the "right side" for this type + lineHTML := diffSection.getLineContentForRender(rightLine.RightIdx, rightLine, fileLanguage, highlightedRightLines) + return diffInlineWithUnicodeEscape(lineHTML, locale) } - return DiffInlineWithUnicodeEscape(lineHTML, locale) + + var diffs [2]template.HTML + if leftLine != nil { + diffs[0] = diffSection.getLineContentForRender(leftLine.LeftIdx, leftLine, fileLanguage, highlightedLeftLines) + } + if rightLine != nil { + diffs[1] = diffSection.getLineContentForRender(rightLine.RightIdx, rightLine, fileLanguage, highlightedRightLines) + } + + if leftLine != nil && rightLine != nil { + // if only some parts of a line are changed, highlight these changed parts as "deleted/added". + // "diff" the left&right sides together, then cache the diff result for another side, + // because when viewing the diff page, both "deleted" and "added" lines will to be rendered eventually, + // so here only diff them once, then next render can just use the cached result, no need to "diff" again. + hcd := newHighlightCodeDiff() + lineHTMLDel, lineHTMLAdd := hcd.diffLineWithHighlight(diffs[0], diffs[1]) + leftLine.cachedDiffInline = new(diffInlineWithUnicodeEscape(lineHTMLDel, locale)) + rightLine.cachedDiffInline = new(diffInlineWithUnicodeEscape(lineHTMLAdd, locale)) + return *lines[sideIdx].cachedDiffInline + } + + // if left is empty or right is empty (a line is fully deleted or added), then we do not need to diff anymore. + // the tmpl code already adds background colors for these cases. + return diffInlineWithUnicodeEscape(diffs[sideIdx], locale) } // GetComputedInlineDiffFor computes inline diff for the given line. @@ -404,7 +408,8 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc // try to find equivalent diff line. ignore, otherwise switch diffLine.Type { case DiffLineSection: - return getLineContent(diffLine.Content, locale) + // section content is a diff hunk header, it isn't code diff, its trailing context might come from the file content, might not + return diffInlineWithUnicodeEscape(htmlutil.EscapeString(diffLine.Content), locale) case DiffLineAdd: compareDiffLine := diffSection.GetLine(diffLine.Match) return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale) @@ -412,8 +417,8 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc compareDiffLine := diffSection.GetLine(diffLine.Match) return diffSection.getDiffLineForRender(DiffLineDel, diffLine, compareDiffLine, locale) default: // Plain - // TODO: there was an "if" check: `if diffLine.Content >strings.IndexByte(" +-", diffLine.Content[0]) > -1 { ... } else { ... }` - // no idea why it needs that check, it seems that the "if" should be always true, so try to simplify the code + // Here it always uses "right side" to render the plain content (unchanged lines) + // tmpl also uses "RightIdx" to check whether to add "lines-code-old" CSS class to a line return diffSection.getDiffLineForRender(DiffLinePlain, nil, diffLine, locale) } } @@ -539,8 +544,7 @@ func (diffFile *DiffFile) addTailSection(detail DiffRenderDetail) { lastSection := diffFile.Sections[len(diffFile.Sections)-1] lastLine := lastSection.Lines[len(lastSection.Lines)-1] tailDiffLine := &DiffLine{ - Type: DiffLineSection, - Content: " ", + Type: DiffLineSection, SectionInfo: &DiffLineSectionInfo{ language: &diffFile.language, Path: diffFile.Name, diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 66b5eb4764..bc61dda1ce 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -18,6 +18,7 @@ import ( "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/json" "gitea.dev/modules/setting" + "gitea.dev/modules/translation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1109,6 +1110,15 @@ func TestDiffLine_GetExpandDirection(t *testing.T) { } } +func TestDiffSection_GetComputedInlineDiffFor(t *testing.T) { + t.Run("Section", func(t *testing.T) { + diffLine := &DiffLine{Type: DiffLineSection, Content: "@@ -1,3 +1,3 @@ func \u202ename() "} + diffInline := (&DiffSection{}).GetComputedInlineDiffFor(diffLine, translation.MockLocale{}) + assert.True(t, diffInline.EscapeStatus.Escaped) + assert.Equal(t, `@@ -1,3 +1,3 @@ func `+"\u202e"+`name() <b>`, string(diffInline.Content)) + }) +} + func TestHighlightCodeLines(t *testing.T) { t.Run("CharsetDetecting", func(t *testing.T) { diffFile := &DiffFile{ diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go index fef61de24d..5cdd894e01 100644 --- a/services/gitdiff/highlightdiff.go +++ b/services/gitdiff/highlightdiff.go @@ -150,7 +150,7 @@ func (hcd *highlightCodeDiff) diffEqualPartIsSpaceOnly(s string) bool { return true } -func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA, codeB template.HTML) template.HTML { +func (hcd *highlightCodeDiff) diffLineWithHighlight(codeA, codeB template.HTML) (del, add template.HTML) { hcd.collectUsedRunes(codeA) hcd.collectUsedRunes(codeB) @@ -161,8 +161,6 @@ func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA diffs := dmp.DiffMain(convertedCodeA, convertedCodeB, true) diffs = dmp.DiffCleanupSemantic(diffs) - buf := bytes.NewBuffer(nil) - if hcd.diffCodeClose == 0 { // tests can pre-set the placeholders hcd.diffCodeAddedOpen = hcd.registerTokenAsPlaceholder(``) @@ -183,32 +181,37 @@ func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA // only add "added"/"removed" tags when needed: // * non-space contents appear in the DiffEqual parts (not a full-line add/del) // * placeholder map still works (not exhausted, can get the closing tag placeholder) + bufDel := bytes.NewBuffer(nil) + bufAdd := bytes.NewBuffer(nil) addDiffTags := !equalPartSpaceOnly && hcd.diffCodeClose != 0 if addDiffTags { for _, diff := range diffs { - switch { - case diff.Type == diffmatchpatch.DiffEqual: - buf.WriteString(diff.Text) - case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: - buf.WriteRune(hcd.diffCodeAddedOpen) - buf.WriteString(diff.Text) - buf.WriteRune(hcd.diffCodeClose) - case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: - buf.WriteRune(hcd.diffCodeRemovedOpen) - buf.WriteString(diff.Text) - buf.WriteRune(hcd.diffCodeClose) + switch diff.Type { + case diffmatchpatch.DiffEqual: + bufDel.WriteString(diff.Text) + bufAdd.WriteString(diff.Text) + case diffmatchpatch.DiffInsert: + bufAdd.WriteRune(hcd.diffCodeAddedOpen) + bufAdd.WriteString(diff.Text) + bufAdd.WriteRune(hcd.diffCodeClose) + case diffmatchpatch.DiffDelete: + bufDel.WriteRune(hcd.diffCodeRemovedOpen) + bufDel.WriteString(diff.Text) + bufDel.WriteRune(hcd.diffCodeClose) } } } else { // the caller will still add added/removed backgrounds for the whole line for _, diff := range diffs { - take := diff.Type == diffmatchpatch.DiffEqual || (diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd) || (diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel) - if take { - buf.WriteString(diff.Text) + if diff.Type == diffmatchpatch.DiffEqual || diff.Type == diffmatchpatch.DiffDelete { + bufDel.WriteString(diff.Text) + } + if diff.Type == diffmatchpatch.DiffEqual || diff.Type == diffmatchpatch.DiffInsert { + bufAdd.WriteString(diff.Text) } } } - return hcd.recoverOneDiff(buf.String()) + return hcd.recoverOneDiff(util.UnsafeBytesToString(bufDel.Bytes())), hcd.recoverOneDiff(util.UnsafeBytesToString(bufAdd.Bytes())) } func (hcd *highlightCodeDiff) registerTokenAsPlaceholder(token string) rune { diff --git a/services/gitdiff/highlightdiff_test.go b/services/gitdiff/highlightdiff_test.go index 9036311bca..b4a64a2f93 100644 --- a/services/gitdiff/highlightdiff_test.go +++ b/services/gitdiff/highlightdiff_test.go @@ -10,31 +10,20 @@ import ( "testing" "gitea.dev/modules/highlight" + "gitea.dev/modules/translation" + "github.com/alecthomas/chroma/v2" "github.com/stretchr/testify/assert" ) -func BenchmarkHighlightDiff(b *testing.B) { - for b.Loop() { - // still fast enough: BenchmarkHighlightDiff-12 1000000 1027 ns/op - // TODO: the real bottleneck is that "diffLineWithHighlight" is called twice when rendering "added" and "removed" lines by the caller - // Ideally the caller should cache the diff result, and then use the diff result to render "added" and "removed" lines separately - hcd := newHighlightCodeDiff() - codeA := template.HTML(`x foo y`) - codeB := template.HTML(`x bar y`) - hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) - } -} - func TestDiffWithHighlight(t *testing.T) { t.Run("DiffLineAddDel", func(t *testing.T) { t.Run("WithDiffTags", func(t *testing.T) { hcd := newHighlightCodeDiff() codeA := template.HTML(`x foo y`) codeB := template.HTML(`x bar y`) - outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) + outDel, outAdd := hcd.diffLineWithHighlight(codeA, codeB) assert.Equal(t, `x foo y`, string(outDel)) - outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB) assert.Equal(t, `x bar y`, string(outAdd)) }) t.Run("NoRedundantTags", func(t *testing.T) { @@ -43,9 +32,8 @@ func TestDiffWithHighlight(t *testing.T) { hcd := newHighlightCodeDiff() codeA := template.HTML(" \tfoo ") codeB := template.HTML(" bar \n") - outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) + outDel, outAdd := hcd.diffLineWithHighlight(codeA, codeB) assert.Equal(t, string(codeA), string(outDel)) - outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB) assert.Equal(t, string(codeB), string(outAdd)) }) }) @@ -54,16 +42,14 @@ func TestDiffWithHighlight(t *testing.T) { hcd := newHighlightCodeDiff() codeA := template.HTML(` this is a comment`) codeB := template.HTML(` this is updated comment`) - outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) + outDel, outAdd := hcd.diffLineWithHighlight(codeA, codeB) assert.Equal(t, ` this is a comment`, string(outDel)) - outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB) assert.Equal(t, ` this is updated comment`, string(outAdd)) codeA = `line1` + "\n" + `line2` codeB = `line1` + "\n" + `line!` - outDel = hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) + outDel, outAdd = hcd.diffLineWithHighlight(codeA, codeB) assert.Equal(t, `line1`+"\n"+`line2`, string(outDel)) - outAdd = hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB) assert.Equal(t, `line1`+"\n"+`line!`, string(outAdd)) }) @@ -79,12 +65,12 @@ func TestDiffWithHighlight(t *testing.T) { oldCode, _, _ := highlight.RenderCodeSlowGuess("a.go", "Go", `xxx || yyy`) newCode, _, _ := highlight.RenderCodeSlowGuess("a.go", "Go", `bot&xxx || bot&yyy`) hcd := newHighlightCodeDiff() - out := hcd.diffLineWithHighlight(DiffLineAdd, oldCode, newCode) + _, add := hcd.diffLineWithHighlight(oldCode, newCode) assert.Equal(t, strings.ReplaceAll(` bot& xxx || bot& -yyy`, "\n", ""), string(out)) +yyy`, "\n", ""), string(add)) }) forceTokenAsPlaceholder := func(hcd *highlightCodeDiff, r rune, token string) rune { @@ -127,14 +113,14 @@ func TestDiffWithHighlight(t *testing.T) { func TestDiffWithHighlightPlaceholder(t *testing.T) { hcd := newHighlightCodeDiff() - output := hcd.diffLineWithHighlight(DiffLineDel, "a='\U00100000'", "a='\U0010FFFD''") + output, _ := hcd.diffLineWithHighlight("a='\U00100000'", "a='\U0010FFFD''") assert.Empty(t, hcd.placeholderTokenMap[0x00100000]) assert.Empty(t, hcd.placeholderTokenMap[0x0010FFFD]) expected := fmt.Sprintf(`a='%s'`, "\U00100000") assert.Equal(t, expected, string(output)) hcd = newHighlightCodeDiff() - output = hcd.diffLineWithHighlight(DiffLineAdd, "a='\U00100000'", "a='\U0010FFFD'") + _, output = hcd.diffLineWithHighlight("a='\U00100000'", "a='\U0010FFFD'") expected = fmt.Sprintf(`a='%s'`, "\U0010FFFD") assert.Equal(t, expected, string(output)) } @@ -143,32 +129,58 @@ func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) { hcd := newHighlightCodeDiff() hcd.placeholderMaxCount = 0 placeHolderAmp := string(rune(0xFFFD)) - output := hcd.diffLineWithHighlight(DiffLineDel, `<`, `>`) - assert.Equal(t, placeHolderAmp+"lt;", string(output)) - output = hcd.diffLineWithHighlight(DiffLineAdd, `<`, `>`) - assert.Equal(t, placeHolderAmp+"gt;", string(output)) + del, add := hcd.diffLineWithHighlight(`<`, `>`) + assert.Equal(t, placeHolderAmp+"lt;", string(del)) + assert.Equal(t, placeHolderAmp+"gt;", string(add)) - output = hcd.diffLineWithHighlight(DiffLineDel, `foo`, `bar`) - assert.Equal(t, "foo", string(output)) - output = hcd.diffLineWithHighlight(DiffLineAdd, `foo`, `bar`) - assert.Equal(t, "bar", string(output)) + del, add = hcd.diffLineWithHighlight(`foo`, `bar`) + assert.Equal(t, "foo", string(del)) + assert.Equal(t, "bar", string(add)) } func TestDiffWithHighlightTagMatch(t *testing.T) { - f := func(t *testing.T, lineType DiffLineType) { - totalOverflow := 0 - for i := 0; ; i++ { - hcd := newHighlightCodeDiff() - hcd.placeholderMaxCount = i - output := string(hcd.diffLineWithHighlight(lineType, `<`, `>`)) - totalOverflow += hcd.placeholderOverflowCount - assert.Equal(t, strings.Count(output, "<`, `>`) + totalOverflow += hcd.placeholderOverflowCount + assert.Equal(t, strings.Count(string(del), "foo y`) + codeB := template.HTML(`x bar y`) + hcd.diffLineWithHighlight(codeA, codeB) + } +} + +func BenchmarkGetDiffLineForRender(b *testing.B) { + diffSection := &DiffSection{ + FileName: "test.go", + highlightLexer: &diffVarMutable[chroma.Lexer]{}, + } + leftLine := &DiffLine{LeftIdx: 1, Content: `-x foo y`} + rightLine := &DiffLine{RightIdx: 1, Content: `+x bar y`} + locale := translation.MockLocale{} + + b.ResetTimer() + // HINT: CODE-HIGHLIGHT-PERFORMANCE: the real bottleneck is in the Chroma highlighter. + for b.Loop() { + // Clear cache only at the start of rendering the pair + leftLine.cachedDiffInline = nil + rightLine.cachedDiffInline = nil + _ = diffSection.getDiffLineForRender(DiffLineDel, leftLine, rightLine, locale) + _ = diffSection.getDiffLineForRender(DiffLineAdd, leftLine, rightLine, locale) + } } diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go index faa7fd2db5..fa3f7dba6a 100644 --- a/services/markup/renderhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -102,7 +102,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines)) for i, hl := range highlightLines { lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.EscapeOptionsForView()) - escapeStatus = escapeStatus.Or(lineEscapeStatus[i]) + escapeStatus.Combine(lineEscapeStatus[i]) } return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{