diff --git a/modules/git/grep.go b/modules/git/grep.go index 5572bd994f..8a896bc5aa 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -27,12 +27,20 @@ type GrepResult struct { HighlightedRanges [][3]int } +type grepMode int + +const ( + FixedGrepMode grepMode = iota + FixedAnyGrepMode + RegExpGrepMode +) + type GrepOptions struct { RefName string MaxResultLimit int MatchesPerFile int ContextLineNumber int - IsFuzzy bool + Mode grepMode PathSpec []setting.Glob } @@ -74,12 +82,20 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO var results []*GrepResult // -I skips binary files cmd := NewCommand(ctx, "grep", - "-I", "--null", "--break", "--heading", "--column", - "--fixed-strings", "--line-number", "--ignore-case", "--full-name") + "-I", "--null", "--break", "--heading", + "--line-number", "--ignore-case", "--full-name") + if opts.Mode == RegExpGrepMode { + // No `--column` -- regexp mode does not support highlighting in the + // current implementation as the length of the match is unknown from + // `grep` but required for highlighting. + cmd.AddArguments("--perl-regexp") + } else { + cmd.AddArguments("--fixed-strings", "--column") + } cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile)) words := []string{search} - if opts.IsFuzzy { + if opts.Mode == FixedAnyGrepMode { words = strings.Fields(search) } for _, word := range words { @@ -148,6 +164,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { lineNumInt, _ := strconv.Atoi(lineNum) res.LineNumbers = append(res.LineNumbers, lineNumInt) + // We support highlighting only when `--column` parameter is used. if lineCol, lineCode2, ok := strings.Cut(lineCode, "\x00"); ok { lineColInt, _ := strconv.Atoi(lineCol) start := lineColInt - 1 diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 3ba7a6efcb..835f441b19 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -201,3 +201,34 @@ func TestGrepRefs(t *testing.T) { assert.Len(t, res, 1) assert.Equal(t, "A", res[0].LineCodes[0]) } + +func TestGrepCanHazRegexOnDemand(t *testing.T) { + tmpDir := t.TempDir() + + err := InitRepository(DefaultContext, tmpDir, false, Sha1ObjectFormat.Name()) + require.NoError(t, err) + + gitRepo, err := openRepositoryWithDefaultContext(tmpDir) + require.NoError(t, err) + defer gitRepo.Close() + + require.NoError(t, os.WriteFile(path.Join(tmpDir, "matching"), []byte("It's a match!"), 0o666)) + require.NoError(t, os.WriteFile(path.Join(tmpDir, "not-matching"), []byte("Orisitamatch?"), 0o666)) + + err = AddChanges(tmpDir, true) + require.NoError(t, err) + + err = CommitChanges(tmpDir, CommitChangesOptions{Message: "Add fixtures for regexp test"}) + require.NoError(t, err) + + // should find nothing by default... + res, err := GrepSearch(context.Background(), gitRepo, "\\bmatch\\b", GrepOptions{}) + require.NoError(t, err) + assert.Empty(t, res) + + // ... unless configured explicitly + res, err = GrepSearch(context.Background(), gitRepo, "\\bmatch\\b", GrepOptions{Mode: RegExpGrepMode}) + require.NoError(t, err) + assert.Len(t, res, 1) + assert.Equal(t, "matching", res[0].Filename) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 61a820774d..b84cb42b85 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -173,6 +173,8 @@ union = Union union_tooltip = Include results that match any of the whitespace seperated keywords exact = Exact exact_tooltip = Include only results that match the exact search term +regexp = RegExp +regexp_tooltip = Interpret the search term as a regular expression repo_kind = Search repos... user_kind = Search users... org_kind = Search orgs... diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index f61b832572..437282cbb1 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -36,10 +36,18 @@ func Code(ctx *context.Context) { keyword := ctx.FormTrim("q") isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) + if mode := ctx.FormTrim("mode"); len(mode) > 0 { + isFuzzy = mode == "fuzzy" + } ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy + ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"} + if isFuzzy { + ctx.Data["CodeSearchMode"] = "fuzzy" + } else { + ctx.Data["CodeSearchMode"] = "exact" + } ctx.Data["PageIsViewCode"] = true if keyword == "" { diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index c4f9f9afd1..863a279af0 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -17,16 +17,53 @@ import ( const tplSearch base.TplName = "repo/search" +type searchMode int + +const ( + ExactSearchMode searchMode = iota + FuzzySearchMode + RegExpSearchMode +) + +func searchModeFromString(s string) searchMode { + switch s { + case "fuzzy", "union": + return FuzzySearchMode + case "regexp": + return RegExpSearchMode + default: + return ExactSearchMode + } +} + +func (m searchMode) String() string { + switch m { + case ExactSearchMode: + return "exact" + case FuzzySearchMode: + return "fuzzy" + case RegExpSearchMode: + return "regexp" + default: + panic("cannot happen") + } +} + // Search render repository search page func Search(ctx *context.Context) { language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) + mode := ExactSearchMode + if modeStr := ctx.FormString("mode"); len(modeStr) > 0 { + mode = searchModeFromString(modeStr) + } else if ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) { // for backward compatibility in links + mode = FuzzySearchMode + } ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy + ctx.Data["CodeSearchMode"] = mode.String() ctx.Data["PageIsViewCode"] = true if keyword == "" { @@ -47,7 +84,7 @@ func Search(ctx *context.Context) { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, Keyword: keyword, - IsKeywordFuzzy: isFuzzy, + IsKeywordFuzzy: mode == FuzzySearchMode, Language: language, Paginator: &db.ListOptions{ Page: page, @@ -63,12 +100,20 @@ func Search(ctx *context.Context) { } else { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } + ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"} } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + grepOpt := git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, RefName: ctx.Repo.RefName, - }) + } + switch mode { + case FuzzySearchMode: + grepOpt.Mode = git.FixedAnyGrepMode + ctx.Data["CodeSearchMode"] = "union" + case RegExpSearchMode: + grepOpt.Mode = git.RegExpGrepMode + } + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, grepOpt) if err != nil { ctx.ServerError("GrepSearch", err) return @@ -88,6 +133,7 @@ func Search(ctx *context.Context) { Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, r.HighlightedRanges, strings.Join(r.LineCodes, "\n")), }) } + ctx.Data["CodeSearchOptions"] = []string{"exact", "union", "regexp"} } ctx.Data["CodeIndexerDisabled"] = !setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/user/code.go b/routers/web/user/code.go index e2e8f25661..26e48d1ea6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -41,10 +41,18 @@ func CodeSearch(ctx *context.Context) { keyword := ctx.FormTrim("q") isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) + if mode := ctx.FormTrim("mode"); len(mode) > 0 { + isFuzzy = mode == "fuzzy" + } ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy + ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"} + if isFuzzy { + ctx.Data["CodeSearchMode"] = "fuzzy" + } else { + ctx.Data["CodeSearchMode"] = "exact" + } ctx.Data["IsCodePage"] = true if keyword == "" { diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 24779d41e0..e1b37d1e7f 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -417,7 +417,7 @@ func SearchWikiContents(ctx context.Context, repo *repo_model.Repository, keywor return git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{ ContextLineNumber: 0, - IsFuzzy: true, + Mode: git.FixedAnyGrepMode, RefName: repo.GetWikiBranchName(), MaxResultLimit: 10, MatchesPerFile: 3, diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl index 6d114c81e4..34707bc8da 100644 --- a/templates/repo/search.tmpl +++ b/templates/repo/search.tmpl @@ -5,7 +5,7 @@ {{if $.CodeIndexerDisabled}} {{$branchURLPrefix := printf "%s/search/branch/" $.RepoLink}} {{$tagURLPrefix := printf "%s/search/tag/" $.RepoLink}} - {{$suffix := printf "?q=%s&fuzzy=%t" (.Keyword|QueryEscape) .IsFuzzy}} + {{$suffix := printf "?q=%s&mode=%s" (.Keyword|QueryEscape) .CodeSearchMode}} {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mb-3" "branchURLPrefix" $branchURLPrefix "branchURLSuffix" $suffix "tagURLPrefix" $tagURLPrefix "tagURLSuffix" $suffix}} {{end}} {{template "shared/search/code/search" .}} diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl index fe375579bd..98c5430502 100644 --- a/templates/shared/search/code/results.tmpl +++ b/templates/shared/search/code/results.tmpl @@ -1,7 +1,7 @@
{{range $term := .SearchResultLanguages}} + href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&mode={{$.CodeSearchMode}}"> {{$term.Language}}
{{$term.Count}}
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl index 6a52bb9462..7f1fda8822 100644 --- a/templates/shared/search/code/search.tmpl +++ b/templates/shared/search/code/search.tmpl @@ -1,11 +1,11 @@
- {{template "shared/search/combo_fuzzy" + {{template "shared/search/combo_multi" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable - "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind") - "CodeIndexerDisabled" $.CodeIndexerDisabled}} + "Selected" $.CodeSearchMode + "Options" $.CodeSearchOptions}}
diff --git a/templates/shared/search/combo_fuzzy.tmpl b/templates/shared/search/combo_fuzzy.tmpl index 38189b0518..6dfec4c288 100644 --- a/templates/shared/search/combo_fuzzy.tmpl +++ b/templates/shared/search/combo_fuzzy.tmpl @@ -2,14 +2,12 @@ {{/* Disabled (optional) - if search field/button has to be disabled */}} {{/* Placeholder (optional) - placeholder text to be used */}} {{/* IsFuzzy - state of the fuzzy/union search toggle */}} -{{/* CodeIndexerDisabled (optional) - if the performed search is done using git-grep */}} {{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} {{template "shared/search/fuzzy" dict "Disabled" .Disabled - "IsFuzzy" .IsFuzzy - "CodeIndexerDisabled" .CodeIndexerDisabled}} + "IsFuzzy" .IsFuzzy}} {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
diff --git a/templates/shared/search/combo_multi.tmpl b/templates/shared/search/combo_multi.tmpl new file mode 100644 index 0000000000..89dc20b530 --- /dev/null +++ b/templates/shared/search/combo_multi.tmpl @@ -0,0 +1,24 @@ +{{/* Value - value of the search field (for search results page) */}} +{{/* Disabled (optional) - if search field/button has to be disabled */}} +{{/* Placeholder (optional) - placeholder text to be used */}} +{{/* Selected - the currently selected option */}} +{{/* Options - options available to choose from */}} +{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}} +
+ {{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} + + {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}} +
diff --git a/templates/shared/search/fuzzy.tmpl b/templates/shared/search/fuzzy.tmpl index 25cfc5762c..f0344c32b7 100644 --- a/templates/shared/search/fuzzy.tmpl +++ b/templates/shared/search/fuzzy.tmpl @@ -1,21 +1,15 @@ {{/* Disabled (optional) - if dropdown has to be disabled */}} {{/* IsFuzzy - state of the fuzzy search toggle */}} -