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"
"os"
"path"
"regexp"
"runtime/pprof"
"strings"
)
@ -41,6 +41,17 @@ var colors = struct {
}
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 := ""
var args []string
var dec *json.Decoder
@ -114,21 +125,13 @@ func main() {
}
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())
if err := p.Start(); err != nil {
panic(err)
}
if cpuProfile != "" {
pprof.StopCPUProfile()
}
os.Exit(m.exitCode)
}
@ -196,7 +199,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEsc:
m.searchInput.Blur()
//m.searchResults = newDict()
m.clearSearchResults()
m.render()
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):
m.showSearchResults = false
m.searchRegexCompileError = ""
m.searchInput.Focus()
m.render()
return m, textinput.Blink
@ -404,13 +408,13 @@ func (m *model) View() string {
if len(m.searchRegexCompileError) > 0 {
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
}
//if m.showSearchResults {
// if len(m.searchResults.keys) == 0 {
// output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
// } else {
// output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults.keys))
// }
//}
if m.showSearchResults {
if len(m.searchResults) == 0 {
output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
} else {
output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults))
}
}
return output
}

@ -59,15 +59,16 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri
for i, k := range keys {
subpath := path + "." + k
highlight := m.highlightIndex[subpath]
var searchKey []*foundRange
var keyRanges, delimRanges []*foundRange
if highlight != nil {
searchKey = highlight.key
keyRanges = highlight.key
delimRanges = highlight.delim
}
m.connect(subpath, lineNumber)
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)
delim := m.printDelim(": ", highlight)
delim := merge(m.explode(": ", delimRanges, colors.syntax, subpath, false))
keyEndPos := width(ident) + width(key) + width(delim)
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
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 {
if s != nil && s.comma != nil {
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 {
if selectable && m.cursorPath() == path {
if selectable && m.cursorPath() == path && m.showCursor {
return []withStyle{{line, colors.cursor}}
}

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

@ -6,10 +6,45 @@ import (
"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 := `
["first", "second"]
^^^^^ ^^^^^^
["first","second"]
^^^^^ ^^^^^^
`
m := &model{
json: array{"first", "second"},
@ -22,6 +57,7 @@ func Test_model_remapSearchResult_array(t *testing.T) {
s1.ranges = append(s1.ranges,
&foundRange{
parent: s1,
path: "[0]",
start: 1,
end: 6,
kind: valueRange,
@ -31,6 +67,7 @@ func Test_model_remapSearchResult_array(t *testing.T) {
s2.ranges = append(s2.ranges,
&foundRange{
parent: s2,
path: "[1]",
start: 1,
end: 7,
kind: valueRange,
@ -39,10 +76,10 @@ func Test_model_remapSearchResult_array(t *testing.T) {
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 := `
["first", "second"]
^^^^^^^^^^^^^^^
["first","second"]
^^^^^^^^^^^^^^
`
m := &model{
json: array{"first", "second"},
@ -55,12 +92,21 @@ func Test_model_remapSearchResult_between_array(t *testing.T) {
s.ranges = append(s.ranges,
&foundRange{
parent: s,
path: "[0]",
start: 1,
end: 7,
kind: valueRange,
},
&foundRange{
parent: s,
path: "[0]",
start: 0,
end: 1,
kind: commaRange,
},
&foundRange{
parent: s,
path: "[1]",
start: 0,
end: 7,
kind: valueRange,
@ -69,7 +115,7 @@ func Test_model_remapSearchResult_between_array(t *testing.T) {
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
}
func Test_model_remapSearchResult_dict(t *testing.T) {
func Test_search_dict(t *testing.T) {
msg := `
{"key": "hello world"}
^^^^^ ^^^^^^^^^^^^^
@ -87,6 +133,7 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
s1.ranges = append(s1.ranges,
&foundRange{
parent: s1,
path: ".key",
start: 0,
end: 5,
kind: keyRange,
@ -96,6 +143,7 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
s2.ranges = append(s2.ranges,
&foundRange{
parent: s2,
path: ".key",
start: 0,
end: 13,
kind: valueRange,
@ -103,3 +151,38 @@ func Test_model_remapSearchResult_dict(t *testing.T) {
)
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 {
line := fmt.Sprintf("%q", key) + ": " + stringify(v.(*dict).values[key])
if i < len(v.(*dict).keys)-1 {
line += ", "
line += ","
}
result += line
}
@ -38,7 +38,7 @@ func stringify(v interface{}) string {
for i, value := range v.(array) {
line := stringify(value)
if i < len(v.(array))-1 {
line += ", "
line += ","
}
result += line
}

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

Loading…
Cancel
Save