Better handling of search edge cases

sleep-stdin-bug
Anton Medvedev 2 years ago
parent 68212c0c7a
commit 49756d7ff4

@ -10,7 +10,7 @@ import (
"golang.org/x/term" "golang.org/x/term"
"os" "os"
"path" "path"
"regexp" "runtime/pprof"
"strings" "strings"
) )
@ -41,6 +41,17 @@ var colors = struct {
} }
func main() { func main() {
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
panic(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
}
filePath := "" filePath := ""
var args []string var args []string
var dec *json.Decoder var dec *json.Decoder
@ -114,21 +125,13 @@ func main() {
} }
m.collectSiblings(m.json, "") m.collectSiblings(m.json, "")
// DEBUG START
re, _ := regexp.Compile("\"[\\w\\s]+\"")
s := stringify(m.json)
indexes := re.FindAllStringIndex(s, -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
m.indexSearchResults()
searchResults := m.searchResults
highlightIndex := m.highlightIndex
fmt.Println(searchResults, highlightIndex)
// DEBUG END
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil { if err := p.Start(); err != nil {
panic(err) panic(err)
} }
if cpuProfile != "" {
pprof.StopCPUProfile()
}
os.Exit(m.exitCode) os.Exit(m.exitCode)
} }
@ -196,7 +199,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.Type { switch msg.Type {
case tea.KeyEsc: case tea.KeyEsc:
m.searchInput.Blur() m.searchInput.Blur()
//m.searchResults = newDict() m.clearSearchResults()
m.render() m.render()
case tea.KeyEnter: case tea.KeyEnter:
@ -349,6 +352,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keyMap.Search): case key.Matches(msg, m.keyMap.Search):
m.showSearchResults = false m.showSearchResults = false
m.searchRegexCompileError = ""
m.searchInput.Focus() m.searchInput.Focus()
m.render() m.render()
return m, textinput.Blink return m, textinput.Blink
@ -404,13 +408,13 @@ func (m *model) View() string {
if len(m.searchRegexCompileError) > 0 { if len(m.searchRegexCompileError) > 0 {
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError) output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
} }
//if m.showSearchResults { if m.showSearchResults {
// if len(m.searchResults.keys) == 0 { if len(m.searchResults) == 0 {
// output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value()) output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
// } else { } else {
// output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults.keys)) output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults))
// } }
//} }
return output return output
} }

