From 68212c0c7af15738f44017db7d5e5cb617272a3b Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Thu, 7 Apr 2022 10:32:34 +0200 Subject: [PATCH] Refactor search to use search ranges --- go.mod | 6 +- go.sum | 15 +++- main.go | 16 +++- print.go | 76 ++++++++-------- search.go | 229 +++++++++++++++++++++++++++++++++---------------- search_test.go | 104 ++++++++++++++++++++++ 6 files changed, 335 insertions(+), 111 deletions(-) diff --git a/go.mod b/go.mod index 211651b..4305593 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,22 @@ require ( github.com/charmbracelet/bubbles v0.10.3 github.com/charmbracelet/bubbletea v0.20.0 github.com/charmbracelet/lipgloss v0.5.0 + github.com/stretchr/testify v1.7.1 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/containerd/console v1.0.3 // indirect + github.com/davecgh/go-spew v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect + golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 053bcd1..34ae021 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0 github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= @@ -33,18 +35,27 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 246847f..a04768a 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "golang.org/x/term" "os" "path" + "regexp" "strings" ) @@ -113,11 +114,22 @@ 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) } - //os.Exit(m.exitCode) + os.Exit(m.exitCode) } type model struct { @@ -154,7 +166,7 @@ type model struct { showSearchResults bool searchResults []*searchResult searchResultsCursor int - searchResultsIndex map[string]searchResultGroup + highlightIndex map[string]*rangeGroup } func (m *model) Init() tea.Cmd { diff --git a/print.go b/print.go index 6d0279e..82cfe02 100644 --- a/print.go +++ b/print.go @@ -20,25 +20,29 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri m.connect(path, lineNumber) ident := strings.Repeat(" ", level) subident := strings.Repeat(" ", level-1) - sri := m.searchResultsIndex[path] + highlight := m.highlightIndex[path] + var searchValue []*foundRange + if highlight != nil { + searchValue = highlight.value + } switch v.(type) { case nil: - return []string{merge(m.explode("null", sri.value, colors.null, path, selectableValues))} + return []string{merge(m.explode("null", searchValue, colors.null, path, selectableValues))} case bool: if v.(bool) { - return []string{merge(m.explode("true", sri.value, colors.boolean, path, selectableValues))} + return []string{merge(m.explode("true", searchValue, colors.boolean, path, selectableValues))} } else { - return []string{merge(m.explode("false", sri.value, colors.boolean, path, selectableValues))} + return []string{merge(m.explode("false", searchValue, colors.boolean, path, selectableValues))} } case number: - return []string{merge(m.explode(v.(number).String(), sri.value, colors.number, path, selectableValues))} + return []string{merge(m.explode(v.(number).String(), searchValue, colors.number, path, selectableValues))} case string: line := fmt.Sprintf("%q", v) - chunks := m.explode(line, sri.value, colors.string, path, selectableValues) + chunks := m.explode(line, searchValue, colors.string, path, selectableValues) if m.wrap && keyEndPos+width(line) > m.width { return wrapLines(chunks, keyEndPos, m.width, subident) } @@ -49,39 +53,43 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri if !m.expandedPaths[path] { return []string{m.preview(v, path, selectableValues)} } - output := []string{m.printOpenBracket("{", sri, path, selectableValues)} + output := []string{m.printOpenBracket("{", highlight, path, selectableValues)} lineNumber++ // bracket is on separate line keys := v.(*dict).keys for i, k := range keys { subpath := path + "." + k - s := m.searchResultsIndex[subpath] + highlight := m.highlightIndex[subpath] + var searchKey []*foundRange + if highlight != nil { + searchKey = highlight.key + } m.connect(subpath, lineNumber) key := fmt.Sprintf("%q", k) - key = merge(m.explode(key, s.key, colors.key, subpath, true)) + key = merge(m.explode(key, searchKey, colors.key, subpath, true)) value, _ := v.(*dict).get(k) - delim := m.printDelim(": ", s) + delim := m.printDelim(": ", highlight) keyEndPos := width(ident) + width(key) + width(delim) lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false) lines[0] = ident + key + delim + lines[0] if i < len(keys)-1 { - lines[len(lines)-1] += m.printComma(",", s) + lines[len(lines)-1] += m.printComma(",", highlight) } output = append(output, lines...) lineNumber += len(lines) } - output = append(output, subident+m.printCloseBracket("}", sri, path, false)) + output = append(output, subident+m.printCloseBracket("}", highlight, path, false)) return output case array: if !m.expandedPaths[path] { return []string{m.preview(v, path, selectableValues)} } - output := []string{m.printOpenBracket("[", sri, path, selectableValues)} + output := []string{m.printOpenBracket("[", highlight, path, selectableValues)} lineNumber++ // bracket is on separate line slice := v.(array) for i, value := range slice { subpath := fmt.Sprintf("%v[%v]", path, i) - s := m.searchResultsIndex[subpath] + s := m.highlightIndex[subpath] m.connect(subpath, lineNumber) lines := m.print(value, level+1, lineNumber, width(ident), subpath, true) lines[0] = ident + lines[0] @@ -91,7 +99,7 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri lineNumber += len(lines) output = append(output, lines...) } - output = append(output, subident+m.printCloseBracket("]", sri, path, false)) + output = append(output, subident+m.printCloseBracket("]", highlight, path, false)) return output default: @@ -100,7 +108,7 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri } func (m *model) preview(v interface{}, path string, selectableValues bool) string { - searchResult := m.searchResultsIndex[path] + searchResult := m.highlightIndex[path] previewStyle := colors.preview if selectableValues && m.cursorPath() == path { previewStyle = colors.cursor @@ -183,12 +191,12 @@ func (w withStyle) Render(s string) string { return w.style.Render(s) } -func (m *model) printOpenBracket(line string, s searchResultGroup, path string, selectableValues bool) string { +func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string { if selectableValues && m.cursorPath() == path { return colors.cursor.Render(line) } - if s.openBracket != nil { - if s.openBracket.index == m.searchResultsCursor { + if s != nil && s.openBracket != nil { + if s.openBracket.parent.index == m.searchResultsCursor { return colors.cursor.Render(line) } else { return colors.search.Render(line) @@ -198,12 +206,12 @@ func (m *model) printOpenBracket(line string, s searchResultGroup, path string, } } -func (m *model) printCloseBracket(line string, s searchResultGroup, path string, selectableValues bool) string { +func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selectableValues bool) string { if selectableValues && m.cursorPath() == path { return colors.cursor.Render(line) } - if s.closeBracket != nil { - if s.closeBracket.index == m.searchResultsCursor { + if s != nil && s.closeBracket != nil { + if s.closeBracket.parent.index == m.searchResultsCursor { return colors.cursor.Render(line) } else { return colors.search.Render(line) @@ -213,9 +221,9 @@ func (m *model) printCloseBracket(line string, s searchResultGroup, path string, } } -func (m *model) printDelim(line string, s searchResultGroup) string { - if s.delim != nil { - if s.delim.index == m.searchResultsCursor { +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) @@ -225,9 +233,9 @@ func (m *model) printDelim(line string, s searchResultGroup) string { } } -func (m *model) printComma(line string, s searchResultGroup) string { - if s.comma != nil { - if s.comma.index == m.searchResultsCursor { +func (m *model) printComma(line string, s *rangeGroup) string { + if s != nil && s.comma != nil { + if s.comma.parent.index == m.searchResultsCursor { return colors.cursor.Render(line) } else { return colors.search.Render(line) @@ -242,27 +250,27 @@ type withStyle struct { style lipgloss.Style } -func (m *model) explode(line string, searchResults []*searchResult, 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 { return []withStyle{{line, colors.cursor}} } out := make([]withStyle, 0, 1) pos := 0 - for _, sr := range searchResults { + for _, r := range highlightRanges { style := colors.search - if sr.index == m.searchResultsCursor { + if r.parent.index == m.searchResultsCursor { style = colors.cursor } out = append(out, withStyle{ - value: line[pos:sr.start], + value: line[pos:r.start], style: defaultStyle, }) out = append(out, withStyle{ - value: line[sr.start:sr.end], + value: line[r.start:r.end], style: style, }) - pos = sr.end + pos = r.end } out = append(out, withStyle{ value: line[pos:], diff --git a/search.go b/search.go index ada7e30..ed3c5e7 100644 --- a/search.go +++ b/search.go @@ -1,84 +1,169 @@ package main +import ( + "fmt" + "regexp" +) + type searchResult struct { - path string - index int - start, end int + path string + index int + ranges []*foundRange } -type searchResultGroup struct { - key []*searchResult - value []*searchResult - delim *searchResult - openBracket *searchResult - closeBracket *searchResult - comma *searchResult +type rangeKind int + +const ( + keyRange rangeKind = 1 + iota + valueRange + delimRange + openBracketRange + closeBracketRange + commaRange +) + +type foundRange struct { + parent *searchResult + start, end int + kind rangeKind } -// TODO: Implement search. -// TODO: Uncomment all code blocks. +type rangeGroup struct { + key []*foundRange + value []*foundRange + delim *foundRange + openBracket *foundRange + closeBracket *foundRange + comma *foundRange +} func (m *model) doSearch(s string) { - //re, err := regexp.Compile("(?i)" + s) - //if err != nil { - // m.searchRegexCompileError = err.Error() - // m.searchInput.Blur() - // return - //} - //m.searchRegexCompileError = "" - //results := newDict() - //addSearchResult := func(path string, indexes [][]int) { - // if indexes != nil { - // sr := searchResult{} - // prev, ok := results.get(path) - // if ok { - // sr = prev.(searchResult) - // } - // sr.value = indexes - // results.set(path, sr) - // } - //} - // - //dfs(m.json, func(it iterator) { - // switch it.object.(type) { - // case nil: - // line := "null" - // found := re.FindAllStringIndex(line, -1) - // addSearchResult(it.path, found) - // case bool: - // line := stringify(it.object) - // found := re.FindAllStringIndex(line, -1) - // addSearchResult(it.path, found) - // case number: - // line := it.object.(number).String() - // found := re.FindAllStringIndex(line, -1) - // addSearchResult(it.path, found) - // case string: - // line := fmt.Sprintf("%q", it.object) - // found := re.FindAllStringIndex(line, -1) - // addSearchResult(it.path, found) - // case *dict: - // keys := it.object.(*dict).keys - // for _, key := range keys { - // line := fmt.Sprintf("%q", key) - // subpath := it.path + "." + key - // indexes := re.FindAllStringIndex(line, -1) - // if indexes != nil { - // sr := searchResult{} - // prev, ok := results.get(subpath) - // if ok { - // sr = prev.(searchResult) - // } - // sr.key = indexes - // results.set(subpath, sr) - // } - // } - // } - //}) - //m.searchResults = results - //m.searchInput.Blur() - //m.showSearchResults = true - //m.jumpToSearchResult(0) + m.searchRegexCompileError = "" + re, err := regexp.Compile("(?i)" + s) + if err != nil { + m.searchRegexCompileError = err.Error() + m.searchInput.Blur() + return + } + indexes := re.FindAllStringIndex(stringify(m.json), -1) + m.remapSearchResult(m.json, "", 0, indexes, 0, nil) + m.indexSearchResults() + m.searchInput.Blur() + m.showSearchResults = true + m.jumpToSearchResult(0) +} + +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 + + case bool: + if object.(bool) { + return pos + len("true"), id, current + } else { + return pos + len("false"), id, current + } + + case number: + return pos + len(object.(number).String()), 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: + 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 := ": " + 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(", ") + } + } + pos++ // } + return pos, id, current + + case array: + 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(", ") + } + } + pos++ // ] + return pos, id, current + default: + panic("unexpected object type") + } +} + +func (m *model) findRanges(kind rangeKind, s string, path string, pos int, indexes [][]int, id int, current *searchResult) (int, *searchResult) { + for ; id < len(indexes); id++ { + start, end := indexes[id][0]-pos, indexes[id][1]-pos + if end <= 0 { + current = nil + continue + } + if start < len(s) { + if current == nil { + current = &searchResult{ + path: path, + index: len(m.searchResults), + } + m.searchResults = append(m.searchResults, current) + } + found := &foundRange{ + parent: current, + start: max(start, 0), + end: min(end, len(s)), + kind: kind, + } + current.ranges = append(current.ranges, found) + if end < len(s) { + current = nil + } else { + break + } + } else { + break + } + } + return id, current +} + +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] + if !exist { + highlight = &rangeGroup{} + m.highlightIndex[r.parent.path] = highlight + } + switch r.kind { + case keyRange: + highlight.key = append(highlight.key, r) + case valueRange: + highlight.value = append(highlight.value, r) + case delimRange: + highlight.delim = r + case openBracketRange: + highlight.openBracket = r + case closeBracketRange: + highlight.closeBracket = r + case commaRange: + highlight.comma = r + } + } + } } func (m *model) jumpToSearchResult(at int) { diff --git a/search_test.go b/search_test.go index 06ab7d0..2c96744 100644 --- a/search_test.go +++ b/search_test.go @@ -1 +1,105 @@ package main + +import ( + "github.com/stretchr/testify/require" + "regexp" + "testing" +) + +func Test_model_remapSearchResult_array(t *testing.T) { + msg := ` + ["first", "second"] + ^^^^^ ^^^^^^ +` + m := &model{ + json: array{"first", "second"}, + } + re, _ := regexp.Compile("\\w+") + indexes := re.FindAllStringIndex(stringify(m.json), -1) + m.remapSearchResult(m.json, "", 0, indexes, 0, nil) + + s1 := &searchResult{path: "[0]"} + s1.ranges = append(s1.ranges, + &foundRange{ + parent: s1, + start: 1, + end: 6, + kind: valueRange, + }, + ) + s2 := &searchResult{path: "[1]", index: 1} + s2.ranges = append(s2.ranges, + &foundRange{ + parent: s2, + start: 1, + end: 7, + kind: valueRange, + }, + ) + require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg) +} + +func Test_model_remapSearchResult_between_array(t *testing.T) { + msg := ` + ["first", "second"] + ^^^^^^^^^^^^^^^ +` + m := &model{ + json: array{"first", "second"}, + } + re, _ := regexp.Compile("\\w.+\\w") + indexes := re.FindAllStringIndex(stringify(m.json), -1) + m.remapSearchResult(m.json, "", 0, indexes, 0, nil) + + s := &searchResult{path: "[0]"} + s.ranges = append(s.ranges, + &foundRange{ + parent: s, + start: 1, + end: 7, + kind: valueRange, + }, + &foundRange{ + parent: s, + start: 0, + end: 7, + kind: valueRange, + }, + ) + require.Equal(t, []*searchResult{s}, m.searchResults, msg) +} + +func Test_model_remapSearchResult_dict(t *testing.T) { + msg := ` + {"key": "hello world"} + ^^^^^ ^^^^^^^^^^^^^ +` + d := newDict() + d.set("key", "hello world") + m := &model{ + json: d, + } + re, _ := regexp.Compile("\"[\\w\\s]+\"") + indexes := re.FindAllStringIndex(stringify(m.json), -1) + m.remapSearchResult(m.json, "", 0, indexes, 0, nil) + + s1 := &searchResult{path: ".key"} + s1.ranges = append(s1.ranges, + &foundRange{ + parent: s1, + start: 0, + end: 5, + kind: keyRange, + }, + ) + s2 := &searchResult{path: ".key", index: 1} + s2.ranges = append(s2.ranges, + &foundRange{ + parent: s2, + start: 0, + end: 13, + kind: valueRange, + }, + ) + require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg) +}