Highlight matches in table and content
When `--filter` is passed, matches will be highlighted in the existing table. If file names match, the "n file(s)" cell will be highlighted. When `--include-content` is additionally passed, the file name, description, and/or content will be printed like `search code` with matches highlighted.
This commit is contained in:
parent
86f045ef0e
commit
bddadef574
2 changed files with 227 additions and 28 deletions
|
|
@ -49,7 +49,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
or even the content of files in the gist using %[1]s--filter%[1]s.
|
||||
|
||||
Use %[1]s--include-content%[1]s to include content of files, noting that
|
||||
this will be slower and increase the rate limit used.
|
||||
this will be slower and increase the rate limit used. Instead of printing a table,
|
||||
code will be printed with highlights.
|
||||
|
||||
For supported regular expression syntax, see https://pkg.go.dev/regexp/syntax
|
||||
`, "`"),
|
||||
|
|
@ -136,17 +137,39 @@ func listRun(opts *ListOptions) error {
|
|||
fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err)
|
||||
}
|
||||
|
||||
if opts.Filter != nil {
|
||||
return printHighlights(opts.IO, gists, opts.Filter)
|
||||
if opts.Filter != nil && opts.IncludeContent {
|
||||
return printContent(opts.IO, gists, opts.Filter)
|
||||
}
|
||||
|
||||
return printTable(opts.IO, gists)
|
||||
return printTable(opts.IO, gists, opts.Filter)
|
||||
}
|
||||
|
||||
func printTable(io *iostreams.IOStreams, gists []shared.Gist) error {
|
||||
func printTable(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Regexp) error {
|
||||
cs := io.ColorScheme()
|
||||
tp := tableprinter.New(io, tableprinter.WithHeader("ID", "DESCRIPTION", "FILES", "VISIBILITY", "UPDATED"))
|
||||
|
||||
// Highlight filter matches in the description when printing the table.
|
||||
highlightDescription := func(s string) string {
|
||||
if filter != nil {
|
||||
if str, err := highlightMatch(s, filter, nil, cs.Bold, cs.Highlight); err == nil {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return cs.Bold(s)
|
||||
}
|
||||
|
||||
// Highlight the files column when any file name matches the filter.
|
||||
highlightFilesFunc := func(gist *shared.Gist) func(string) string {
|
||||
if filter != nil {
|
||||
for _, file := range gist.Files {
|
||||
if filter.MatchString(file.Filename) {
|
||||
return cs.Highlight
|
||||
}
|
||||
}
|
||||
}
|
||||
return normal
|
||||
}
|
||||
|
||||
for _, gist := range gists {
|
||||
fileCount := len(gist.Files)
|
||||
|
||||
|
|
@ -170,9 +193,12 @@ func printTable(io *iostreams.IOStreams, gists []shared.Gist) error {
|
|||
tp.AddField(gist.ID)
|
||||
tp.AddField(
|
||||
text.RemoveExcessiveWhitespace(description),
|
||||
tableprinter.WithColor(cs.Bold),
|
||||
tableprinter.WithColor(highlightDescription),
|
||||
)
|
||||
tp.AddField(
|
||||
text.Pluralize(fileCount, "file"),
|
||||
tableprinter.WithColor(highlightFilesFunc(&gist)),
|
||||
)
|
||||
tp.AddField(text.Pluralize(fileCount, "file"))
|
||||
tp.AddField(visibility, tableprinter.WithColor(visColor))
|
||||
tp.AddTimeField(time.Now(), gist.UpdatedAt, cs.Gray)
|
||||
tp.EndRow()
|
||||
|
|
@ -181,7 +207,7 @@ func printTable(io *iostreams.IOStreams, gists []shared.Gist) error {
|
|||
return tp.Render()
|
||||
}
|
||||
|
||||
func printHighlights(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Regexp) error {
|
||||
func printContent(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Regexp) error {
|
||||
const tab string = " "
|
||||
cs := io.ColorScheme()
|
||||
|
||||
|
|
@ -191,39 +217,36 @@ func printHighlights(io *iostreams.IOStreams, gists []shared.Gist, filter *regex
|
|||
split := func(r rune) bool {
|
||||
return r == '\n' || r == '\r'
|
||||
}
|
||||
text := func(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
for _, gist := range gists {
|
||||
for _, file := range gist.Files {
|
||||
found := false
|
||||
matched := false
|
||||
out.Reset()
|
||||
|
||||
if filename, err = highlightMatch(file.Filename, filter, &found, cs.Green, cs.Highlight); err != nil {
|
||||
if filename, err = highlightMatch(file.Filename, filter, &matched, cs.Green, cs.Highlight); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(out, cs.Blue(gist.ID), filename)
|
||||
|
||||
if gist.Description != "" {
|
||||
if description, err = highlightMatch(gist.Description, filter, &found, cs.Bold, cs.Highlight); err != nil {
|
||||
if description, err = highlightMatch(gist.Description, filter, &matched, cs.Bold, cs.Highlight); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(out, tab, description)
|
||||
fmt.Fprintf(out, "%s%s\n", tab, description)
|
||||
}
|
||||
|
||||
if file.Content != "" {
|
||||
for _, line := range strings.FieldsFunc(file.Content, split) {
|
||||
if filter.MatchString(line) {
|
||||
if line, err = highlightMatch(line, filter, &found, text, cs.Highlight); err != nil {
|
||||
if line, err = highlightMatch(line, filter, &matched, normal, cs.Highlight); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(out, tab, tab, line)
|
||||
fmt.Fprintf(out, "%[1]s%[1]s%[2]s\n", tab, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if matched {
|
||||
fmt.Fprintln(io.Out, out.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -232,7 +255,7 @@ func printHighlights(io *iostreams.IOStreams, gists []shared.Gist, filter *regex
|
|||
return nil
|
||||
}
|
||||
|
||||
func highlightMatch(s string, filter *regexp.Regexp, found *bool, color, highlight func(string) string) (string, error) {
|
||||
func highlightMatch(s string, filter *regexp.Regexp, matched *bool, color, highlight func(string) string) (string, error) {
|
||||
matches := filter.FindAllStringIndex(s, -1)
|
||||
if matches == nil {
|
||||
return color(s), nil
|
||||
|
|
@ -252,6 +275,13 @@ func highlightMatch(s string, filter *regexp.Regexp, found *bool, color, highlig
|
|||
}
|
||||
}
|
||||
|
||||
*found = *found || true
|
||||
if matched != nil {
|
||||
*matched = *matched || true
|
||||
}
|
||||
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func normal(s string) string {
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ func Test_listRun(t *testing.T) {
|
|||
wantErr bool
|
||||
wantOut string
|
||||
stubs func(*httpmock.Registry)
|
||||
color bool
|
||||
nontty bool
|
||||
}{
|
||||
{
|
||||
|
|
@ -390,12 +391,62 @@ func Test_listRun(t *testing.T) {
|
|||
nontty: true,
|
||||
},
|
||||
{
|
||||
name: "with content filter",
|
||||
name: "filtered",
|
||||
opts: &ListOptions{
|
||||
Filter: regexp.MustCompile("octo"),
|
||||
IncludeContent: true,
|
||||
Visibility: "all",
|
||||
Filter: regexp.MustCompile("octo"),
|
||||
Visibility: "all",
|
||||
},
|
||||
nontty: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" }
|
||||
],
|
||||
"description": "octo match in the description",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "2345",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" },
|
||||
{ "name": "octo.txt", "text": "bar" }
|
||||
],
|
||||
"description": "match in the file name",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": false
|
||||
},
|
||||
{
|
||||
"name": "3456",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "octo in the text" }
|
||||
],
|
||||
"description": "match in the file text",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
absTime.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
},
|
||||
wantOut: heredoc.Docf(`
|
||||
1234%[1]socto match in the description%[1]s1 file%[1]spublic%[1]s2020-07-30T15:24:28Z
|
||||
2345%[1]smatch in the file name%[1]s2 files%[1]ssecret%[1]s2020-07-30T15:24:28Z
|
||||
`, "\t"),
|
||||
},
|
||||
{
|
||||
name: "filtered (tty)",
|
||||
opts: &ListOptions{
|
||||
Filter: regexp.MustCompile("octo"),
|
||||
Visibility: "all",
|
||||
},
|
||||
color: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
|
|
@ -434,13 +485,130 @@ func Test_listRun(t *testing.T) {
|
|||
)),
|
||||
)
|
||||
},
|
||||
wantOut: heredoc.Docf(`
|
||||
%[1]s[0;2;4;37mID %[1]s[0m %[1]s[0;2;4;37mDESCRIPTION %[1]s[0m %[1]s[0;2;4;37mFILES %[1]s[0m %[1]s[0;2;4;37mVISIBILITY%[1]s[0m %[1]s[0;2;4;37mUPDATED %[1]s[0m
|
||||
1234 %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m 1 file %[1]s[0;32mpublic %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m
|
||||
2345 %[1]s[0;1;39mmatch in the file name %[1]s[0m %[1]s[0;30;43m2 files%[1]s[0m %[1]s[0;31msecret %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m
|
||||
`, "\x1b"),
|
||||
},
|
||||
{
|
||||
name: "filtered with content",
|
||||
opts: &ListOptions{
|
||||
Filter: regexp.MustCompile("octo"),
|
||||
IncludeContent: true,
|
||||
Visibility: "all",
|
||||
},
|
||||
nontty: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" }
|
||||
],
|
||||
"description": "octo match in the description",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "2345",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" },
|
||||
{ "name": "octo.txt", "text": "bar" }
|
||||
],
|
||||
"description": "match in the file name",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": false
|
||||
},
|
||||
{
|
||||
"name": "3456",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "octo in the text" }
|
||||
],
|
||||
"description": "match in the file text",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
absTime.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
},
|
||||
wantOut: heredoc.Doc(`
|
||||
ID DESCRIPTION FILES VISIBILITY UPDATED
|
||||
1234 octo match in the description 1 file public about 6 hours ago
|
||||
2345 match in the file name 2 files secret about 6 hours ago
|
||||
3456 match in the file text 1 file public about 6 hours ago
|
||||
1234 main.txt
|
||||
octo match in the description
|
||||
|
||||
2345 octo.txt
|
||||
match in the file name
|
||||
|
||||
3456 main.txt
|
||||
match in the file text
|
||||
octo in the text
|
||||
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "filtered with content (tty)",
|
||||
opts: &ListOptions{
|
||||
Filter: regexp.MustCompile("octo"),
|
||||
IncludeContent: true,
|
||||
Visibility: "all",
|
||||
},
|
||||
color: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" }
|
||||
],
|
||||
"description": "octo match in the description",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "2345",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "foo" },
|
||||
{ "name": "octo.txt", "text": "bar" }
|
||||
],
|
||||
"description": "match in the file name",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": false
|
||||
},
|
||||
{
|
||||
"name": "3456",
|
||||
"files": [
|
||||
{ "name": "main.txt", "text": "octo in the text" }
|
||||
],
|
||||
"description": "match in the file text",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
},
|
||||
wantOut: heredoc.Docf(`
|
||||
%[1]s[0;34m1234%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
|
||||
%[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m
|
||||
|
||||
%[1]s[0;34m2345%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;32m.txt%[1]s[0m
|
||||
%[1]s[0;1;39mmatch in the file name%[1]s[0m
|
||||
|
||||
%[1]s[0;34m3456%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
|
||||
%[1]s[0;1;39mmatch in the file text%[1]s[0m
|
||||
%[1]s[0;30;43mocto%[1]s[0m in the text
|
||||
|
||||
`, "\x1b"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -456,6 +624,7 @@ func Test_listRun(t *testing.T) {
|
|||
}
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetColorEnabled(tt.color)
|
||||
ios.SetStdoutTTY(!tt.nontty)
|
||||
tt.opts.IO = ios
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue