code.gitea.io/gitea@v1.22.3/modules/indexer/code/search.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package code 5 6 import ( 7 "bytes" 8 "context" 9 "html/template" 10 "strings" 11 12 "code.gitea.io/gitea/modules/highlight" 13 "code.gitea.io/gitea/modules/indexer/code/internal" 14 "code.gitea.io/gitea/modules/timeutil" 15 ) 16 17 // Result a search result to display 18 type Result struct { 19 RepoID int64 20 Filename string 21 CommitID string 22 UpdatedUnix timeutil.TimeStamp 23 Language string 24 Color string 25 Lines []*ResultLine 26 } 27 28 type ResultLine struct { 29 Num int 30 FormattedContent template.HTML 31 } 32 33 type SearchResultLanguages = internal.SearchResultLanguages 34 35 type SearchOptions = internal.SearchOptions 36 37 func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { 38 startIndex := selectionStartIndex 39 numLinesBefore := 0 40 for ; startIndex > 0; startIndex-- { 41 if content[startIndex-1] == '\n' { 42 if numLinesBefore == 1 { 43 break 44 } 45 numLinesBefore++ 46 } 47 } 48 49 endIndex := selectionEndIndex 50 numLinesAfter := 0 51 for ; endIndex < len(content); endIndex++ { 52 if content[endIndex] == '\n' { 53 if numLinesAfter == 1 { 54 break 55 } 56 numLinesAfter++ 57 } 58 } 59 60 return startIndex, endIndex 61 } 62 63 func writeStrings(buf *bytes.Buffer, strs ...string) error { 64 for _, s := range strs { 65 _, err := buf.WriteString(s) 66 if err != nil { 67 return err 68 } 69 } 70 return nil 71 } 72 73 func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine { 74 // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting 75 hl, _ := highlight.Code(filename, language, code) 76 highlightedLines := strings.Split(string(hl), "\n") 77 78 // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` 79 lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums))) 80 for i := 0; i < len(lines); i++ { 81 lines[i] = &ResultLine{ 82 Num: lineNums[i], 83 FormattedContent: template.HTML(highlightedLines[i]), 84 } 85 } 86 return lines 87 } 88 89 func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { 90 startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") 91 92 var formattedLinesBuffer bytes.Buffer 93 94 contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n") 95 lineNums := make([]int, 0, len(contentLines)) 96 index := startIndex 97 for i, line := range contentLines { 98 var err error 99 if index < result.EndIndex && 100 result.StartIndex < index+len(line) && 101 result.StartIndex < result.EndIndex { 102 openActiveIndex := max(result.StartIndex-index, 0) 103 closeActiveIndex := min(result.EndIndex-index, len(line)) 104 err = writeStrings(&formattedLinesBuffer, 105 line[:openActiveIndex], 106 line[openActiveIndex:closeActiveIndex], 107 line[closeActiveIndex:], 108 ) 109 } else { 110 err = writeStrings(&formattedLinesBuffer, line) 111 } 112 if err != nil { 113 return nil, err 114 } 115 116 lineNums = append(lineNums, startLineNum+i) 117 index += len(line) 118 } 119 120 return &Result{ 121 RepoID: result.RepoID, 122 Filename: result.Filename, 123 CommitID: result.CommitID, 124 UpdatedUnix: result.UpdatedUnix, 125 Language: result.Language, 126 Color: result.Color, 127 Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()), 128 }, nil 129 } 130 131 // PerformSearch perform a search on a repository 132 // if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2 133 func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) { 134 if opts == nil || len(opts.Keyword) == 0 { 135 return 0, nil, nil, nil 136 } 137 138 total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts) 139 if err != nil { 140 return 0, nil, nil, err 141 } 142 143 displayResults := make([]*Result, len(results)) 144 145 for i, result := range results { 146 startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex) 147 displayResults[i], err = searchResult(result, startIndex, endIndex) 148 if err != nil { 149 return 0, nil, nil, err 150 } 151 } 152 return int(total), displayResults, resultLanguages, nil 153 }