mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
feat: add GenericSearchResult interface, and re-implement commit and diff search using it (#998)
This adds the generic search result interface, which is implemented by all search result types except FileMatches. This makes diff, commit, and repository search results implement the GenericSearchResult interface, and includes the corresponding front end changes to use only the fields in GenericSearchResultInterface when displaying results.
This commit is contained in:
parent
f4e5b5a63e
commit
b5e695eea1
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
@ -53,6 +54,21 @@ func (*schemaResolver) RenderMarkdown(args *struct {
|
||||
return markdown.Render(args.Markdown, nil)
|
||||
}
|
||||
|
||||
func (*schemaResolver) HighlightCode(ctx context.Context, args *struct {
|
||||
Code string
|
||||
FuzzyLanguage string
|
||||
DisableTimeout bool
|
||||
IsLightTheme bool
|
||||
}) (string, error) {
|
||||
language := highlight.SyntectLanguageMap[strings.ToLower(args.FuzzyLanguage)]
|
||||
filePath := "file." + language
|
||||
html, _, err := highlight.Code(ctx, []byte(args.Code), filePath, args.DisableTimeout, args.IsLightTheme)
|
||||
if err != nil {
|
||||
return args.Code, err
|
||||
}
|
||||
return string(html), nil
|
||||
}
|
||||
|
||||
func (r *gitTreeEntryResolver) Binary(ctx context.Context) (bool, error) {
|
||||
content, err := r.Content(ctx)
|
||||
if err != nil {
|
||||
|
||||
19
cmd/frontend/graphqlbackend/markdown.go
Normal file
19
cmd/frontend/graphqlbackend/markdown.go
Normal file
@ -0,0 +1,19 @@
|
||||
package graphqlbackend
|
||||
|
||||
import "github.com/sourcegraph/sourcegraph/pkg/markdown"
|
||||
|
||||
type markdownResolver struct {
|
||||
text string
|
||||
}
|
||||
|
||||
func (m *markdownResolver) Text() string {
|
||||
return m.text
|
||||
}
|
||||
|
||||
func (m *markdownResolver) HTML() string {
|
||||
html, err := markdown.Render(m.text, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return html
|
||||
}
|
||||
@ -28,6 +28,8 @@ import (
|
||||
type repositoryResolver struct {
|
||||
repo *types.Repo
|
||||
redirectURL *string
|
||||
icon string
|
||||
matches []*searchResultMatchResolver
|
||||
}
|
||||
|
||||
func repositoryByID(ctx context.Context, id graphql.ID) (*repositoryResolver, error) {
|
||||
@ -197,6 +199,22 @@ func (r *repositoryResolver) ExternalURLs(ctx context.Context) ([]*externallink.
|
||||
return externallink.Repository(ctx, r.repo)
|
||||
}
|
||||
|
||||
func (r *repositoryResolver) Icon() string {
|
||||
return r.icon
|
||||
}
|
||||
func (r *repositoryResolver) Label() (*markdownResolver, error) {
|
||||
text := "[" + string(r.repo.Name) + "](/" + string(r.repo.Name) + ")"
|
||||
return &markdownResolver{text: text}, nil
|
||||
}
|
||||
|
||||
func (r *repositoryResolver) Detail() *markdownResolver {
|
||||
return &markdownResolver{text: "Repository name match"}
|
||||
}
|
||||
|
||||
func (r *repositoryResolver) Matches() []*searchResultMatchResolver {
|
||||
return r.matches
|
||||
}
|
||||
|
||||
func (*schemaResolver) AddPhabricatorRepo(ctx context.Context, args *struct {
|
||||
Callsign string
|
||||
Name *string
|
||||
|
||||
60
cmd/frontend/graphqlbackend/schema.go
generated
60
cmd/frontend/graphqlbackend/schema.go
generated
@ -762,6 +762,8 @@ type Query {
|
||||
# Renders Markdown to HTML. The returned HTML is already sanitized and
|
||||
# escaped and thus is always safe to render.
|
||||
renderMarkdown(markdown: String!, options: MarkdownOptions): String!
|
||||
# EXPERIMENTAL: Syntax highlights a code string.
|
||||
highlightCode(code: String!, fuzzyLanguage: String!, disableTimeout: Boolean!, isLightTheme: Boolean!): String!
|
||||
# Looks up an instance of a type that implements SettingsSubject (i.e., something that has settings). This can
|
||||
# be a site (which has global settings), an organization, or a user.
|
||||
settingsSubject(id: ID!): SettingsSubject
|
||||
@ -831,6 +833,42 @@ type Search {
|
||||
# A search result.
|
||||
union SearchResult = FileMatch | CommitSearchResult | Repository
|
||||
|
||||
# An object representing a markdown string.
|
||||
type Markdown {
|
||||
# The raw markdown string.
|
||||
text: String!
|
||||
# HTML for the rendered markdown string, or null if there is no HTML representation provided.
|
||||
# If specified, clients should render this directly.
|
||||
html: String!
|
||||
}
|
||||
|
||||
# A search result. Every type of search result, except FileMatch, must implement this interface.
|
||||
interface GenericSearchResultInterface {
|
||||
# URL to an icon that is displayed with every search result.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# The URL of the result.
|
||||
url: String!
|
||||
# A markdown string that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# A list of matches in this search result.
|
||||
matches: [SearchResultMatch!]!
|
||||
}
|
||||
|
||||
# A match in a search result. Matches make up the body content of a search result.
|
||||
type SearchResultMatch {
|
||||
# URL for the individual result match.
|
||||
url: String!
|
||||
# A markdown string containing the preview contents of the result match.
|
||||
body: Markdown!
|
||||
# A list of highlights that specify locations of matches of the query in the body. Each highlight is
|
||||
# a line number, character offset, and length. Currently, highlights are only displayed on match bodies
|
||||
# that are code blocks. If the result body is a code block, exclude the markdown code fence lines in
|
||||
# the line and character count. Leave as an empty list if no highlights are available.
|
||||
highlights: [Highlight!]!
|
||||
}
|
||||
|
||||
# Search results.
|
||||
type SearchResults {
|
||||
# The results. Inside each SearchResult there may be multiple matches, e.g.
|
||||
@ -962,7 +1000,17 @@ type Diff {
|
||||
}
|
||||
|
||||
# A search result that is a Git commit.
|
||||
type CommitSearchResult {
|
||||
type CommitSearchResult implements GenericSearchResultInterface {
|
||||
# Base64 data uri to an icon.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# The URL of the result.
|
||||
url: String!
|
||||
# A markdown string of that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# The result previews of the result.
|
||||
matches: [SearchResultMatch!]!
|
||||
# The commit that matched the search query.
|
||||
commit: GitCommit!
|
||||
# The ref names of the commit.
|
||||
@ -1088,7 +1136,7 @@ type RepositoryConnection {
|
||||
}
|
||||
|
||||
# A repository is a Git source control repository that is mirrored from some origin code host.
|
||||
type Repository implements Node {
|
||||
type Repository implements Node & GenericSearchResultInterface {
|
||||
# The repository's unique ID.
|
||||
id: ID!
|
||||
# The repository's name, as a path with one or more components. It conventionally consists of
|
||||
@ -1197,6 +1245,14 @@ type Repository implements Node {
|
||||
redirectURL: String
|
||||
# Whether the viewer has admin privileges on this repository.
|
||||
viewerCanAdminister: Boolean!
|
||||
# Base64 data uri to an icon.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# A markdown string of that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# The result previews of the result.
|
||||
matches: [SearchResultMatch!]!
|
||||
}
|
||||
|
||||
# A URL to a resource on an external service, such as the URL to a repository on its external (origin) code host.
|
||||
|
||||
@ -769,6 +769,8 @@ type Query {
|
||||
# Renders Markdown to HTML. The returned HTML is already sanitized and
|
||||
# escaped and thus is always safe to render.
|
||||
renderMarkdown(markdown: String!, options: MarkdownOptions): String!
|
||||
# EXPERIMENTAL: Syntax highlights a code string.
|
||||
highlightCode(code: String!, fuzzyLanguage: String!, disableTimeout: Boolean!, isLightTheme: Boolean!): String!
|
||||
# Looks up an instance of a type that implements SettingsSubject (i.e., something that has settings). This can
|
||||
# be a site (which has global settings), an organization, or a user.
|
||||
settingsSubject(id: ID!): SettingsSubject
|
||||
@ -838,6 +840,42 @@ type Search {
|
||||
# A search result.
|
||||
union SearchResult = FileMatch | CommitSearchResult | Repository
|
||||
|
||||
# An object representing a markdown string.
|
||||
type Markdown {
|
||||
# The raw markdown string.
|
||||
text: String!
|
||||
# HTML for the rendered markdown string, or null if there is no HTML representation provided.
|
||||
# If specified, clients should render this directly.
|
||||
html: String!
|
||||
}
|
||||
|
||||
# A search result. Every type of search result, except FileMatch, must implement this interface.
|
||||
interface GenericSearchResultInterface {
|
||||
# URL to an icon that is displayed with every search result.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# The URL of the result.
|
||||
url: String!
|
||||
# A markdown string that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# A list of matches in this search result.
|
||||
matches: [SearchResultMatch!]!
|
||||
}
|
||||
|
||||
# A match in a search result. Matches make up the body content of a search result.
|
||||
type SearchResultMatch {
|
||||
# URL for the individual result match.
|
||||
url: String!
|
||||
# A markdown string containing the preview contents of the result match.
|
||||
body: Markdown!
|
||||
# A list of highlights that specify locations of matches of the query in the body. Each highlight is
|
||||
# a line number, character offset, and length. Currently, highlights are only displayed on match bodies
|
||||
# that are code blocks. If the result body is a code block, exclude the markdown code fence lines in
|
||||
# the line and character count. Leave as an empty list if no highlights are available.
|
||||
highlights: [Highlight!]!
|
||||
}
|
||||
|
||||
# Search results.
|
||||
type SearchResults {
|
||||
# The results. Inside each SearchResult there may be multiple matches, e.g.
|
||||
@ -969,7 +1007,17 @@ type Diff {
|
||||
}
|
||||
|
||||
# A search result that is a Git commit.
|
||||
type CommitSearchResult {
|
||||
type CommitSearchResult implements GenericSearchResultInterface {
|
||||
# Base64 data uri to an icon.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# The URL of the result.
|
||||
url: String!
|
||||
# A markdown string of that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# The result previews of the result.
|
||||
matches: [SearchResultMatch!]!
|
||||
# The commit that matched the search query.
|
||||
commit: GitCommit!
|
||||
# The ref names of the commit.
|
||||
@ -1095,7 +1143,7 @@ type RepositoryConnection {
|
||||
}
|
||||
|
||||
# A repository is a Git source control repository that is mirrored from some origin code host.
|
||||
type Repository implements Node {
|
||||
type Repository implements Node & GenericSearchResultInterface {
|
||||
# The repository's unique ID.
|
||||
id: ID!
|
||||
# The repository's name, as a path with one or more components. It conventionally consists of
|
||||
@ -1204,6 +1252,14 @@ type Repository implements Node {
|
||||
redirectURL: String
|
||||
# Whether the viewer has admin privileges on this repository.
|
||||
viewerCanAdminister: Boolean!
|
||||
# Base64 data uri to an icon.
|
||||
icon: String!
|
||||
# A markdown string that is rendered prominently.
|
||||
label: Markdown!
|
||||
# A markdown string of that is rendered less prominently.
|
||||
detail: Markdown!
|
||||
# The result previews of the result.
|
||||
matches: [SearchResultMatch!]!
|
||||
}
|
||||
|
||||
# A URL to a resource on an external service, such as the URL to a repository on its external (origin) code host.
|
||||
|
||||
@ -28,6 +28,11 @@ type commitSearchResultResolver struct {
|
||||
sourceRefs []*gitRefResolver
|
||||
messagePreview *highlightedString
|
||||
diffPreview *highlightedString
|
||||
icon string
|
||||
label string
|
||||
url string
|
||||
detail string
|
||||
matches []*searchResultMatchResolver
|
||||
}
|
||||
|
||||
func (r *commitSearchResultResolver) Commit() *gitCommitResolver { return r.commit }
|
||||
@ -35,6 +40,24 @@ func (r *commitSearchResultResolver) Refs() []*gitRefResolver { retur
|
||||
func (r *commitSearchResultResolver) SourceRefs() []*gitRefResolver { return r.sourceRefs }
|
||||
func (r *commitSearchResultResolver) MessagePreview() *highlightedString { return r.messagePreview }
|
||||
func (r *commitSearchResultResolver) DiffPreview() *highlightedString { return r.diffPreview }
|
||||
func (r *commitSearchResultResolver) Icon() string {
|
||||
return r.icon
|
||||
}
|
||||
func (r *commitSearchResultResolver) Label() *markdownResolver {
|
||||
return &markdownResolver{text: r.label}
|
||||
}
|
||||
|
||||
func (r *commitSearchResultResolver) URL() string {
|
||||
return r.url
|
||||
}
|
||||
|
||||
func (r *commitSearchResultResolver) Detail() *markdownResolver {
|
||||
return &markdownResolver{text: r.detail}
|
||||
}
|
||||
|
||||
func (r *commitSearchResultResolver) Matches() []*searchResultMatchResolver {
|
||||
return r.matches
|
||||
}
|
||||
|
||||
var mockSearchCommitDiffsInRepo func(ctx context.Context, repoRevs search.RepositoryRevisions, info *search.PatternInfo, query *query.Query) (results []*commitSearchResultResolver, limitHit, timedOut bool, err error)
|
||||
|
||||
@ -226,7 +249,8 @@ func searchCommitsInRepo(ctx context.Context, op commitSearchOp) (results []*com
|
||||
results = make([]*commitSearchResultResolver, len(rawResults))
|
||||
for i, rawResult := range rawResults {
|
||||
commit := rawResult.Commit
|
||||
results[i] = &commitSearchResultResolver{commit: toGitCommitResolver(repoResolver, &commit)}
|
||||
commitResolver := toGitCommitResolver(repoResolver, &commit)
|
||||
results[i] = &commitSearchResultResolver{commit: commitResolver}
|
||||
|
||||
addRefs := func(dst *[]*gitRefResolver, src []string) {
|
||||
for _, ref := range src {
|
||||
@ -238,7 +262,8 @@ func searchCommitsInRepo(ctx context.Context, op commitSearchOp) (results []*com
|
||||
}
|
||||
addRefs(&results[i].refs, rawResult.Refs)
|
||||
addRefs(&results[i].sourceRefs, rawResult.SourceRefs)
|
||||
|
||||
var matchBody string
|
||||
var highlights []*highlightedRange
|
||||
// TODO(sqs): properly combine message: and term values for type:commit searches
|
||||
if !op.diff {
|
||||
var patString string
|
||||
@ -250,20 +275,111 @@ func searchCommitsInRepo(ctx context.Context, op commitSearchOp) (results []*com
|
||||
pat, err := regexp.Compile(patString)
|
||||
if err == nil {
|
||||
results[i].messagePreview = highlightMatches(pat, []byte(commit.Message))
|
||||
highlights = results[i].messagePreview.highlights
|
||||
}
|
||||
} else {
|
||||
results[i].messagePreview = &highlightedString{value: string(commit.Message)}
|
||||
}
|
||||
matchBody = "```COMMIT_EDITMSG\n" + rawResult.Commit.Message + "\n```"
|
||||
}
|
||||
|
||||
if rawResult.Diff != nil && op.diff {
|
||||
highlights = fromVCSHighlights(rawResult.DiffHighlights)
|
||||
results[i].diffPreview = &highlightedString{
|
||||
value: rawResult.Diff.Raw,
|
||||
highlights: fromVCSHighlights(rawResult.DiffHighlights),
|
||||
highlights: highlights,
|
||||
}
|
||||
matchBody = "```diff\n" + cleanDiffPreview(highlights, rawResult.Diff.Raw) + "```"
|
||||
}
|
||||
|
||||
commitIcon := "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE3LDEyQzE3LDE0LjQyIDE1LjI4LDE2LjQ0IDEzLDE2LjlWMjFIMTFWMTYuOUM4LjcyLDE2LjQ0IDcsMTQuNDIgNywxMkM3LDkuNTggOC43Miw3LjU2IDExLDcuMVYzSDEzVjcuMUMxNS4yOCw3LjU2IDE3LDkuNTggMTcsMTJNMTIsOUEzLDMgMCAwLDAgOSwxMkEzLDMgMCAwLDAgMTIsMTVBMywzIDAgMCwwIDE1LDEyQTMsMyAwIDAsMCAxMiw5WiIgLz48L3N2Zz4="
|
||||
results[i].label = createLabel(rawResult, commitResolver)
|
||||
if len(rawResult.Commit.ID) > 7 {
|
||||
results[i].detail = string(rawResult.Commit.ID)[:7]
|
||||
} else {
|
||||
results[i].detail = string(rawResult.Commit.ID)
|
||||
}
|
||||
results[i].url = commitResolver.URL()
|
||||
results[i].icon = commitIcon
|
||||
match := &searchResultMatchResolver{body: matchBody, highlights: highlights, url: commitResolver.URL()}
|
||||
matches := []*searchResultMatchResolver{match}
|
||||
results[i].matches = matches
|
||||
}
|
||||
|
||||
return results, limitHit, timedOut, nil
|
||||
}
|
||||
|
||||
func cleanDiffPreview(highlights []*highlightedRange, rawDiffResult string) string {
|
||||
// A map of line number to number of lines that have been ignored before the particular line number.
|
||||
var lineByCountIgnored = make(map[int]int32)
|
||||
// The line numbers of lines that were ignored.
|
||||
var ignoredLineNumbers = make(map[int]bool)
|
||||
|
||||
lines := strings.Split(rawDiffResult, "\n")
|
||||
var finalLines []string
|
||||
ignoreUntilAtAt := false
|
||||
var countIgnored int32
|
||||
for i, line := range lines {
|
||||
// ignore index, ---file, and +++file lines
|
||||
if ignoreUntilAtAt && !strings.HasPrefix(line, "@@ ") {
|
||||
ignoredLineNumbers[i] = true
|
||||
countIgnored++
|
||||
continue
|
||||
} else {
|
||||
ignoreUntilAtAt = false
|
||||
}
|
||||
if strings.HasPrefix(line, "diff ") {
|
||||
ignoreUntilAtAt = true
|
||||
lineByCountIgnored[i] = countIgnored
|
||||
l := strings.Replace(line, "diff --git ", "", 1)
|
||||
finalLines = append(finalLines, l)
|
||||
} else {
|
||||
lineByCountIgnored[i] = countIgnored
|
||||
finalLines = append(finalLines, line)
|
||||
}
|
||||
}
|
||||
return results, limitHit, timedOut, nil
|
||||
|
||||
for n := range highlights {
|
||||
// For each highlight, adjust the line number by the number of lines that were
|
||||
// ignored in the diff before.
|
||||
linesIgnored := lineByCountIgnored[int(highlights[n].line)]
|
||||
if ignoredLineNumbers[int(highlights[n].line)-1] {
|
||||
// Effectively remove highlights that were on ignored lines by setting
|
||||
// line to -1.
|
||||
highlights[n].line = -1
|
||||
}
|
||||
if linesIgnored > 0 {
|
||||
highlights[n].line = highlights[n].line - linesIgnored
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(finalLines, "\n")
|
||||
}
|
||||
|
||||
func createLabel(rawResult *git.LogCommitSearchResult, commitResolver *gitCommitResolver) string {
|
||||
message := commitSubject(rawResult.Commit.Message)
|
||||
author := rawResult.Commit.Author.Name
|
||||
repoName := displayRepoName(commitResolver.Repository().Name())
|
||||
repoURL := commitResolver.Repository().URL()
|
||||
url := commitResolver.URL()
|
||||
|
||||
return fmt.Sprintf("[%s](%s) [%s](%s) [%s](%s)", repoName, repoURL, author, url, message, url)
|
||||
}
|
||||
|
||||
func commitSubject(message string) string {
|
||||
idx := strings.Index(message, "\n")
|
||||
if idx != -1 {
|
||||
return message[:idx]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func displayRepoName(repoPath string) string {
|
||||
parts := strings.Split(repoPath, "/")
|
||||
if len(parts) >= 3 && strings.Contains(parts[0], ".") {
|
||||
parts = parts[1:] // remove hostname from repo path (reduce visual noise)
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func highlightMatches(pattern *regexp.Regexp, data []byte) *highlightedString {
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/db"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/pkg/search"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/pkg/search/query"
|
||||
@ -68,9 +69,14 @@ func TestSearchCommitsInRepo(t *testing.T) {
|
||||
author: *toSignatureResolver(&git.Signature{}),
|
||||
},
|
||||
diffPreview: &highlightedString{value: "x", highlights: []*highlightedRange{}},
|
||||
icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE3LDEyQzE3LDE0LjQyIDE1LjI4LDE2LjQ0IDEzLDE2LjlWMjFIMTFWMTYuOUM4LjcyLDE2LjQ0IDcsMTQuNDIgNywxMkM3LDkuNTggOC43Miw3LjU2IDExLDcuMVYzSDEzVjcuMUMxNS4yOCw3LjU2IDE3LDkuNTggMTcsMTJNMTIsOUEzLDMgMCAwLDAgOSwxMkEzLDMgMCAwLDAgMTIsMTVBMywzIDAgMCwwIDE1LDEyQTMsMyAwIDAsMCAxMiw5WiIgLz48L3N2Zz4=",
|
||||
label: "[repo](/repo) [](/repo/-/commit/c1) [](/repo/-/commit/c1)",
|
||||
url: "/repo/-/commit/c1",
|
||||
detail: "c1",
|
||||
matches: []*searchResultMatchResolver{&searchResultMatchResolver{url: "/repo/-/commit/c1", body: "```diff\nx```", highlights: []*highlightedRange{}}},
|
||||
},
|
||||
}; !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("results\ngot %v\nwant %v", results, want)
|
||||
t.Errorf("results\ngot %v\nwant %v\ndiff: %v", results, want, pretty.Compare(results, want))
|
||||
}
|
||||
if limitHit {
|
||||
t.Error("limitHit")
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var mockSearchRepositories func(args *search.Args) ([]*searchResultResolver, *searchResultsCommon, error)
|
||||
var repoIcon = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNjQgNjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDY0IDY0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+Cjx0aXRsZT5JY29ucyA0MDA8L3RpdGxlPgo8Zz4KCTxwYXRoIGQ9Ik0yMywyMi40YzEuMywwLDIuNC0xLjEsMi40LTIuNHMtMS4xLTIuNC0yLjQtMi40Yy0xLjMsMC0yLjQsMS4xLTIuNCwyLjRTMjEuNywyMi40LDIzLDIyLjR6Ii8+Cgk8cGF0aCBkPSJNMzUsMjYuNGMxLjMsMCwyLjQtMS4xLDIuNC0yLjRzLTEuMS0yLjQtMi40LTIuNHMtMi40LDEuMS0yLjQsMi40UzMzLjcsMjYuNCwzNSwyNi40eiIvPgoJPHBhdGggZD0iTTIzLDQyLjRjMS4zLDAsMi40LTEuMSwyLjQtMi40cy0xLjEtMi40LTIuNC0yLjRzLTIuNCwxLjEtMi40LDIuNFMyMS43LDQyLjQsMjMsNDIuNHoiLz4KCTxwYXRoIGQ9Ik01MCwxNmgtMS41Yy0wLjMsMC0wLjUsMC4yLTAuNSwwLjV2MzVjMCwwLjMtMC4yLDAuNS0wLjUsMC41aC0yN2MtMC41LDAtMS0wLjItMS40LTAuNmwtMC42LTAuNmMtMC4xLTAuMS0wLjEtMC4yLTAuMS0wLjQKCQljMC0wLjMsMC4yLTAuNSwwLjUtMC41SDQ0YzEuMSwwLDItMC45LDItMlYxMmMwLTEuMS0wLjktMi0yLTJIMTRjLTEuMSwwLTIsMC45LTIsMnYzNi4zYzAsMS4xLDAuNCwyLjEsMS4yLDIuOGwzLjEsMy4xCgkJYzEuMSwxLjEsMi43LDEuOCw0LjIsMS44SDUwYzEuMSwwLDItMC45LDItMlYxOEM1MiwxNi45LDUxLjEsMTYsNTAsMTZ6IE0xOSwyMGMwLTIuMiwxLjgtNCw0LTRjMS40LDAsMi44LDAuOCwzLjUsMgoJCWMxLjEsMS45LDAuNCw0LjMtMS41LDUuNFYzM2MxLTAuNiwyLjMtMC45LDQtMC45YzEsMCwyLTAuNSwyLjgtMS4zQzMyLjUsMzAsMzMsMjkuMSwzMywyOHYtMC42Yy0xLjItMC43LTItMi0yLTMuNQoJCWMwLTIuMiwxLjgtNCw0LTRjMi4yLDAsNCwxLjgsNCw0YzAsMS41LTAuOCwyLjctMiwzLjVoMGMtMC4xLDIuMS0wLjksNC40LTIuNSw2Yy0xLjYsMS42LTMuNCwyLjQtNS41LDIuNWMtMC44LDAtMS40LDAuMS0xLjksMC4zCgkJYy0wLjIsMC4xLTEsMC44LTEuMiwwLjlDMjYuNiwzOCwyNywzOC45LDI3LDQwYzAsMi4yLTEuOCw0LTQsNHMtNC0xLjgtNC00YzAtMS41LDAuOC0yLjcsMi0zLjRWMjMuNEMxOS44LDIyLjcsMTksMjEuNCwxOSwyMHoiLz4KPC9nPgo8L3N2Zz4K"
|
||||
|
||||
// searchRepositories searches for repositories by name.
|
||||
//
|
||||
@ -52,7 +53,7 @@ func searchRepositories(ctx context.Context, args *search.Args, limit int32) (re
|
||||
break
|
||||
}
|
||||
if pattern.MatchString(string(repo.Repo.Name)) {
|
||||
results = append(results, &searchResultResolver{repo: &repositoryResolver{repo: repo.Repo}})
|
||||
results = append(results, &searchResultResolver{repo: &repositoryResolver{repo: repo.Repo, icon: repoIcon}})
|
||||
}
|
||||
}
|
||||
return results, common, nil
|
||||
|
||||
20
cmd/frontend/graphqlbackend/search_result_match.go
Normal file
20
cmd/frontend/graphqlbackend/search_result_match.go
Normal file
@ -0,0 +1,20 @@
|
||||
package graphqlbackend
|
||||
|
||||
// A resolver for the GraphQL type GenericSearchMatch
|
||||
type searchResultMatchResolver struct {
|
||||
url string
|
||||
body string
|
||||
highlights []*highlightedRange
|
||||
}
|
||||
|
||||
func (m *searchResultMatchResolver) URL() string {
|
||||
return m.url
|
||||
}
|
||||
|
||||
func (m *searchResultMatchResolver) Body() *markdownResolver {
|
||||
return &markdownResolver{text: m.body}
|
||||
}
|
||||
|
||||
func (m *searchResultMatchResolver) Highlights() []*highlightedRange {
|
||||
return m.highlights
|
||||
}
|
||||
@ -65,7 +65,6 @@ type fileMatchResolver struct {
|
||||
uri string
|
||||
repo *types.Repo
|
||||
commitID api.CommitID // or empty for default branch
|
||||
|
||||
// inputRev is the Git revspec that the user originally requested to search. It is used to
|
||||
// preserve the original revision specifier from the user instead of navigating them to the
|
||||
// absolute commit ID when they select a result.
|
||||
|
||||
1
go.mod
1
go.mod
@ -70,6 +70,7 @@ require (
|
||||
github.com/kevinburke/differ v0.0.0-20181006040839-bdfd927653c8
|
||||
github.com/kevinburke/go-bindata v3.12.0+incompatible
|
||||
github.com/kr/text v0.1.0
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
|
||||
github.com/lib/pq v1.0.0
|
||||
github.com/lightstep/lightstep-tracer-go v0.15.4
|
||||
github.com/mattn/goreman v0.2.1-0.20180930133601-738cf1257bd3
|
||||
|
||||
@ -184,6 +184,8 @@
|
||||
"@sourcegraph/codeintellify": "^4.0.0",
|
||||
"@sourcegraph/react-loading-spinner": "0.0.6",
|
||||
"@sqs/jsonc-parser": "^1.0.3",
|
||||
"@types/he": "^1.1.0",
|
||||
"@types/sanitize-html": "^1.18.2",
|
||||
"abortable-rx": "^1.0.9",
|
||||
"babel-plugin-istanbul": "^5.0.1",
|
||||
"bootstrap": "^4.1.3",
|
||||
@ -196,6 +198,7 @@
|
||||
"downshift": "^2.0.14",
|
||||
"formdata-polyfill": "^3.0.9",
|
||||
"graphiql": "^0.11.11",
|
||||
"he": "^1.2.0",
|
||||
"highlight.js": "^9.13.1",
|
||||
"intersection-observer": "^0.5.0",
|
||||
"lodash": "^4.17.10",
|
||||
|
||||
@ -58,7 +58,7 @@ func Code(ctx context.Context, content []byte, filepath string, disableTimeout b
|
||||
|
||||
themechoice := "Sourcegraph"
|
||||
if isLightTheme {
|
||||
themechoice = "Solarized (light)"
|
||||
themechoice = "Sourcegraph (light)"
|
||||
}
|
||||
|
||||
// Trim a single newline from the end of the file. This means that a file
|
||||
|
||||
346
pkg/highlight/syntect_language_map.go
Normal file
346
pkg/highlight/syntect_language_map.go
Normal file
@ -0,0 +1,346 @@
|
||||
package highlight
|
||||
|
||||
// SyntectLanguageMap is a map that maps language identifiers that may be provided at the end of markdown code fences
|
||||
// to valid syntect file extensions.
|
||||
var SyntectLanguageMap = map[string]string{"txt": "txt",
|
||||
"asa": "asa",
|
||||
"asp": "asp",
|
||||
"actionscript": "as",
|
||||
"as": "as",
|
||||
"applescript": "applescript",
|
||||
"script editor": "script editor",
|
||||
"batchfile": "bat",
|
||||
"bat": "bat",
|
||||
"cmd": "cmd",
|
||||
"build": "build",
|
||||
"c#": "cs",
|
||||
"cs": "cs",
|
||||
"csx": "csx",
|
||||
"cpp": "cpp",
|
||||
"cc": "cc",
|
||||
"cp": "cp",
|
||||
"cxx": "cxx",
|
||||
"c++": "c++",
|
||||
"c": "c",
|
||||
"h": "h",
|
||||
"hh": "hh",
|
||||
"hpp": "hpp",
|
||||
"hxx": "hxx",
|
||||
"h++": "h++",
|
||||
"inl": "inl",
|
||||
"ipp": "ipp",
|
||||
"cmakecache.txt": "CMakeCache.txt",
|
||||
"cmakelists.txt": "CMakeLists.txt",
|
||||
"cmake": "cmake",
|
||||
"css": "css",
|
||||
"css.erb": "css.erb",
|
||||
"css.liquid": "css.liquid",
|
||||
"capnp": "capnp",
|
||||
"cg": "cg",
|
||||
"clojure": "clj",
|
||||
"clj": "clj",
|
||||
"cljs": "cljs",
|
||||
"cljc": "cljc",
|
||||
"cljx": "cljx",
|
||||
"crontab": "crontab",
|
||||
"d": "d",
|
||||
"di": "di",
|
||||
"diff": "diff",
|
||||
"patch": "patch",
|
||||
"dockerfile": "Dockerfile",
|
||||
"erlang": "erl",
|
||||
"erl": "erl",
|
||||
"hrl": "hrl",
|
||||
"emakefile": "emakefile",
|
||||
"yaws": "yaws",
|
||||
"fsharp": "fs",
|
||||
"forth": "frt",
|
||||
"frt": "frt",
|
||||
"essl": "essl",
|
||||
"f.essl": "f.essl",
|
||||
"v.essl": "v.essl",
|
||||
"_v.essl": "_v.essl",
|
||||
"_f.essl": "_f.essl",
|
||||
"_vs.essl": "_vs.essl",
|
||||
"_fs.essl": "_fs.essl",
|
||||
"vs": "vs",
|
||||
"fs": "fs",
|
||||
"gs": "gs",
|
||||
"vsh": "vsh",
|
||||
"fsh": "fsh",
|
||||
"gsh": "gsh",
|
||||
"vshader": "vshader",
|
||||
"fshader": "fshader",
|
||||
"gshader": "gshader",
|
||||
"vert": "vert",
|
||||
"frag": "frag",
|
||||
"geom": "geom",
|
||||
"tesc": "tesc",
|
||||
"tese": "tese",
|
||||
"comp": "comp",
|
||||
"glsl": "glsl",
|
||||
"attributes": "attributes",
|
||||
"gitattributes": "gitattributes",
|
||||
".gitattributes": "gitattributes",
|
||||
"commit_editmsg": "COMMIT_EDITMSG",
|
||||
"merge_msg": "MERGE_MSG",
|
||||
"tag_editmsg": "TAG_EDITMSG",
|
||||
"gitconfig": "gitconfig",
|
||||
".gitconfig": "gitconfig",
|
||||
".gitmodules": "gitmodules",
|
||||
"exclude": "exclude",
|
||||
"gitignore": "gitignore",
|
||||
".gitignore": "gitignore",
|
||||
"gitlink": "git",
|
||||
".git": "git",
|
||||
"gitlog": "gitlog",
|
||||
"git-rebase-todo": "git-rebase-todo",
|
||||
"go": "go",
|
||||
"graphviz-dot": "dot",
|
||||
"dot": "dot",
|
||||
"gv": "gv",
|
||||
"groovy": "groovy",
|
||||
"gvy": "gvy",
|
||||
"gradle": "gradle",
|
||||
"fx": "fx",
|
||||
"fxh": "fxh",
|
||||
"hlsl": "hlsl",
|
||||
"hlsli": "hlsli",
|
||||
"usf": "usf",
|
||||
"html": "html",
|
||||
"htm": "htm",
|
||||
"shtml": "shtml",
|
||||
"xhtml": "xhtml",
|
||||
"tmpl": "tmpl",
|
||||
"tpl": "tpl",
|
||||
"haskell": "hs",
|
||||
"hs": "hs",
|
||||
"literatehaskell": "lhs",
|
||||
"lhs": "lhs",
|
||||
"ini": "ini",
|
||||
"inf": "INF",
|
||||
"reg": "reg",
|
||||
"lng": "lng",
|
||||
"cfg": "cfg",
|
||||
"url": "url",
|
||||
".editorconfig": "editorconfig",
|
||||
"jsp": "jsp",
|
||||
"java": "java",
|
||||
"bsh": "bsh",
|
||||
"javaproperties": "javaproperties",
|
||||
"properties": "properties",
|
||||
"json": "json",
|
||||
"sublime-settings": "sublime-settings",
|
||||
"sublime-menu": "sublime-menu",
|
||||
"sublime-keymap": "sublime-keymap",
|
||||
"sublime-mousemap": "sublime-mousemap",
|
||||
"sublime-theme": "sublime-theme",
|
||||
"sublime-build": "sublime-build",
|
||||
"sublime-project": "sublime-project",
|
||||
"sublime-completions": "sublime-completions",
|
||||
"sublime-commands": "sublime-commands",
|
||||
"sublime-macro": "sublime-macro",
|
||||
"sublime-color-scheme": "sublime-color-scheme",
|
||||
"javascript": "js",
|
||||
"js": "js",
|
||||
"jsx": "jsx",
|
||||
"babel": "babel",
|
||||
"es6": "es6",
|
||||
"less": "less",
|
||||
"bibteX": "bib",
|
||||
"bib": "bib",
|
||||
"latex": "ltx",
|
||||
"tex": "tex",
|
||||
"ltx": "ltx",
|
||||
"sty": "sty",
|
||||
"cls": "cls",
|
||||
"lisp": "lisp",
|
||||
"cl": "cl",
|
||||
"clisp": "clisp",
|
||||
"l": "l",
|
||||
"mud": "mud",
|
||||
"el": "el",
|
||||
"scm": "scm",
|
||||
"ss": "ss",
|
||||
"lsp": "lsp",
|
||||
"fasl": "fasl",
|
||||
"lua": "lua",
|
||||
"proj": "proj",
|
||||
"targets": "targets",
|
||||
"msbuild": "msbuild",
|
||||
"csproj": "csproj",
|
||||
"vbproj": "vbproj",
|
||||
"fsproj": "fsproj",
|
||||
"vcxproj": "vcxproj",
|
||||
"make": "make",
|
||||
"gnumakefile": "GNUmakefile",
|
||||
"makefile": "makefile",
|
||||
"ocamlmakefile": "OCamlMakefile",
|
||||
"mak": "mak",
|
||||
"mk": "mk",
|
||||
"man": "man",
|
||||
"md": "md",
|
||||
"mdown": "mdown",
|
||||
"markdown": "markdown",
|
||||
"markdn": "markdn",
|
||||
"matlab": "matlab",
|
||||
"pom.xml": "pom.xml",
|
||||
"mediawiki": "mediawiki",
|
||||
"wikipedia": "wikipedia",
|
||||
"wiki": "wiki",
|
||||
"ninja": "ninja",
|
||||
"ocaml": "ml",
|
||||
"ml": "ml",
|
||||
"mli": "mli",
|
||||
"ocamllex": "mll",
|
||||
"mll": "mll",
|
||||
"ocamlyacc": "mly",
|
||||
"mly": "mly",
|
||||
"objective-c++": "mm",
|
||||
"mm": "mm",
|
||||
"objective-c": "m",
|
||||
"m": "m",
|
||||
"php": "php",
|
||||
"php3": "php3",
|
||||
"php4": "php4",
|
||||
"php5": "php5",
|
||||
"php7": "php7",
|
||||
"phps": "phps",
|
||||
"phpt": "phpt",
|
||||
"phtml": "phtml",
|
||||
"pascal": "pas",
|
||||
"pas": "pas",
|
||||
"p": "p",
|
||||
"dpr": "dpr",
|
||||
"spec": "spec",
|
||||
"client": "client",
|
||||
"perl": "pl",
|
||||
"pm": "pm",
|
||||
"pod": "pod",
|
||||
"t": "t",
|
||||
"pl": "pl",
|
||||
"postscript": "ps",
|
||||
"ps": "ps",
|
||||
"eps": "eps",
|
||||
"powershell": "ps1",
|
||||
"ps1": "ps1",
|
||||
"psm1": "psm1",
|
||||
"psd1": "psd1",
|
||||
"python": "py",
|
||||
"py": "py",
|
||||
"py3": "py3",
|
||||
"pyw": "pyw",
|
||||
"pyi": "pyi",
|
||||
"pyx": "pyx",
|
||||
"pyx.in": "pyx.in",
|
||||
"pxd": "pxd",
|
||||
"pxd.in": "pxd.in",
|
||||
"pxi": "pxi",
|
||||
"pxi.in": "pxi.in",
|
||||
"rpy": "rpy",
|
||||
"cpy": "cpy",
|
||||
"sconstruct": "sconstruct",
|
||||
"sconscript": "SConscript",
|
||||
"gyp": "gyp",
|
||||
"gypi": "gypi",
|
||||
"snakefile": "snakefile",
|
||||
"wscript": "wscript",
|
||||
"r": "r",
|
||||
"s": "s",
|
||||
"rprofile": "Rprofile",
|
||||
"rd": "rd",
|
||||
"rails": "rails",
|
||||
"rhtml": "rhtml",
|
||||
"erb": "erb",
|
||||
"html.erb": "html.erb",
|
||||
"js.erb": "js.erb",
|
||||
"haml": "haml",
|
||||
"rubyonrails": "rxml",
|
||||
"rxml": "rxml",
|
||||
"builder": "builder",
|
||||
"erbsql": "erbsql",
|
||||
"sql.erb": "sql.erb",
|
||||
"re": "re",
|
||||
"restructuredtext": "rst",
|
||||
"rst": "rst",
|
||||
"rest": "rest",
|
||||
"ruby": "rb",
|
||||
"rb": "rb",
|
||||
"appfile": "Appfile",
|
||||
"appraisals": "Appraisals",
|
||||
"berksfile": "Berksfile",
|
||||
"brewfile": "Brewfile",
|
||||
"capfile": "capfile",
|
||||
"cgi": "cgi",
|
||||
"cheffile": "cheffile",
|
||||
"config.ru": "config.ru",
|
||||
"deliverfile": "Deliverfile",
|
||||
"fastfile": "Fastfile",
|
||||
"fcgi": "fcgi",
|
||||
"gemfile": "Gemfile",
|
||||
"gemspec": "gemspec",
|
||||
"guardfile": "Guardfile",
|
||||
"irbrc": "irbrc",
|
||||
"jbuilder": "jbuilder",
|
||||
"podspec": "podspec",
|
||||
"prawn": "prawn",
|
||||
"rabl": "rabl",
|
||||
"rake": "rake",
|
||||
"rakefile": "Rakefile",
|
||||
"rantfile": "Rantfile",
|
||||
"rbx": "rbx",
|
||||
"rjs": "rjs",
|
||||
"ruby.rail": "ruby.rail",
|
||||
"scanfile": "Scanfile",
|
||||
"simplecov": "simplecov",
|
||||
"snapfile": "Snapfile",
|
||||
"thor": "thor",
|
||||
"thorfile": "Thorfile",
|
||||
"vagrantfile": "Vagrantfile",
|
||||
"rs": "rs",
|
||||
"sass": "sass",
|
||||
"scss": "scss",
|
||||
"sql": "sql",
|
||||
"ddl": "ddl",
|
||||
"dml": "dml",
|
||||
"scala": "scala",
|
||||
"sbt": "sbt",
|
||||
"sh": "sh",
|
||||
"bash": "bash",
|
||||
"zsh": "zsh",
|
||||
"fish": "fish",
|
||||
".bash_aliases": "bash_aliases",
|
||||
".bash_completions": "bash_completions",
|
||||
".bash_functions": "bash_functions",
|
||||
".bash_login": "bash_login",
|
||||
".bash_logout": "bash_logout",
|
||||
".bash_profile": "bash_profile",
|
||||
".bash_variables": "bash_variables",
|
||||
".bashrc": "bashrc",
|
||||
".profile": "profile",
|
||||
".textmate_init": "textmate_init",
|
||||
"smalltalk": "st",
|
||||
"st": "st",
|
||||
"swift": "swift",
|
||||
"adp": "adp",
|
||||
"tcl": "tcl",
|
||||
"toml": "toml",
|
||||
"tml": "tml",
|
||||
"lock": "lock",
|
||||
"textile": "textile",
|
||||
"thrift": "thrift",
|
||||
"typescript": "ts",
|
||||
"ts": "ts",
|
||||
"typescriptreact": "tsx",
|
||||
"tsx": "tsx",
|
||||
"xml": "xml",
|
||||
"xsd": "xsd",
|
||||
"xslt": "xslt",
|
||||
"tld": "tld",
|
||||
"dtml": "dtml",
|
||||
"rss": "rss",
|
||||
"opml": "opml",
|
||||
"svg": "svg",
|
||||
"yaml": "yaml",
|
||||
"yml": "yml",
|
||||
"sublime-syntax": "sublime-syntax"}
|
||||
@ -70,6 +70,20 @@
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.search-result-match__markdown {
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
@ -98,5 +112,13 @@
|
||||
color: #6a737d;
|
||||
border-left-color: #dfe2e5;
|
||||
}
|
||||
|
||||
&.search-result-match__markdown {
|
||||
code,
|
||||
pre {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,15 @@ import * as React from 'react'
|
||||
|
||||
interface Props {
|
||||
dangerousInnerHTML: string
|
||||
className?: string
|
||||
/** A function to attain a reference to the top-level div from a parent component. */
|
||||
refFn?: (ref: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
export const Markdown: React.FunctionComponent<Props> = (props: Props) => (
|
||||
<div className="markdown" dangerouslySetInnerHTML={{ __html: props.dangerousInnerHTML }} />
|
||||
<div
|
||||
ref={props.refFn}
|
||||
className={`markdown ${props.className}`}
|
||||
dangerouslySetInnerHTML={{ __html: props.dangerousInnerHTML }}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
.result-container {
|
||||
position: relative;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 2px;
|
||||
|
||||
@ -26,11 +27,19 @@
|
||||
&:hover {
|
||||
background-color: darken($background-color, 0.05);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-matches-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon-inline__filtered {
|
||||
filter: contrast(0) saturate(700%) grayscale(100%) sepia(70%) hue-rotate(180deg) brightness(120%);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
@ -44,4 +53,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-inline__filtered {
|
||||
filter: brightness(0%);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ export interface Props {
|
||||
|
||||
/** Expand all results */
|
||||
allExpanded?: boolean
|
||||
stringIcon?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -90,6 +91,7 @@ export class ResultContainer extends React.PureComponent<Props, State> {
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
const Icon = this.props.icon
|
||||
const stringIcon = this.props.stringIcon ? this.props.stringIcon : undefined
|
||||
return (
|
||||
<div className="result-container">
|
||||
<div
|
||||
@ -99,7 +101,11 @@ export class ResultContainer extends React.PureComponent<Props, State> {
|
||||
}
|
||||
onClick={this.toggle}
|
||||
>
|
||||
<Icon className="icon-inline" />
|
||||
{!!stringIcon ? (
|
||||
<img src={stringIcon} className="icon-inline icon-inline__filtered" />
|
||||
) : (
|
||||
<Icon className="icon-inline" />
|
||||
)}
|
||||
<div className={`result-container__header-title ${this.props.titleClassName || ''}`}>
|
||||
{this.props.collapsible ? (
|
||||
<span onClick={blockExpandAndCollapse}>{this.props.title}</span>
|
||||
|
||||
@ -14,7 +14,14 @@ export function highlightNode(node: HTMLElement, start: number, length: number):
|
||||
if (length > node.textContent!.length - start) {
|
||||
return
|
||||
}
|
||||
|
||||
// We want to treat text nodes as walkable so they can be highlighted. Wrap these in a span and
|
||||
// replace them in the DOM.
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent !== null) {
|
||||
const sp = document.createElement('span')
|
||||
sp.innerHTML = node.textContent
|
||||
node.parentNode!.replaceChild(sp, node)
|
||||
node = sp
|
||||
}
|
||||
node.classList.add('annotated-selection-match')
|
||||
highlightNodeHelper(node, 0, start, length)
|
||||
}
|
||||
|
||||
@ -269,5 +269,7 @@ hr {
|
||||
@import './marketing/SurveyPage';
|
||||
@import './components/SingleValueCard';
|
||||
@import './repo/blob/discussions/DiscussionsTree';
|
||||
@import './components/SearchResult';
|
||||
@import './components/SearchResultMatch';
|
||||
|
||||
@import './extensions/shared';
|
||||
|
||||
@ -3,7 +3,7 @@ import VisibilitySensor from 'react-visibility-sensor'
|
||||
import { LinkOrSpan } from '../../../shared/src/components/LinkOrSpan'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { highlightNode } from '../../../shared/src/util/dom'
|
||||
|
||||
import { HighlightRange } from './SearchResult'
|
||||
interface Props {
|
||||
/**
|
||||
* A CSS class name to add to this component's element.
|
||||
@ -18,7 +18,7 @@ interface Props {
|
||||
/**
|
||||
* The highlights for the lines.
|
||||
*/
|
||||
highlights?: GQL.IHighlight[]
|
||||
highlights?: GQL.IHighlight[] | HighlightRange[]
|
||||
|
||||
/**
|
||||
* A list of classes to apply to 1-indexed line numbers.
|
||||
@ -33,7 +33,7 @@ interface Props {
|
||||
|
||||
interface DecoratedLine {
|
||||
value: string
|
||||
highlights?: GQL.IHighlight[]
|
||||
highlights?: (GQL.IHighlight | HighlightRange)[]
|
||||
classNames?: string[]
|
||||
url?: string
|
||||
}
|
||||
@ -97,6 +97,9 @@ export class DecoratedTextLines extends React.PureComponent<Props, State> {
|
||||
const lines: DecoratedLine[] = lineValues.map(line => ({ value: line }))
|
||||
if (props.highlights) {
|
||||
for (const highlight of props.highlights) {
|
||||
if (highlight.line > lines.length - 1) {
|
||||
continue
|
||||
}
|
||||
const line = lines[highlight.line - 1]
|
||||
if (!line.highlights) {
|
||||
line.highlights = []
|
||||
|
||||
9
web/src/components/SearchResult.scss
Normal file
9
web/src/components/SearchResult.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.search-result {
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
95
web/src/components/SearchResult.tsx
Normal file
95
web/src/components/SearchResult.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { decode } from 'he'
|
||||
import _ from 'lodash'
|
||||
import marked from 'marked'
|
||||
import FileIcon from 'mdi-react/FileIcon'
|
||||
import React from 'react'
|
||||
import { ResultContainer } from '../../../shared/src/components/ResultContainer'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { SearchResultMatch } from './SearchResultMatch'
|
||||
|
||||
export interface HighlightRange {
|
||||
/**
|
||||
* The 0-based line number that this highlight appears in
|
||||
*/
|
||||
line: number
|
||||
/**
|
||||
* The 0-based character offset to start highlighting at
|
||||
*/
|
||||
character: number
|
||||
/**
|
||||
* The number of characters to highlight
|
||||
*/
|
||||
length: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
result: GQL.GenericSearchResultInterface
|
||||
isLightTheme: boolean
|
||||
}
|
||||
|
||||
export class SearchResult extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
private renderTitle = () => (
|
||||
<div className="search-result__title">
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.result.label.html
|
||||
? decode(this.props.result.label.html)
|
||||
: marked(this.props.result.label.text, { gfm: true, breaks: true, sanitize: true }),
|
||||
}}
|
||||
/>
|
||||
{this.props.result.detail && (
|
||||
<>
|
||||
<span className="search-result__spacer" />
|
||||
<small
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.result.detail.html
|
||||
? decode(this.props.result.detail.html)
|
||||
: marked(this.props.result.detail.text, { gfm: true, breaks: true, sanitize: true }),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
private renderBody = () => (
|
||||
<>
|
||||
{this.props.result.matches.map((match, index) => {
|
||||
const highlightRanges: HighlightRange[] = []
|
||||
match.highlights.map(highlight =>
|
||||
highlightRanges.push({
|
||||
line: highlight.line,
|
||||
character: highlight.character,
|
||||
length: highlight.length,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<SearchResultMatch
|
||||
key={`item.url#${index}`}
|
||||
item={match}
|
||||
highlightRanges={highlightRanges}
|
||||
isLightTheme={this.props.isLightTheme}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<ResultContainer
|
||||
stringIcon={this.props.result.icon}
|
||||
icon={FileIcon}
|
||||
collapsible={this.props.result && this.props.result.matches.length > 0}
|
||||
defaultExpanded={true}
|
||||
title={this.renderTitle()}
|
||||
expandedChildren={this.renderBody()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
151
web/src/components/SearchResultMatch.scss
Normal file
151
web/src/components/SearchResultMatch.scss
Normal file
@ -0,0 +1,151 @@
|
||||
.search-result-match {
|
||||
text-decoration: none; // don't use cascading link style
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background-color: #070a0d;
|
||||
|
||||
&-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #151d28;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #2a3a51;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table,
|
||||
tr,
|
||||
td {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
.code {
|
||||
padding: 0;
|
||||
}
|
||||
span:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__markdown {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__code-excerpt {
|
||||
padding-top: 0;
|
||||
padding-bottom: 1rem;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
td.line {
|
||||
display: none;
|
||||
}
|
||||
td.code > span:first-child {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
td.code > span:last-child {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__line {
|
||||
&--hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
code.language-diff & {
|
||||
font-style: italic;
|
||||
tr:first-child {
|
||||
td.code:first-child {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light & {
|
||||
background-color: $color-light-bg-1;
|
||||
border-top: 1px solid $color-light-border;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-light-bg-4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table,
|
||||
tr,
|
||||
td {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
.code {
|
||||
padding: 0;
|
||||
}
|
||||
span:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__code-excerpt {
|
||||
padding-top: 0;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
td.line {
|
||||
display: none;
|
||||
}
|
||||
td.code > span:first-child {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
td.code > span:last-child {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
192
web/src/components/SearchResultMatch.tsx
Normal file
192
web/src/components/SearchResultMatch.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
|
||||
import { decode } from 'he'
|
||||
import _ from 'lodash'
|
||||
import { range } from 'lodash'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import VisibilitySensor from 'react-visibility-sensor'
|
||||
import { combineLatest, of, Subject, Subscription } from 'rxjs'
|
||||
import { catchError, filter, switchMap } from 'rxjs/operators'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { Markdown } from '../../../shared/src/components/Markdown'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { highlightNode } from '../../../shared/src/util/dom'
|
||||
import { renderMarkdown } from '../discussions/backend'
|
||||
import { highlightCode } from '../search/backend'
|
||||
import { HighlightRange } from './SearchResult'
|
||||
|
||||
interface SearchResultMatchProps {
|
||||
item: GQL.ISearchResultMatch
|
||||
highlightRanges: HighlightRange[]
|
||||
isLightTheme: boolean
|
||||
}
|
||||
|
||||
interface SearchResultMatchState {
|
||||
HTML?: string
|
||||
}
|
||||
|
||||
export class SearchResultMatch extends React.Component<SearchResultMatchProps, SearchResultMatchState> {
|
||||
public state: SearchResultMatchState = {}
|
||||
private tableContainerElement: HTMLElement | null = null
|
||||
private visibilitySensorOffset = { bottom: -500 }
|
||||
|
||||
private visibilityChanges = new Subject<boolean>()
|
||||
private subscriptions = new Subscription()
|
||||
private propsChanges = new Subject<SearchResultMatchProps>()
|
||||
|
||||
private getLanguage(): string | undefined {
|
||||
const matches = /(?:```)([^\s]+)\s/.exec(this.props.item.body.text)
|
||||
if (!matches) {
|
||||
return undefined
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
private bodyIsCode(): boolean {
|
||||
return this.props.item.body.text.startsWith('```') && this.props.item.body.text.endsWith('```')
|
||||
}
|
||||
|
||||
public constructor(props: SearchResultMatchProps) {
|
||||
super(props)
|
||||
|
||||
// Render the match body as markdown, and syntax highlight the response if it's a code block.
|
||||
// This is a lot of network requests right now, but once extensions can run on the backend we can
|
||||
// run results through the renderer and syntax highlighter without network requests.
|
||||
this.subscriptions.add(
|
||||
combineLatest(this.propsChanges, this.visibilityChanges)
|
||||
.pipe(
|
||||
filter(([, isVisible]) => isVisible),
|
||||
switchMap(
|
||||
([props]) =>
|
||||
props.item.body.html
|
||||
? of(sanitizeHtml(props.item.body.html))
|
||||
: renderMarkdown({ markdown: props.item.body.text })
|
||||
),
|
||||
switchMap(markdownHTML => {
|
||||
if (this.bodyIsCode() && markdownHTML.includes('<code') && markdownHTML.includes('</code>')) {
|
||||
const lang = this.getLanguage() || 'txt'
|
||||
const parser = new DOMParser()
|
||||
// Get content between the outermost code tags.
|
||||
const codeContent = parser
|
||||
.parseFromString(markdownHTML, 'text/html')
|
||||
.querySelector('code')!
|
||||
.innerHTML.toString()
|
||||
if (codeContent) {
|
||||
return highlightCode({
|
||||
code: decode(codeContent),
|
||||
fuzzyLanguage: lang,
|
||||
disableTimeout: false,
|
||||
isLightTheme: this.props.isLightTheme,
|
||||
}).pipe(
|
||||
switchMap(highlightedStr => {
|
||||
const highlightedMarkdown = markdownHTML.replace(codeContent, highlightedStr)
|
||||
return of(highlightedMarkdown)
|
||||
}),
|
||||
// Return the rendered markdown if highlighting fails.
|
||||
catchError(() => of(markdownHTML))
|
||||
)
|
||||
}
|
||||
}
|
||||
return of(markdownHTML)
|
||||
}),
|
||||
// Return the raw body if markdown rendering fails, maintaing the text structure.
|
||||
catchError(() => of('<pre>' + sanitizeHtml(props.item.body.text) + '</pre>'))
|
||||
)
|
||||
.subscribe(str => this.setState({ HTML: str }), error => console.error(error))
|
||||
)
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.propsChanges.next(this.props)
|
||||
this.highlightNodes()
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
this.highlightNodes()
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
|
||||
private highlightNodes(): void {
|
||||
if (this.tableContainerElement) {
|
||||
const visibleRows = this.tableContainerElement.querySelectorAll('table tr')
|
||||
if (visibleRows.length > 0) {
|
||||
for (const h of this.props.highlightRanges) {
|
||||
const code = visibleRows[h.line - 1]
|
||||
if (code) {
|
||||
highlightNode(code as HTMLElement, h.character, h.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onChangeVisibility = (isVisible: boolean): void => {
|
||||
this.visibilityChanges.next(isVisible)
|
||||
}
|
||||
|
||||
private getFirstLine(): number {
|
||||
return Math.max(0, Math.min(...this.props.highlightRanges.map(r => r.line)) - 1)
|
||||
}
|
||||
|
||||
private getLastLine(): number {
|
||||
const lastLine = Math.max(...this.props.highlightRanges.map(r => r.line)) + 1
|
||||
return this.props.highlightRanges ? Math.min(lastLine, this.props.highlightRanges.length) : lastLine
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const firstLine = this.getFirstLine()
|
||||
let lastLine = this.getLastLine()
|
||||
if (firstLine === lastLine) {
|
||||
// Some edge cases yield the same first and last line, causing the visibility sensor to break, so make sure to avoid this.
|
||||
lastLine++
|
||||
}
|
||||
|
||||
return (
|
||||
<VisibilitySensor
|
||||
active={true}
|
||||
onChange={this.onChangeVisibility}
|
||||
partialVisibility={true}
|
||||
offset={this.visibilitySensorOffset}
|
||||
>
|
||||
<>
|
||||
{this.state.HTML && (
|
||||
<Link key={this.props.item.url} to={this.props.item.url} className="search-result-match">
|
||||
<Markdown
|
||||
refFn={this.setTableContainerElement}
|
||||
className={`search-result-match__markdown ${
|
||||
this.bodyIsCode() ? 'search-result-match__code-excerpt' : ''
|
||||
}`}
|
||||
dangerousInnerHTML={this.state.HTML}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
{!this.state.HTML && (
|
||||
<>
|
||||
<LoadingSpinner className="icon-inline search-result-match__loader" />
|
||||
<table>
|
||||
<tbody>
|
||||
{range(firstLine, lastLine).map(i => (
|
||||
<tr key={`this.props.item.url#${i}`}>
|
||||
{/* create empty space to fill viewport (as if the blob content were already fetched, otherwise we'll overfetch) */}
|
||||
<td className="line search-result-match__line--hidden">
|
||||
<code>{i}</code>
|
||||
</td>
|
||||
<td className="code"> </td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</VisibilitySensor>
|
||||
)
|
||||
}
|
||||
|
||||
private setTableContainerElement = (ref: HTMLElement | null) => {
|
||||
this.tableContainerElement = ref
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { map } from 'rxjs/operators'
|
||||
import { gql } from '../../../shared/src/graphql/graphql'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { createAggregateError } from '../../../shared/src/util/errors'
|
||||
import { memoizeObservable } from '../../../shared/src/util/memoizeObservable'
|
||||
import { mutateGraphQL, queryGraphQL } from '../backend/graphql'
|
||||
|
||||
const discussionCommentFieldsFragment = gql`
|
||||
@ -271,20 +272,22 @@ export function updateComment(input: GQL.IDiscussionCommentUpdateInput): Observa
|
||||
*
|
||||
* @return Observable that emits the HTML string, which is already sanitized and escaped and thus is always safe to render.
|
||||
*/
|
||||
export function renderMarkdown(markdown: string, options?: GQL.IMarkdownOptions): Observable<string> {
|
||||
return queryGraphQL(
|
||||
gql`
|
||||
query RenderMarkdown($markdown: String!, $options: MarkdownOptions) {
|
||||
renderMarkdown(markdown: $markdown, options: $options)
|
||||
}
|
||||
`,
|
||||
{ markdown }
|
||||
).pipe(
|
||||
map(({ data, errors }) => {
|
||||
if (!data || !data.renderMarkdown) {
|
||||
throw createAggregateError(errors)
|
||||
}
|
||||
return data.renderMarkdown
|
||||
})
|
||||
)
|
||||
}
|
||||
export const renderMarkdown = memoizeObservable(
|
||||
(ctx: { markdown: string; options?: GQL.IMarkdownOptions }): Observable<string> =>
|
||||
queryGraphQL(
|
||||
gql`
|
||||
query RenderMarkdown($markdown: String!, $options: MarkdownOptions) {
|
||||
renderMarkdown(markdown: $markdown, options: $options)
|
||||
}
|
||||
`,
|
||||
ctx
|
||||
).pipe(
|
||||
map(({ data, errors }) => {
|
||||
if (!data || !data.renderMarkdown) {
|
||||
throw createAggregateError(errors)
|
||||
}
|
||||
return data.renderMarkdown
|
||||
})
|
||||
),
|
||||
ctx => `${ctx.markdown}:${ctx.options}`
|
||||
)
|
||||
|
||||
@ -21,3 +21,5 @@ registerLanguage('r', require('highlight.js/lib/languages/r'))
|
||||
registerLanguage('ruby', require('highlight.js/lib/languages/ruby'))
|
||||
registerLanguage('rust', require('highlight.js/lib/languages/rust'))
|
||||
registerLanguage('swift', require('highlight.js/lib/languages/swift'))
|
||||
registerLanguage('markdown', require('highlight.js/lib/languages/markdown'))
|
||||
registerLanguage('diff', require('highlight.js/lib/languages/diff'))
|
||||
|
||||
@ -136,7 +136,7 @@ export class DiscussionsInput extends React.PureComponent<Props, State> {
|
||||
mergeMap(([, { textAreaValue }]) =>
|
||||
concat(
|
||||
of<Update>(state => ({ ...state, previewHTML: undefined, previewLoading: true })),
|
||||
renderMarkdown(this.trimImplicitTitle(textAreaValue)).pipe(
|
||||
renderMarkdown({ markdown: this.trimImplicitTitle(textAreaValue) }).pipe(
|
||||
map(
|
||||
(previewHTML): Update => state => ({
|
||||
...state,
|
||||
|
||||
@ -5,6 +5,7 @@ import { ExtensionsControllerProps } from '../../../shared/src/extensions/contro
|
||||
import { gql } from '../../../shared/src/graphql/graphql'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { asError, createAggregateError, ErrorLike } from '../../../shared/src/util/errors'
|
||||
import { memoizeObservable } from '../../../shared/src/util/memoizeObservable'
|
||||
import { mutateGraphQL, queryGraphQL } from '../backend/graphql'
|
||||
|
||||
export function search(
|
||||
@ -48,6 +49,25 @@ export function search(
|
||||
id
|
||||
name
|
||||
url
|
||||
label {
|
||||
html
|
||||
}
|
||||
icon
|
||||
detail {
|
||||
html
|
||||
}
|
||||
matches {
|
||||
url
|
||||
body {
|
||||
text
|
||||
html
|
||||
}
|
||||
highlights {
|
||||
line
|
||||
character
|
||||
length
|
||||
}
|
||||
}
|
||||
}
|
||||
... on FileMatch {
|
||||
__typename
|
||||
@ -77,57 +97,24 @@ export function search(
|
||||
}
|
||||
... on CommitSearchResult {
|
||||
__typename
|
||||
refs {
|
||||
name
|
||||
displayName
|
||||
prefix
|
||||
repository {
|
||||
name
|
||||
}
|
||||
label {
|
||||
html
|
||||
}
|
||||
sourceRefs {
|
||||
name
|
||||
displayName
|
||||
prefix
|
||||
repository {
|
||||
name
|
||||
}
|
||||
url
|
||||
icon
|
||||
detail {
|
||||
html
|
||||
}
|
||||
messagePreview {
|
||||
value
|
||||
highlights {
|
||||
line
|
||||
character
|
||||
length
|
||||
}
|
||||
}
|
||||
diffPreview {
|
||||
value
|
||||
highlights {
|
||||
line
|
||||
character
|
||||
length
|
||||
}
|
||||
}
|
||||
commit {
|
||||
id
|
||||
repository {
|
||||
name
|
||||
url
|
||||
}
|
||||
oid
|
||||
abbreviatedOID
|
||||
author {
|
||||
person {
|
||||
displayName
|
||||
avatarURL
|
||||
}
|
||||
date
|
||||
}
|
||||
message
|
||||
matches {
|
||||
url
|
||||
tree(path: "") {
|
||||
canonicalURL
|
||||
body {
|
||||
text
|
||||
html
|
||||
}
|
||||
highlights {
|
||||
line
|
||||
character
|
||||
length
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,3 +426,38 @@ export function deleteSavedQuery(
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const highlightCode = memoizeObservable(
|
||||
(ctx: {
|
||||
code: string
|
||||
fuzzyLanguage: string
|
||||
disableTimeout: boolean
|
||||
isLightTheme: boolean
|
||||
}): Observable<string> =>
|
||||
queryGraphQL(
|
||||
gql`
|
||||
query highlightCode(
|
||||
$code: String!
|
||||
$fuzzyLanguage: String!
|
||||
$disableTimeout: Boolean!
|
||||
$isLightTheme: Boolean!
|
||||
) {
|
||||
highlightCode(
|
||||
code: $code
|
||||
fuzzyLanguage: $fuzzyLanguage
|
||||
disableTimeout: $disableTimeout
|
||||
isLightTheme: $isLightTheme
|
||||
)
|
||||
}
|
||||
`,
|
||||
ctx
|
||||
).pipe(
|
||||
map(({ data, errors }) => {
|
||||
if (!data || !data.highlightCode) {
|
||||
throw createAggregateError(errors)
|
||||
}
|
||||
return data.highlightCode
|
||||
})
|
||||
),
|
||||
ctx => `${ctx.code}:${ctx.fuzzyLanguage}:${ctx.disableTimeout}:${ctx.isLightTheme}`
|
||||
)
|
||||
|
||||
@ -19,10 +19,9 @@ import { SettingsCascadeProps } from '../../../../shared/src/settings/settings'
|
||||
import { ErrorLike, isErrorLike } from '../../../../shared/src/util/errors'
|
||||
import { isDefined } from '../../../../shared/src/util/types'
|
||||
import { ModalContainer } from '../../components/ModalContainer'
|
||||
import { SearchResult } from '../../components/SearchResult'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { SavedQueryCreateForm } from '../saved-queries/SavedQueryCreateForm'
|
||||
import { CommitSearchResult } from './CommitSearchResult'
|
||||
import { RepositorySearchResult } from './RepositorySearchResult'
|
||||
import { SearchResultsInfoBar } from './SearchResultsInfoBar'
|
||||
|
||||
const isSearchResults = (val: any): val is GQL.ISearchResults => val && val.__typename === 'SearchResults'
|
||||
@ -405,10 +404,11 @@ export class SearchResultsList extends React.PureComponent<SearchResultsListProp
|
||||
)
|
||||
}
|
||||
|
||||
private renderResult(result: GQL.SearchResult, expanded: boolean): JSX.Element | undefined {
|
||||
private renderResult(
|
||||
result: GQL.GenericSearchResultInterface | GQL.IFileMatch,
|
||||
expanded: boolean
|
||||
): JSX.Element | undefined {
|
||||
switch (result.__typename) {
|
||||
case 'Repository':
|
||||
return <RepositorySearchResult key={'repo:' + result.id} result={result} onSelect={this.logEvent} />
|
||||
case 'FileMatch':
|
||||
return (
|
||||
<FileMatch
|
||||
@ -423,19 +423,8 @@ export class SearchResultsList extends React.PureComponent<SearchResultsListProp
|
||||
fetchHighlightedFileLines={this.props.fetchHighlightedFileLines}
|
||||
/>
|
||||
)
|
||||
case 'CommitSearchResult':
|
||||
return (
|
||||
<CommitSearchResult
|
||||
key={'commit:' + result.commit.id}
|
||||
location={this.props.location}
|
||||
result={result}
|
||||
onSelect={this.logEvent}
|
||||
expanded={expanded}
|
||||
allExpanded={this.props.allExpanded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
return <SearchResult key={result.url} result={result} isLightTheme={this.props.isLightTheme} />
|
||||
}
|
||||
|
||||
/** onBottomHit increments the amount of results to be shown when we have scrolled to the bottom of the list. */
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@ -1709,6 +1709,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd"
|
||||
integrity sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA==
|
||||
|
||||
"@types/he@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@types/he/-/he-1.1.0.tgz#0ddc2ae80f0814f729f0f7e5aa77b191ab4a9598"
|
||||
integrity sha512-HyiLOiJhclRBPzcbYrNThdi0JOdq7bT4hq9jFBPQk4HGjzkwYVQnMj9IDi7qvYkg9QTly2oZ9kjm4j7d8Ic9eA==
|
||||
|
||||
"@types/highlight.js@9.12.3", "@types/highlight.js@^9.12.3":
|
||||
version "9.12.3"
|
||||
resolved "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca"
|
||||
@ -1719,6 +1724,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220"
|
||||
integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==
|
||||
|
||||
"@types/htmlparser2@*":
|
||||
version "3.7.31"
|
||||
resolved "https://registry.npmjs.org/@types/htmlparser2/-/htmlparser2-3.7.31.tgz#ae89353691ce37fa2463c3b8b4698f20ef67a59b"
|
||||
integrity sha512-6Kjy02k+KfJJE2uUiCytS31SXCYnTjKA+G0ydb83DTlMFzorBlezrV2XiKazRO5HSOEvVW3cpzDFPoP9n/9rSA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/http-proxy-middleware@*":
|
||||
version "0.17.6"
|
||||
resolved "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-0.17.6.tgz#9d1fcb45d8d74b1d4ac24b2cc31553d5389aea38"
|
||||
@ -1957,6 +1969,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37"
|
||||
integrity sha512-LqJkY4VQ7S09XhI7kA3ON71AxauROhSv74639VsNXC9ish4IWHnIi98if+nP1MxQV3RMPqXSCYgpPsDHjlg9UQ==
|
||||
|
||||
"@types/sanitize-html@^1.18.2":
|
||||
version "1.18.2"
|
||||
resolved "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-1.18.2.tgz#14e9971064d0f29aa4feaa8421122ced9e8346d9"
|
||||
integrity sha512-WSE/HsqOHfHd1c0vPOOWOWNippsscBU72r5tpWT/+pFL3zBiCPJCp0NO7sQT8V0gU0xjSKpMAve3iMEJrRhUWQ==
|
||||
dependencies:
|
||||
"@types/htmlparser2" "*"
|
||||
|
||||
"@types/semver@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
||||
@ -7546,7 +7565,7 @@ he@1.1.1:
|
||||
resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||
integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
|
||||
|
||||
he@^1.1.1:
|
||||
he@^1.1.1, he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user