From 663e957d3d83a760e03d14c12d53c3aefa97d20e Mon Sep 17 00:00:00 2001
From: Shiny Nematoda <snematoda.751k2@aleeas.com>
Date: Fri, 16 Aug 2024 13:23:25 +0000
Subject: [PATCH] ui(git-grep): expose regexp mode in dropdown

---
 modules/git/grep.go                      |  4 ++--
 options/locale/locale_en-US.ini          |  2 ++
 routers/web/explore/code.go              | 13 ++++++++++++-
 routers/web/repo/search.go               |  7 +++++--
 routers/web/user/code.go                 | 13 ++++++++++++-
 templates/shared/search/code/search.tmpl |  7 ++++---
 templates/shared/search/combo_multi.tmpl | 24 ++++++++++++++++++++++++
 templates/shared/search/fuzzy.tmpl       | 16 +++++-----------
 8 files changed, 66 insertions(+), 20 deletions(-)
 create mode 100644 templates/shared/search/combo_multi.tmpl

diff --git a/modules/git/grep.go b/modules/git/grep.go
index 1e34f0275c..3aa1442d29 100644
--- a/modules/git/grep.go
+++ b/modules/git/grep.go
@@ -82,12 +82,12 @@ 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",
+		"-I", "--null", "--break", "--heading",
 		"--line-number", "--ignore-case", "--full-name")
 	if opts.Mode == RegExpGrepMode {
 		cmd.AddArguments("--perl-regexp")
 	} else {
-		cmd.AddArguments("--fixed-strings")
+		cmd.AddArguments("--fixed-strings", "--column")
 	}
 	cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
 	cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile))
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1631c90ba2..625f85fac3 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 = Include results that match the 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..4db41fd726 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -35,11 +35,22 @@ func Code(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
+	isFuzzy := true
+	if mode := ctx.FormTrim("mode"); len(mode) > 0 {
+		isFuzzy = mode == "fuzzy"
+	} else {
+		isFuzzy = ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
+	}
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
+	ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"}
 	ctx.Data["IsFuzzy"] = isFuzzy
+	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 9a033c0f1b..724bb82612 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -27,7 +27,7 @@ const (
 
 func searchModeFromString(s string) searchMode {
 	switch s {
-	case "fuzzy":
+	case "fuzzy", "union":
 		return FuzzySearchMode
 	case "regexp":
 		return RegExpSearchMode
@@ -65,7 +65,7 @@ func Search(ctx *context.Context) {
 	ctx.Data["Language"] = language
 	ctx.Data["IsFuzzy"] = mode == FuzzySearchMode
 	ctx.Data["IsRegExp"] = mode == RegExpSearchMode
-	ctx.Data["SearchMode"] = mode.String()
+	ctx.Data["CodeSearchMode"] = mode.String()
 	ctx.Data["PageIsViewCode"] = true
 
 	if keyword == "" {
@@ -102,6 +102,7 @@ func Search(ctx *context.Context) {
 		} else {
 			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
 		}
+		ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"}
 	} else {
 		grepOpt := git.GrepOptions{
 			ContextLineNumber: 1,
@@ -110,6 +111,7 @@ func Search(ctx *context.Context) {
 		switch mode {
 		case FuzzySearchMode:
 			grepOpt.Mode = git.FixedAnyGrepMode
+			ctx.Data["CodeSearchMode"] = "union"
 		case RegExpSearchMode:
 			grepOpt.Mode = git.RegExpGrepMode
 		}
@@ -133,6 +135,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..9e8614cb04 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -40,11 +40,22 @@ func CodeSearch(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
+	isFuzzy := true
+	if mode := ctx.FormTrim("mode"); len(mode) > 0 {
+		isFuzzy = mode == "fuzzy"
+	} else {
+		isFuzzy = ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
+	}
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
+	ctx.Data["CodeSearchOptions"] = []string{"exact", "fuzzy"}
 	ctx.Data["IsFuzzy"] = isFuzzy
+	if isFuzzy {
+		ctx.Data["CodeSearchMode"] = "fuzzy"
+	} else {
+		ctx.Data["CodeSearchMode"] = "exact"
+	}
 	ctx.Data["IsCodePage"] = true
 
 	if keyword == "" {
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl
index 6a52bb9462..b81c6c8f36 100644
--- a/templates/shared/search/code/search.tmpl
+++ b/templates/shared/search/code/search.tmpl
@@ -1,11 +1,12 @@
 <form class="ui form ignore-dirty">
-	{{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}}
+			"CodeIndexerDisabled" $.CodeIndexerDisabled
+			"Selected" $.CodeSearchMode
+			"Options" $.CodeSearchOptions}}
 </form>
 <div class="divider"></div>
 <div class="ui user list">
diff --git a/templates/shared/search/combo_multi.tmpl b/templates/shared/search/combo_multi.tmpl
new file mode 100644
index 0000000000..23c4b4d406
--- /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 */}}
+<div class="ui small fluid action input">
+	{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+	<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
+        <div class="text">
+            {{ctx.Locale.Tr (printf "search.%s" .Selected)}}
+        </div>
+        <div class="menu" data-test-tag="fuzzy-dropdown">
+            {{range $opt := .Options}}
+                {{$isActive := eq $.Selected $opt}}
+                <label class="{{if $isActive}}active {{end}}item" data-value="{{$opt}}" data-tooltip-content="{{ctx.Locale.Tr (printf "search.%s_tooltip" $opt)}}">
+                    <input hidden type="radio" name="mode" value="{{$opt}}"{{if $isActive}} checked{{end}}/>
+                    {{ctx.Locale.Tr (printf "search.%s" $opt)}}
+                </label>
+            {{end}}
+        </div>
+    </div>
+	{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
+</div>
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 */}}
-<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}" data-test-tag="fuzzy-dropdown">
-	{{$fuzzyType := "fuzzy"}}
-	{{if .CodeIndexerDisabled}}
-		{{$fuzzyType = "union"}}
-	{{end}}
+<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
 	<input name="fuzzy" type="hidden"{{if .Disabled}} disabled{{end}} value="{{.IsFuzzy}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-	<div class="text">{{/*
-		if code indexer is disabled display fuzzy as union
-	*/}}{{if .IsFuzzy}}{{/*
-		*/}}{{ctx.Locale.Tr (printf "search.%s" $fuzzyType)}}{{/*
+	<div class="text">{{if .IsFuzzy}}{{/*
+		*/}}{{ctx.Locale.Tr "search.fuzzy"}}{{/*
 	*/}}{{else}}{{/*
 		*/}}{{ctx.Locale.Tr "search.exact"}}{{/*
 	*/}}{{end}}</div>
 	<div class="menu">
-		<div class="item" data-value="true" data-tooltip-content="{{ctx.Locale.Tr (printf "search.%s_tooltip" $fuzzyType)}}">{{/*
-		*/}}{{ctx.Locale.Tr (printf "search.%s" $fuzzyType)}}</div>
+		<div class="item" data-value="true" data-tooltip-content="{{ctx.Locale.Tr "search.fuzzy_tooltip"}}">{{/*
+		*/}}{{ctx.Locale.Tr "search.fuzzy"}}</div>
 		<div class="item" data-value="false" data-tooltip-content="{{ctx.Locale.Tr "search.exact_tooltip"}}">{{ctx.Locale.Tr "search.exact"}}</div>
 	</div>
 </div>