diff options
Diffstat (limited to '')
-rw-r--r-- | modules/indexer/code/search.go | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go new file mode 100644 index 0000000..f45907a --- /dev/null +++ b/modules/indexer/code/search.go @@ -0,0 +1,228 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package code + +import ( + "bytes" + "context" + "html/template" + "strings" + + "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/services/gitdiff" +) + +// Result a search result to display +type Result struct { + RepoID int64 + Filename string + CommitID string + UpdatedUnix timeutil.TimeStamp + Language string + Color string + Lines []ResultLine +} + +type ResultLine struct { + Num int + FormattedContent template.HTML +} + +type SearchResultLanguages = internal.SearchResultLanguages + +type SearchOptions = internal.SearchOptions + +func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { + startIndex := selectionStartIndex + numLinesBefore := 0 + for ; startIndex > 0; startIndex-- { + if content[startIndex-1] == '\n' { + if numLinesBefore == 1 { + break + } + numLinesBefore++ + } + } + + endIndex := selectionEndIndex + numLinesAfter := 0 + for ; endIndex < len(content); endIndex++ { + if content[endIndex] == '\n' { + if numLinesAfter == 1 { + break + } + numLinesAfter++ + } + } + + return startIndex, endIndex +} + +func writeStrings(buf *bytes.Buffer, strs ...string) error { + for _, s := range strs { + _, err := buf.WriteString(s) + if err != nil { + return err + } + } + return nil +} + +const ( + highlightTagStart = "<span class=\"search-highlight\">" + highlightTagEnd = "</span>" +) + +func HighlightSearchResultCode(filename string, lineNums []int, highlightRanges [][3]int, code string) []ResultLine { + hcd := gitdiff.NewHighlightCodeDiff() + hcd.CollectUsedRunes(code) + startTag, endTag := hcd.NextPlaceholder(), hcd.NextPlaceholder() + hcd.PlaceholderTokenMap[startTag] = highlightTagStart + hcd.PlaceholderTokenMap[endTag] = highlightTagEnd + + // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting + hl, _ := highlight.Code(filename, "", code) + conv := hcd.ConvertToPlaceholders(string(hl)) + convLines := strings.Split(conv, "\n") + + // each highlightRange is of the form [line number, start pos, end pos] + for _, highlightRange := range highlightRanges { + ln, start, end := highlightRange[0], highlightRange[1], highlightRange[2] + line := convLines[ln] + if line == "" || len(line) <= start || len(line) < end { + continue + } + + sb := strings.Builder{} + count := -1 + isOpen := false + for _, r := range line { + if token, ok := hcd.PlaceholderTokenMap[r]; + // token was not found + !ok || + // token was marked as used + token == "" || + // the token is not an valid html tag emitted by chroma + !(len(token) > 6 && (token[0:5] == "<span" || token[0:6] == "</span")) { + count++ + } else if !isOpen { + // open the tag only after all other placeholders + sb.WriteRune(r) + continue + } else if isOpen && count < end { + // if the tag is open, but a placeholder exists in between + // close the tag + sb.WriteRune(endTag) + // write the placeholder + sb.WriteRune(r) + // reopen the tag + sb.WriteRune(startTag) + continue + } + + switch count { + case end: + // if tag is not open, no need to close + if !isOpen { + break + } + sb.WriteRune(endTag) + isOpen = false + case start: + // if tag is open, do not open again + if isOpen { + break + } + isOpen = true + sb.WriteRune(startTag) + } + + sb.WriteRune(r) + } + if isOpen { + sb.WriteRune(endTag) + } + convLines[ln] = sb.String() + } + conv = strings.Join(convLines, "\n") + + highlightedLines := strings.Split(hcd.Recover(conv), "\n") + // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` + lines := make([]ResultLine, min(len(highlightedLines), len(lineNums))) + for i := 0; i < len(lines); i++ { + lines[i].Num = lineNums[i] + lines[i].FormattedContent = template.HTML(highlightedLines[i]) + } + return lines +} + +func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { + startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") + + var formattedLinesBuffer bytes.Buffer + + contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n") + lineNums := make([]int, 0, len(contentLines)) + index := startIndex + var highlightRanges [][3]int + for i, line := range contentLines { + var err error + if index < result.EndIndex && + result.StartIndex < index+len(line) && + result.StartIndex < result.EndIndex { + openActiveIndex := max(result.StartIndex-index, 0) + closeActiveIndex := min(result.EndIndex-index, len(line)) + highlightRanges = append(highlightRanges, [3]int{i, openActiveIndex, closeActiveIndex}) + err = writeStrings(&formattedLinesBuffer, + line[:openActiveIndex], + line[openActiveIndex:closeActiveIndex], + line[closeActiveIndex:], + ) + } else { + err = writeStrings(&formattedLinesBuffer, line) + } + if err != nil { + return nil, err + } + + lineNums = append(lineNums, startLineNum+i) + index += len(line) + } + + return &Result{ + RepoID: result.RepoID, + Filename: result.Filename, + CommitID: result.CommitID, + UpdatedUnix: result.UpdatedUnix, + Language: result.Language, + Color: result.Color, + Lines: HighlightSearchResultCode(result.Filename, lineNums, highlightRanges, formattedLinesBuffer.String()), + }, nil +} + +// PerformSearch perform a search on a repository +// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2 +func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) { + if opts == nil || len(opts.Keyword) == 0 { + return 0, nil, nil, nil + } + + total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts) + if err != nil { + return 0, nil, nil, err + } + + displayResults := make([]*Result, len(results)) + + for i, result := range results { + startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex) + displayResults[i], err = searchResult(result, startIndex, endIndex) + if err != nil { + return 0, nil, nil, err + } + } + return int(total), displayResults, resultLanguages, nil +} |