@ -59,15 +59,16 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
for i, k := range keys { for i, k := range keys {
subpath := path + "." + k subpath := path + "." + k
highlight := m.highlightIndex[subpath] highlight := m.highlightIndex[subpath]
var searchKey []*foundRange var keyRanges, delimRanges []*foundRange
if highlight != nil { if highlight != nil {
searchKey = highlight.key keyRanges = highlight.key
delimRanges = highlight.delim
} }
m.connect(subpath, lineNumber) m.connect(subpath, lineNumber)
key := fmt.Sprintf("%q", k) key := fmt.Sprintf("%q", k)
key = merge(m.explode(key, searchKey, colors.key, subpath, true)) key = merge(m.explode(key, keyRanges, colors.key, subpath, true))
value, _ := v.(*dict).get(k) value, _ := v.(*dict).get(k)
delim := m.printDelim(": ", highlight) delim := merge(m.explode(": ", delimRanges, colors.syntax, subpath, false))
keyEndPos := width(ident) + width(key) + width(delim) keyEndPos := width(ident) + width(key) + width(delim)
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false) lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
lines[0] = ident + key + delim + lines[0] lines[0] = ident + key + delim + lines[0]
@ -221,18 +222,6 @@ func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selec
} }
} }
func (m *model) printDelim(line string, s *rangeGroup) string {
if s != nil && s.delim != nil {
if s.delim.parent.index == m.searchResultsCursor {
return colors.cursor.Render(line)
} else {
return colors.search.Render(line)
}
} else {
return colors.syntax.Render(line)
}
}
func (m *model) printComma(line string, s *rangeGroup) string { func (m *model) printComma(line string, s *rangeGroup) string {
if s != nil && s.comma != nil { if s != nil && s.comma != nil {
if s.comma.parent.index == m.searchResultsCursor { if s.comma.parent.index == m.searchResultsCursor {
@ -251,7 +240,7 @@ type withStyle struct {
} }
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle lipgloss.Style, path string, selectable bool) []withStyle { func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle lipgloss.Style, path string, selectable bool) []withStyle {
if selectable && m.cursorPath() == path { if selectable && m.cursorPath() == path && m.showCursor {
return []withStyle{{line, colors.cursor}} return []withStyle{{line, colors.cursor}}
} }

@ -23,7 +23,10 @@ const (
) )
type foundRange struct { type foundRange struct {
parent *searchResult parent *searchResult
// Range needs separate path, as for one searchResult's path
// there can be multiple ranges for different paths (within parent path).
path string
start, end int start, end int
kind rangeKind kind rangeKind
} }
@ -31,14 +34,20 @@ type foundRange struct {
type rangeGroup struct { type rangeGroup struct {
key []*foundRange key []*foundRange
value []*foundRange value []*foundRange
delim *foundRange delim []*foundRange
openBracket *foundRange openBracket *foundRange
closeBracket *foundRange closeBracket *foundRange
comma *foundRange comma *foundRange
} }
func (m *model) doSearch(s string) { func (m *model) clearSearchResults() {
m.searchRegexCompileError = "" m.searchRegexCompileError = ""
m.searchResults = nil
m.highlightIndex = nil
}
func (m *model) doSearch(s string) {
m.clearSearchResults()
re, err := regexp.Compile("(?i)" + s) re, err := regexp.Compile("(?i)" + s)
if err != nil { if err != nil {
m.searchRegexCompileError = err.Error() m.searchRegexCompileError = err.Error()
@ -56,50 +65,71 @@ func (m *model) doSearch(s string) {
func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) { func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) {
switch object.(type) { switch object.(type) {
case nil: case nil:
return pos + len("null"), id, current s := "null"
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case bool: case bool:
var s string
if object.(bool) { if object.(bool) {
return pos + len("true"), id, current s = "true"
} else { } else {
return pos + len("false"), id, current s = "false"
} }
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case number: case number:
return pos + len(object.(number).String()), id, current s := object.(number).String()
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case string: case string:
s := fmt.Sprintf("%q", object) s := fmt.Sprintf("%q", object)
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current) id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current return pos + len(s), id, current
case *dict: case *dict:
id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
pos++ // { pos++ // {
for i, k := range object.(*dict).keys { for i, k := range object.(*dict).keys {
subpath := path + "." + k subpath := path + "." + k
key := fmt.Sprintf("%q", k) key := fmt.Sprintf("%q", k)
id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current) id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current)
pos += len(key) pos += len(key)
delim := ": " delim := ": "
id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
pos += len(delim) pos += len(delim)
pos, id, current = m.remapSearchResult(object.(*dict).values[k], subpath, pos, indexes, id, current) pos, id, current = m.remapSearchResult(object.(*dict).values[k], subpath, pos, indexes, id, current)
if i < len(object.(*dict).keys)-1 { if i < len(object.(*dict).keys)-1 {
pos += len(", ") comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)
} }
} }
id, current = m.findRanges(closeBracketRange, "}", path, pos, indexes, id, current)
pos++ // } pos++ // }
return pos, id, current return pos, id, current
case array: case array:
id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
pos++ // [ pos++ // [
for i, v := range object.(array) { for i, v := range object.(array) {
subpath := fmt.Sprintf("%v[%v]", path, i) subpath := fmt.Sprintf("%v[%v]", path, i)
pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current) pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
if i < len(object.(array))-1 { if i < len(object.(array))-1 {
pos += len(", ") comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)
} }
} }
id, current = m.findRanges(closeBracketRange, "]", path, pos, indexes, id, current)
pos++ // ] pos++ // ]
return pos, id, current return pos, id, current
default: default:
panic("unexpected object type") panic("unexpected object type")
} }
@ -122,6 +152,7 @@ func (m *model) findRanges(kind rangeKind, s string, path string, pos int, index
} }
found := &foundRange{ found := &foundRange{
parent: current, parent: current,
path: path,
start: max(start, 0), start: max(start, 0),
end: min(end, len(s)), end: min(end, len(s)),
kind: kind, kind: kind,
@ -143,10 +174,10 @@ func (m *model) indexSearchResults() {
m.highlightIndex = map[string]*rangeGroup{} m.highlightIndex = map[string]*rangeGroup{}
for _, s := range m.searchResults { for _, s := range m.searchResults {
for _, r := range s.ranges { for _, r := range s.ranges {
highlight, exist := m.highlightIndex[r.parent.path] highlight, exist := m.highlightIndex[r.path]
if !exist { if !exist {
highlight = &rangeGroup{} highlight = &rangeGroup{}
m.highlightIndex[r.parent.path] = highlight m.highlightIndex[r.path] = highlight
} }
switch r.kind { switch r.kind {
case keyRange: case keyRange:
@ -154,7 +185,7 @@ func (m *model) indexSearchResults() {
case valueRange: case valueRange:
highlight.value = append(highlight.value, r) highlight.value = append(highlight.value, r)
case delimRange: case delimRange:
highlight.delim = r highlight.delim = append(highlight.delim, r)
case openBracketRange: case openBracketRange:
highlight.openBracket = r highlight.openBracket = r
case closeBracketRange: case closeBracketRange:
@ -167,22 +198,22 @@ func (m *model) indexSearchResults() {
} }
func (m *model) jumpToSearchResult(at int) { func (m *model) jumpToSearchResult(at int) {
//if m.searchResults == nil || len(m.searchResults.keys) == 0 { if len(m.searchResults) == 0 {
// return return
//} }
//m.showCursor = false m.showCursor = false
//m.searchResultsCursor = at % len(m.searchResults.keys) m.searchResultsCursor = at % len(m.searchResults)
//desiredPath := m.searchResults.keys[m.searchResultsCursor] desiredPath := m.searchResults[m.searchResultsCursor].path
//lineNumber, ok := m.pathToLineNumber.get(desiredPath) lineNumber, ok := m.pathToLineNumber[desiredPath]
//if ok { if ok {
// m.cursor = m.pathToLineNumber.indexes[desiredPath] m.cursor = m.pathToIndex[desiredPath]
// m.SetOffset(lineNumber.(int)) m.SetOffset(lineNumber)
// m.render() m.render()
//} else { } else {
// m.expandToPath(desiredPath) m.expandToPath(desiredPath)
// m.render() m.render()
// m.jumpToSearchResult(at) m.jumpToSearchResult(at)
//} }
} }
func (m *model) expandToPath(path string) { func (m *model) expandToPath(path string) {
@ -193,21 +224,22 @@ func (m *model) expandToPath(path string) {
} }
func (m *model) nextSearchResult() { func (m *model) nextSearchResult() {
//m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults.keys)) if len(m.searchResults) > 0 {
m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults))
}
} }
func (m *model) prevSearchResult() { func (m *model) prevSearchResult() {
//i := m.searchResultsCursor - 1 i := m.searchResultsCursor - 1
//if i < 0 { if i < 0 {
// i = len(m.searchResults.keys) - 1 i = len(m.searchResults) - 1
//} }
//m.jumpToSearchResult(i) m.jumpToSearchResult(i)
} }
func (m *model) resultsCursorPath() string { func (m *model) resultsCursorPath() string {
//if m.searchResults == nil || len(m.searchResults.keys) == 0 { if len(m.searchResults) == 0 {
// return "?" return "?"
//} }
//return m.searchResults.keys[m.searchResultsCursor] return m.searchResults[m.searchResultsCursor].path
return ""
} }

@ -6,10 +6,45 @@ import (
"testing" "testing"
) )
func Test_model_remapSearchResult_array(t *testing.T) { func Test_search_values(t *testing.T) {
tests := []struct {
name string
object interface{}
want *foundRange
}{
{name: "null", object: nil},
{name: "true", object: true},
{name: "false", object: false},
{name: "number", object: number("42")},
{name: "string", object: "Hello, World!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &model{
json: tt.object,
}
re, _ := regexp.Compile(".+")
str := stringify(m.json)
indexes := re.FindAllStringIndex(str, -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: ""}
s.ranges = append(s.ranges, &foundRange{
parent: s,
path: "",
start: 0,
end: len(str),
kind: valueRange,
})
require.Equal(t, []*searchResult{s}, m.searchResults)
})
}
}
func Test_search_array(t *testing.T) {
msg := ` msg := `
["first", "second"] ["first","second"]
^^^^^ ^^^^^^ ^^^^^ ^^^^^^
` `
m := &model{ m := &model{
json: array{"first", "second"}, json: array{"first", "second"},
@ -22,6 +57,7 @@ func Test_model_remapSearchResult_array(t *testing.T) {
s1.ranges = append(s1.ranges, s1.ranges = append(s1.ranges,
&foundRange{ &foundRange{
parent: s1, parent: s1,
path: "[0]",
start: 1, start: 1,
end: 6, end: 6,
kind: valueRange, kind: valueRange,
@ -31,6 +67,7 @@ func Test_model_remapSearchResult_array(t *testing.T) {
s2.ranges = append(s2.ranges, s2.ranges = append(s2.ranges,
&foundRange{ &foundRange{
parent: s2, parent: s2,
path: "[1]",
start: 1, start: 1,
end: 7, end: 7,
kind: valueRange, kind: valueRange,
@ -39,10 +76,10 @@ func Test_model_remapSearchResult_array(t *testing.T) {
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg) require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
} }
func Test_model_remapSearchResult_between_array(t *testing.T) { func Test_search_between_array(t *testing.T) {
msg := ` msg := `
["first", "second"] ["first","second"]
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
` `
m := &model{ m := &model{
json: array{"first", "second"}, json: array{"first", "second"},
@ -55,12 +92,21 @@ func Test_model_remapSearchResult_between_array(t *testing.T) {
s.ranges = append(s.ranges, s.ranges = append(s.ranges,
&foundRange{ &foundRange{
parent: s, parent: s,
path: "[0]",
start: 1, start: 1,
end: 7, end: 7,
kind: valueRange, kind: valueRange,
}, },
&foundRange{ &foundRange{
parent: s, parent: s,
path: "[0]",
start: 0,
end: 1,
kind: commaRange,
},
&foundRange{
parent: s,
path: "[1]",
start: 0, start: 0,
end: 7, end: 7,
kind: valueRange, kind: valueRange,
@ -69,7 +115,7 @@ func Test_model_remapSearchResult_between_array(t *testing.T) {
require.Equal(t, []*searchResult{s}, m.searchResults, msg) require.Equal(t, []*searchResult{s}, m.searchResults, msg)
} }
func Test_model_remapSearchResult_dict(t *testing.T) { func Test_search_dict(t *testing.T) {
msg := ` msg := `
{"key": "hello world"} {"key": "hello world"}
^^^^^ ^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^
@ -87,6 +133,7 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
s1.ranges = append(s1.ranges, s1.ranges = append(s1.ranges,
&foundRange{ &foundRange{
parent: s1, parent: s1,
path: ".key",
start: 0, start: 0,
end: 5, end: 5,
kind: keyRange, kind: keyRange,
@ -96,6 +143,7 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
s2.ranges = append(s2.ranges, s2.ranges = append(s2.ranges,
&foundRange{ &foundRange{
parent: s2, parent: s2,
path: ".key",
start: 0, start: 0,
end: 13, end: 13,
kind: valueRange, kind: valueRange,
@ -103,3 +151,38 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
) )
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg) require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
} }
func Test_search_dict_with_array(t *testing.T) {
msg := `
{"first": [1,2],"second": []}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`
d := newDict()
d.set("first", array{number("1"), number("2")})
d.set("second", array{})
m := &model{
json: d,
}
re, _ := regexp.Compile(".+")
indexes := re.FindAllStringIndex(stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: ""}
s.ranges = append(s.ranges,
/* { */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: openBracketRange},
/* "first" */ &foundRange{parent: s, path: ".first", start: 0, end: 7, kind: keyRange},
/* : */ &foundRange{parent: s, path: ".first", start: 0, end: 2, kind: delimRange},
/* [ */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: openBracketRange},
/* 1 */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: valueRange},
/* , */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: commaRange},
/* 2 */ &foundRange{parent: s, path: ".first[1]", start: 0, end: 1, kind: valueRange},
/* ] */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: closeBracketRange},
/* , */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: commaRange},
/* "second" */ &foundRange{parent: s, path: ".second", start: 0, end: 8, kind: keyRange},
/* : */ &foundRange{parent: s, path: ".second", start: 0, end: 2, kind: delimRange},
/* [ */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: openBracketRange},
/* ] */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: closeBracketRange},
/* } */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: closeBracketRange},
)
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
}

@ -27,7 +27,7 @@ func stringify(v interface{}) string {
for i, key := range v.(*dict).keys { for i, key := range v.(*dict).keys {
line := fmt.Sprintf("%q", key) + ": " + stringify(v.(*dict).values[key]) line := fmt.Sprintf("%q", key) + ": " + stringify(v.(*dict).values[key])
if i < len(v.(*dict).keys)-1 { if i < len(v.(*dict).keys)-1 {
line += ", " line += ","
} }
result += line result += line
} }
@ -38,7 +38,7 @@ func stringify(v interface{}) string {
for i, value := range v.(array) { for i, value := range v.(array) {
line := stringify(value) line := stringify(value)
if i < len(v.(array))-1 { if i < len(v.(array))-1 {
line += ", " line += ","
} }
result += line result += line
} }

@ -7,21 +7,21 @@ func Test_stringify(t *testing.T) {
arg := newDict() arg := newDict()
arg.set("a", number("1")) arg.set("a", number("1"))
arg.set("b", number("2")) arg.set("b", number("2"))
want := `{"a": 1, "b": 2}` want := `{"a": 1,"b": 2}`
if got := stringify(arg); got != want { if got := stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want) t.Errorf("stringify() = %v, want %v", got, want)
} }
}) })
t.Run("array", func(t *testing.T) { t.Run("array", func(t *testing.T) {
arg := array{number("1"), number("2")} arg := array{number("1"), number("2")}
want := `[1, 2]` want := `[1,2]`
if got := stringify(arg); got != want { if got := stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want) t.Errorf("stringify() = %v, want %v", got, want)
} }
}) })
t.Run("array_with_dict", func(t *testing.T) { t.Run("array_with_dict", func(t *testing.T) {
arg := array{newDict(), array{}} arg := array{newDict(), array{}}
want := `[{}, []]` want := `[{},[]]`
if got := stringify(arg); got != want { if got := stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want) t.Errorf("stringify() = %v, want %v", got, want)
} }

Loading…
Cancel
Save