From b59b0cad0a550223f74add109ff13c0d2f4309f3 Mon Sep 17 00:00:00 2001
From: Lauris BH <lauris@nix.lv>
Date: Tue, 11 Oct 2022 02:12:03 +0300
Subject: Add user/organization code search (#19977)

Fixes #19925

Screenshots:

![attels](https://user-images.githubusercontent.com/165205/173864718-fe789429-55bc-4cad-808c-9f02f335cddf.png)
---
 models/repo/repo_list.go            |  30 ++++++---
 modules/context/org.go              |   1 +
 options/locale/locale_en-US.ini     |   8 +++
 routers/web/explore/code.go         | 125 +++++++++++++++++++-----------------
 routers/web/repo/search.go          |  22 +++++--
 routers/web/user/code.go            | 114 ++++++++++++++++++++++++++++++++
 routers/web/user/package.go         |   4 ++
 routers/web/user/profile.go         |   1 +
 routers/web/web.go                  |   1 +
 templates/code/searchform.tmpl      |  14 ++++
 templates/code/searchresults.tmpl   |  43 +++++++++++++
 templates/explore/code.tmpl         |  61 +-----------------
 templates/explore/repo_search.tmpl  |   2 +-
 templates/explore/search.tmpl       |   2 +-
 templates/org/menu.tmpl             |  13 ++--
 templates/repo/search.tmpl          |   6 +-
 templates/user/code.tmpl            |  25 ++++++++
 templates/user/overview/header.tmpl |   7 +-
 templates/user/profile.tmpl         |   7 +-
 19 files changed, 341 insertions(+), 145 deletions(-)
 create mode 100644 routers/web/user/code.go
 create mode 100644 templates/code/searchform.tmpl
 create mode 100644 templates/code/searchresults.tmpl
 create mode 100644 templates/user/code.tmpl

diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index ee72dc6ee7..cc524a417e 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -593,6 +593,16 @@ func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c
 	return sess, count, nil
 }
 
+// SearchRepositoryIDsByCondition search repository IDs by given condition.
+func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]int64, error) {
+	repoIDs := make([]int64, 0, 10)
+	return repoIDs, db.GetEngine(ctx).
+		Table("repository").
+		Cols("id").
+		Where(cond).
+		Find(&repoIDs)
+}
+
 // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
 func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
 	cond := builder.NewCond()
@@ -680,16 +690,16 @@ func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder {
 }
 
 // FindUserCodeAccessibleRepoIDs finds all at Code level accessible repositories' ID by the user's id
-func FindUserCodeAccessibleRepoIDs(user *user_model.User) ([]int64, error) {
-	repoIDs := make([]int64, 0, 10)
-	if err := db.GetEngine(db.DefaultContext).
-		Table("repository").
-		Cols("id").
-		Where(AccessibleRepositoryCondition(user, unit.TypeCode)).
-		Find(&repoIDs); err != nil {
-		return nil, fmt.Errorf("FindUserCodeAccesibleRepoIDs: %v", err)
-	}
-	return repoIDs, nil
+func FindUserCodeAccessibleRepoIDs(ctx context.Context, user *user_model.User) ([]int64, error) {
+	return SearchRepositoryIDsByCondition(ctx, AccessibleRepositoryCondition(user, unit.TypeCode))
+}
+
+// FindUserCodeAccessibleOwnerRepoIDs finds all repository IDs for the given owner whose code the user can see.
+func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user *user_model.User) ([]int64, error) {
+	return SearchRepositoryIDsByCondition(ctx, builder.NewCond().And(
+		builder.Eq{"owner_id": ownerID},
+		AccessibleRepositoryCondition(user, unit.TypeCode),
+	))
 }
 
 // GetUserRepositories returns a list of repositories of given user.
diff --git a/modules/context/org.go b/modules/context/org.go
index d020befa40..89260b8654 100644
--- a/modules/context/org.go
+++ b/modules/context/org.go
@@ -130,6 +130,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
 	ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
 	ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
 	ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["IsPublicMember"] = func(uid int64) bool {
 		is, _ := organization.IsPublicMembership(ctx.Org.Organization.ID, uid)
 		return is
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1dba1d71d8..fbf9b70643 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -268,8 +268,11 @@ users = Users
 organizations = Organizations
 search = Search
 code = Code
+search.type.tooltip = Search type
 search.fuzzy = Fuzzy
+search.fuzzy.tooltip = Include results that also matches the search term closely
 search.match = Match
+search.match.tooltip = Include only results that matches the exact search term
 code_search_unavailable = Currently code search is not available. Please contact your site administrator.
 repo_no_results = No matching repositories found.
 user_no_results = No matching users found.
@@ -507,6 +510,7 @@ activity = Public Activity
 followers = Followers
 starred = Starred Repositories
 watched = Watched Repositories
+code = Code
 projects = Projects
 following = Following
 follow = Follow
@@ -1763,8 +1767,11 @@ activity.git_stats_deletion_n = %d deletions
 
 search = Search
 search.search_repo = Search repository
+search.type.tooltip = Search type
 search.fuzzy = Fuzzy
+search.fuzzy.tooltip = Include results that also matches the search term closely
 search.match = Match
+search.match.tooltip = Include only results that matches the exact search term
 search.results = Search results for "%s" in <a href="%s">%s</a>
 search.code_no_results = No source code matching your search term found.
 search.code_search_unavailable = Currently code search is not available. Please contact your site administrator.
@@ -2310,6 +2317,7 @@ create_org = Create Organization
 repo_updated = Updated
 people = People
 teams = Teams
+code = Code
 lower_members = members
 lower_repositories = repositories
 create_new_team = New Team
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 3afb2110d9..2357b34fd0 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -34,86 +34,91 @@ func Code(ctx *context.Context) {
 
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
+
+	queryType := ctx.FormTrim("t")
+	isMatch := queryType == "match"
+
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Language"] = language
+	ctx.Data["queryType"] = queryType
+	ctx.Data["PageIsViewCode"] = true
+
+	if keyword == "" {
+		ctx.HTML(http.StatusOK, tplExploreCode)
+		return
+	}
+
 	page := ctx.FormInt("page")
 	if page <= 0 {
 		page = 1
 	}
 
-	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	var (
+		repoIDs []int64
+		err     error
+		isAdmin bool
+	)
+	if ctx.Doer != nil {
+		isAdmin = ctx.Doer.IsAdmin
+	}
 
-	if keyword != "" {
-		var (
-			repoIDs []int64
-			err     error
-			isAdmin bool
-		)
-		if ctx.Doer != nil {
-			isAdmin = ctx.Doer.IsAdmin
+	// guest user or non-admin user
+	if ctx.Doer == nil || !isAdmin {
+		repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
+		if err != nil {
+			ctx.ServerError("FindUserCodeAccessibleRepoIDs", err)
+			return
 		}
+	}
 
-		// guest user or non-admin user
-		if ctx.Doer == nil || !isAdmin {
-			repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx.Doer)
-			if err != nil {
+	var (
+		total                 int
+		searchResults         []*code_indexer.Result
+		searchResultLanguages []*code_indexer.SearchResultLanguages
+	)
+
+	if (len(repoIDs) > 0) || isAdmin {
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		if err != nil {
+			if code_indexer.IsAvailable() {
 				ctx.ServerError("SearchResults", err)
 				return
 			}
+			ctx.Data["CodeIndexerUnavailable"] = true
+		} else {
+			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
 		}
 
-		var (
-			total                 int
-			searchResults         []*code_indexer.Result
-			searchResultLanguages []*code_indexer.SearchResultLanguages
-		)
-
-		if (len(repoIDs) > 0) || isAdmin {
-			total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-			if err != nil {
-				if code_indexer.IsAvailable() {
-					ctx.ServerError("SearchResults", err)
-					return
+		loadRepoIDs := make([]int64, 0, len(searchResults))
+		for _, result := range searchResults {
+			var find bool
+			for _, id := range loadRepoIDs {
+				if id == result.RepoID {
+					find = true
+					break
 				}
-				ctx.Data["CodeIndexerUnavailable"] = true
-			} else {
-				ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
 			}
-
-			loadRepoIDs := make([]int64, 0, len(searchResults))
-			for _, result := range searchResults {
-				var find bool
-				for _, id := range loadRepoIDs {
-					if id == result.RepoID {
-						find = true
-						break
-					}
-				}
-				if !find {
-					loadRepoIDs = append(loadRepoIDs, result.RepoID)
-				}
-			}
-
-			repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs)
-			if err != nil {
-				ctx.ServerError("SearchResults", err)
-				return
+			if !find {
+				loadRepoIDs = append(loadRepoIDs, result.RepoID)
 			}
+		}
 
-			ctx.Data["RepoMaps"] = repoMaps
+		repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs)
+		if err != nil {
+			ctx.ServerError("GetRepositoriesMapByIDs", err)
+			return
 		}
 
-		ctx.Data["Keyword"] = keyword
-		ctx.Data["Language"] = language
-		ctx.Data["queryType"] = queryType
-		ctx.Data["SearchResults"] = searchResults
-		ctx.Data["SearchResultLanguages"] = searchResultLanguages
-		ctx.Data["PageIsViewCode"] = true
-
-		pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
-		pager.SetDefaultParams(ctx)
-		pager.AddParam(ctx, "l", "Language")
-		ctx.Data["Page"] = pager
+		ctx.Data["RepoMaps"] = repoMaps
 	}
 
+	ctx.Data["SearchResults"] = searchResults
+	ctx.Data["SearchResultLanguages"] = searchResultLanguages
+
+	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+	pager.SetDefaultParams(ctx)
+	pager.AddParam(ctx, "l", "Language")
+	ctx.Data["Page"] = pager
+
 	ctx.HTML(http.StatusOK, tplExploreCode)
 }
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 8f141cb149..3d1835c7c3 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -21,14 +21,27 @@ func Search(ctx *context.Context) {
 		ctx.Redirect(ctx.Repo.RepoLink)
 		return
 	}
+
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
+
+	queryType := ctx.FormTrim("t")
+	isMatch := queryType == "match"
+
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Language"] = language
+	ctx.Data["queryType"] = queryType
+	ctx.Data["PageIsViewCode"] = true
+
+	if keyword == "" {
+		ctx.HTML(http.StatusOK, tplSearch)
+		return
+	}
+
 	page := ctx.FormInt("page")
 	if page <= 0 {
 		page = 1
 	}
-	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
 
 	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
 		language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
@@ -41,13 +54,10 @@ func Search(ctx *context.Context) {
 	} else {
 		ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
 	}
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+
 	ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
 	ctx.Data["SearchResults"] = searchResults
 	ctx.Data["SearchResultLanguages"] = searchResultLanguages
-	ctx.Data["PageIsViewCode"] = true
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
new file mode 100644
index 0000000000..89bd23588b
--- /dev/null
+++ b/routers/web/user/code.go
@@ -0,0 +1,114 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package user
+
+import (
+	"net/http"
+
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	code_indexer "code.gitea.io/gitea/modules/indexer/code"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+const (
+	tplUserCode base.TplName = "user/code"
+)
+
+// CodeSearch render user/organization code search page
+func CodeSearch(ctx *context.Context) {
+	if !setting.Indexer.RepoIndexerEnabled {
+		ctx.Redirect(ctx.ContextUser.HomeLink())
+		return
+	}
+
+	ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	ctx.Data["Title"] = ctx.Tr("code.title")
+	ctx.Data["ContextUser"] = ctx.ContextUser
+
+	language := ctx.FormTrim("l")
+	keyword := ctx.FormTrim("q")
+
+	queryType := ctx.FormTrim("t")
+	isMatch := queryType == "match"
+
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Language"] = language
+	ctx.Data["queryType"] = queryType
+	ctx.Data["IsCodePage"] = true
+
+	if keyword == "" {
+		ctx.HTML(http.StatusOK, tplUserCode)
+		return
+	}
+
+	var (
+		repoIDs []int64
+		err     error
+	)
+
+	page := ctx.FormInt("page")
+	if page <= 0 {
+		page = 1
+	}
+
+	repoIDs, err = repo_model.FindUserCodeAccessibleOwnerRepoIDs(ctx, ctx.ContextUser.ID, ctx.Doer)
+	if err != nil {
+		ctx.ServerError("FindUserCodeAccessibleOwnerRepoIDs", err)
+		return
+	}
+
+	var (
+		total                 int
+		searchResults         []*code_indexer.Result
+		searchResultLanguages []*code_indexer.SearchResultLanguages
+	)
+
+	if len(repoIDs) > 0 {
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		if err != nil {
+			if code_indexer.IsAvailable() {
+				ctx.ServerError("SearchResults", err)
+				return
+			}
+			ctx.Data["CodeIndexerUnavailable"] = true
+		} else {
+			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
+		}
+
+		loadRepoIDs := make([]int64, 0, len(searchResults))
+		for _, result := range searchResults {
+			var find bool
+			for _, id := range loadRepoIDs {
+				if id == result.RepoID {
+					find = true
+					break
+				}
+			}
+			if !find {
+				loadRepoIDs = append(loadRepoIDs, result.RepoID)
+			}
+		}
+
+		repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs)
+		if err != nil {
+			ctx.ServerError("GetRepositoriesMapByIDs", err)
+			return
+		}
+
+		ctx.Data["RepoMaps"] = repoMaps
+	}
+	ctx.Data["SearchResults"] = searchResults
+	ctx.Data["SearchResultLanguages"] = searchResultLanguages
+
+	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+	pager.SetDefaultParams(ctx)
+	pager.AddParam(ctx, "l", "Language")
+	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, tplUserCode)
+}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 20d8e32d29..c72592e728 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -86,6 +86,7 @@ func ListPackages(ctx *context.Context) {
 
 	ctx.Data["Title"] = ctx.Tr("packages.title")
 	ctx.Data["IsPackagesPage"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["Query"] = query
 	ctx.Data["PackageType"] = packageType
@@ -157,6 +158,7 @@ func ViewPackageVersion(ctx *context.Context) {
 
 	ctx.Data["Title"] = pd.Package.Name
 	ctx.Data["IsPackagesPage"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = pd
 
@@ -234,6 +236,7 @@ func ListPackageVersions(ctx *context.Context) {
 
 	ctx.Data["Title"] = ctx.Tr("packages.title")
 	ctx.Data["IsPackagesPage"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{
 		Package: p,
@@ -305,6 +308,7 @@ func PackageSettings(ctx *context.Context) {
 
 	ctx.Data["Title"] = pd.Package.Name
 	ctx.Data["IsPackagesPage"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["ContextUser"] = ctx.ContextUser
 	ctx.Data["PackageDescriptor"] = pd
 
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index a3452fd692..6e16b377db 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -290,6 +290,7 @@ func Profile(ctx *context.Context) {
 	}
 	ctx.Data["Page"] = pager
 	ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
 	ctx.Data["ShowUserEmail"] = len(ctx.ContextUser.Email) > 0 && ctx.IsSigned && (!ctx.ContextUser.KeepEmailPrivate || ctx.ContextUser.ID == ctx.Doer.ID)
 
diff --git a/routers/web/web.go b/routers/web/web.go
index 656cd52b54..c74343c8cf 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -754,6 +754,7 @@ func RegisterRoutes(m *web.Route) {
 				})
 			}, ignSignIn, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
 		}
+		m.Get("/code", user.CodeSearch)
 	}, context_service.UserAssignmentWeb())
 
 	// ***** Release Attachment Download without Signin
diff --git a/templates/code/searchform.tmpl b/templates/code/searchform.tmpl
new file mode 100644
index 0000000000..26611cf4b2
--- /dev/null
+++ b/templates/code/searchform.tmpl
@@ -0,0 +1,14 @@
+<form class="ui form ignore-dirty" style="max-width: 100%">
+	<div class="ui fluid action input">
+		<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable }} disabled{{end}} placeholder="{{.locale.Tr "explore.search"}}…" autofocus>
+		<div class="ui dropdown selection tooltip{{if .CodeIndexerUnavailable }} disabled{{end}}" data-content="{{.locale.Tr "explore.search.type.tooltip"}}">
+			<input name="t" type="hidden" value="{{.queryType}}"{{if .CodeIndexerUnavailable }} disabled{{end}}>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="text">{{.locale.Tr (printf "explore.search.%s" (or .queryType "fuzzy"))}}</div>
+			<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
+				<div class="item tooltip" data-value="" data-content="{{.locale.Tr "explore.search.fuzzy.tooltip"}}">{{.locale.Tr "explore.search.fuzzy"}}</div>
+				<div class="item tooltip" data-value="match" data-content="{{.locale.Tr "explore.search.match.tooltip"}}">{{.locale.Tr "explore.search.match"}}</div>
+			</div>
+		</div>
+		<button class="ui primary button"{{if .CodeIndexerUnavailable }} disabled{{end}}>{{.locale.Tr "explore.search"}}</button>
+	</div>
+</form>
diff --git a/templates/code/searchresults.tmpl b/templates/code/searchresults.tmpl
new file mode 100644
index 0000000000..e21a50e1f1
--- /dev/null
+++ b/templates/code/searchresults.tmpl
@@ -0,0 +1,43 @@
+<div class="df ac fw">
+	{{range $term := .SearchResultLanguages}}
+	<a class="ui text-label df ac mr-1 my-1 {{if eq $.Language $term.Language}}primary {{end}}basic label" href="{{AppSubUrl}}{{if $.ContextUser}}/{{$.ContextUser.Name}}/-/code{{else}}/explore/code{{end}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
+		<i class="color-icon mr-3" style="background-color: {{$term.Color}}"></i>
+		{{$term.Language}}
+		<div class="detail">{{$term.Count}}</div>
+	</a>
+	{{end}}
+</div>
+<div class="repository search">
+	{{range $result := .SearchResults}}
+		{{$repo := (index $.RepoMaps .RepoID)}}
+		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
+			<h4 class="ui top attached normal header">
+				<span class="file">
+					<a rel="nofollow" href="{{$repo.HTMLURL}}">{{$repo.FullName}}</a>
+						{{if $repo.IsArchived}}
+							<span class="ui basic label">{{$.locale.Tr "repo.desc.archived"}}</span>
+						{{end}}
+					- {{.Filename}}
+				</span>
+				<a class="ui basic tiny button" rel="nofollow" href="{{$repo.HTMLURL}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{$.locale.Tr "repo.diff.view_file"}}</a>
+			</h4>
+			<div class="ui attached table segment">
+				<div class="file-body file-code code-view">
+					<table>
+						<tbody>
+							<tr>
+								<td class="lines-num">
+									{{range .LineNumbers}}
+										<a href="{{$repo.HTMLURL}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
+									{{end}}
+								</td>
+								<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines | Safe}}</code></td>
+							</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+			{{template "shared/searchbottom" dict "root" $ "result" .}}
+		</div>
+	{{end}}
+</div>
diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl
index b6429b1919..f4e46d1198 100644
--- a/templates/explore/code.tmpl
+++ b/templates/explore/code.tmpl
@@ -2,20 +2,7 @@
 <div class="page-content explore users">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		<form class="ui form ignore-dirty" style="max-width: 100%">
-			<div class="ui fluid action input">
-				<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{.locale.Tr "explore.search"}}..." autofocus>
-				<div class="ui dropdown selection{{if .CodeIndexerUnavailable}} disabled{{end}}">
-					<input name="t" type="hidden" value="{{.queryType}}"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-					<div class="text">{{.locale.Tr (printf "explore.search.%s" (or .queryType "fuzzy"))}}</div>
-					<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
-						<div class="item" data-value="">{{.locale.Tr "explore.search.fuzzy"}}</div>
-						<div class="item" data-value="match">{{.locale.Tr "explore.search.match"}}</div>
-					</div>
-				</div>
-				<button class="ui primary button"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{.locale.Tr "explore.search"}}</button>
-			</div>
-		</form>
+		{{template "code/searchform" .}}
 		<div class="ui divider"></div>
 		<div class="ui user list">
 			{{if .CodeIndexerUnavailable}}
@@ -26,50 +13,8 @@
 				<h3>
 					{{.locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html}}
 				</h3>
-				<div class="df ac fw">
-					{{range $term := .SearchResultLanguages}}
-					<a class="ui text-label df ac mr-1 my-1 {{if eq $.Language $term.Language}}primary {{end}}basic label" href="{{AppSubUrl}}/explore/code?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
-						<i class="color-icon mr-3" style="background-color: {{$term.Color}}"></i>
-						{{$term.Language}}
-						<div class="detail">{{$term.Count}}</div>
-					</a>
-					{{end}}
-				</div>
-				<div class="repository search">
-					{{range $result := .SearchResults}}
-						{{$repo := (index $.RepoMaps .RepoID)}}
-						<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-							<h4 class="ui top attached normal header">
-								<span class="file">
-									<a rel="nofollow" href="{{$repo.HTMLURL}}">{{$repo.FullName}}</a>
-										{{if $repo.IsArchived}}
-											<span class="ui basic label">{{$.locale.Tr "repo.desc.archived"}}</span>
-										{{end}}
-									- {{.Filename}}
-								</span>
-								<a class="ui basic tiny button" rel="nofollow" href="{{$repo.HTMLURL}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{$.locale.Tr "repo.diff.view_file"}}</a>
-							</h4>
-							<div class="ui attached table segment">
-								<div class="file-body file-code code-view">
-									<table>
-										<tbody>
-											<tr>
-												<td class="lines-num">
-													{{range .LineNumbers}}
-														<a href="{{$repo.HTMLURL}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
-													{{end}}
-												</td>
-												<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines | Safe}}</code></td>
-											</tr>
-										</tbody>
-									</table>
-								</div>
-							</div>
-							{{template "shared/searchbottom" dict "root" $ "result" .}}
-						</div>
-					{{end}}
-				</div>
-			{{else}}
+				{{template "code/searchresults" .}}
+			{{else if .Keyword}}
 				<div>{{$.locale.Tr "explore.code_no_results"}}</div>
 			{{end}}
 		</div>
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
index 1dabfede8c..b784722f50 100644
--- a/templates/explore/repo_search.tmpl
+++ b/templates/explore/repo_search.tmpl
@@ -25,7 +25,7 @@
 	<input type="hidden" name="sort" value="{{$.SortType}}">
 	<input type="hidden" name="language" value="{{$.Language}}">
 	<div class="ui fluid action input">
-		<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}..." autofocus>
+		<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}…" autofocus>
 		<button class="ui primary button">{{.locale.Tr "explore.search"}}</button>
 	</div>
 </form>
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 7e10412db8..123efa56dd 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -17,7 +17,7 @@
 </div>
 <form class="ui form ignore-dirty" style="max-width: 90%">
 	<div class="ui fluid action input">
-		<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}..." autofocus>
+		<input name="q" value="{{.Keyword}}" placeholder="{{.locale.Tr "explore.search"}}…" autofocus>
 		<button class="ui primary button">{{.locale.Tr "explore.search"}}</button>
 	</div>
 </form>
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index f86570d47b..87242b94d3 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -1,6 +1,6 @@
 <div class="ui tabs container">
 	<div class="ui secondary stackable pointing menu">
-		<a class="{{if .PageIsViewRepositories}}active{{end}} item" href="{{$.Org.HomeLink}}">
+		<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
 			{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
 		</a>
 		{{if .IsPackageEnabled}}
@@ -8,14 +8,19 @@
 			{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
 		</a>
 		{{end}}
+		{{if .IsRepoIndexerEnabled}}
+		<a class="{{if $.PageIsOrgCode}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
+			{{svg "octicon-code"}}&nbsp;{{$.locale.Tr "org.code"}}
+		</a>
+		{{end}}
 		{{if .IsOrganizationMember}}
-			<a class="{{if $.PageIsOrgMembers}}active{{end}} item" href="{{$.OrgLink}}/members">
+			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
 				{{svg "octicon-organization"}}&nbsp;{{$.locale.Tr "org.people"}}
 				{{if .NumMembers}}
 					<div class="ui primary label">{{.NumMembers}}</div>
 				{{end}}
 			</a>
-			<a class="{{if $.PageIsOrgTeams}}active{{end}} item" href="{{$.OrgLink}}/teams">
+			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
 				{{svg "octicon-people"}}&nbsp;{{$.locale.Tr "org.teams"}}
 				{{if .NumTeams}}
 					<div class="ui primary label">{{.NumTeams}}</div>
@@ -25,7 +30,7 @@
 
 		{{if .IsOrganizationOwner}}
 			<div class="right menu">
-				<a class="{{if .PageIsOrgSettings}}active{{end}} item" href="{{.OrgLink}}/settings">
+				<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
 				{{svg "octicon-tools"}} {{.locale.Tr "repo.settings"}}
 				</a>
 			</div>
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index 9d8845443e..c68d20cdd2 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -6,12 +6,12 @@
 			<form class="ui form ignore-dirty" method="get">
 				<div class="ui fluid action input">
 					<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{.locale.Tr "repo.search.search_repo"}}">
-					<div class="ui dropdown selection{{if .CodeIndexerUnavailable}} disabled{{end}}">
+					<div class="ui dropdown selection tooltip{{if .CodeIndexerUnavailable}} disabled{{end}}" data-content="{{.locale.Tr "repo.search.type.tooltip"}}">
 						<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="text">{{.locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
 						<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
-							<div class="item" data-value="">{{.locale.Tr "repo.search.fuzzy"}}</div>
-							<div class="item" data-value="match">{{.locale.Tr "repo.search.match"}}</div>
+							<div class="item tooltip" data-value="" data-content="{{.locale.Tr "repo.search.fuzzy.tooltip"}}">{{.locale.Tr "repo.search.fuzzy"}}</div>
+							<div class="item tooltip" data-value="match" data-content="{{.locale.Tr "repo.search.match.tooltip"}}">{{.locale.Tr "repo.search.match"}}</div>
 						</div>
 					</div>
 					<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
diff --git a/templates/user/code.tmpl b/templates/user/code.tmpl
new file mode 100644
index 0000000000..730d4dc9eb
--- /dev/null
+++ b/templates/user/code.tmpl
@@ -0,0 +1,25 @@
+{{template "base/head" .}}
+<div class="page-content repository code-search">
+	{{template "user/overview/header" .}}
+	<div class="ui container">
+		{{template "code/searchform" .}}
+		<div class="ui divider"></div>
+		<div class="ui user list">
+			{{if .CodeIndexerUnavailable }}
+				<div class="ui error message">
+					<p>{{$.locale.Tr "explore.code_search_unavailable"}}</p>
+				</div>
+			{{else if .SearchResults}}
+				<h3>
+					{{.locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html }}
+				</h3>
+				{{template "code/searchresults" .}}
+			{{else if .Keyword}}
+				<div>{{$.locale.Tr "explore.code_no_results"}}</div>
+			{{end}}
+		</div>
+
+		{{template "base/paginate" .}}
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl
index b61883b0c1..f007973607 100644
--- a/templates/user/overview/header.tmpl
+++ b/templates/user/overview/header.tmpl
@@ -23,10 +23,15 @@
 				{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
 			</a>
 			{{if (not .UnitPackagesGlobalDisabled)}}
-				<a href="{{.ContextUser.HTMLURL}}/-/packages" class="{{if .IsPackagesPage}}active{{end}} item">
+				<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
 					{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
 				</a>
 			{{end}}
+			{{if .IsRepoIndexerEnabled}}
+				<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
+					{{svg "octicon-code"}} {{.locale.Tr "user.code"}}
+				</a>
+			{{end}}
 
 			{{if .ContextUser.IsOrganization}}
 				{{if .IsOrganizationMember}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 7644872f09..a4ac65db6a 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -101,7 +101,7 @@
 			</div>
 			<div class="ui eleven wide column">
 				<div class="ui secondary stackable pointing tight menu">
-					<a class='{{if and (ne .TabName "activity") (ne .TabName "following") (ne .TabName "followers") (ne .TabName "stars") (ne .TabName "watching") (ne .TabName "projects")}}active{{end}} item' href="{{.Owner.HomeLink}}">
+					<a class='{{if and (ne .TabName "activity") (ne .TabName "following") (ne .TabName "followers") (ne .TabName "stars") (ne .TabName "watching") (ne .TabName "projects") (ne .TabName "code")}}active{{end}} item' href="{{.Owner.HomeLink}}">
 						{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
 					</a>
 					{{if .IsPackageEnabled}}
@@ -109,6 +109,11 @@
 						{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
 					</a>
 					{{end}}
+					{{if .IsRepoIndexerEnabled}}
+					<a class='{{if eq .TabName "code"}}active{{end}} item' href="{{.Owner.HomeLink}}/-/code">
+						{{svg "octicon-code"}} {{.locale.Tr "user.code"}}
+					</a>
+					{{end}}
 					<a class='{{if eq .TabName "activity"}}active{{end}} item' href="{{.Owner.HomeLink}}?tab=activity">
 						{{svg "octicon-rss"}} {{.locale.Tr "user.activity"}}
 					</a>
-- 
cgit v1.2.3