diff --git a/main.go b/main.go index a04768a..40c3aa5 100644 --- a/main.go +++ b/main.go @@ -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 } diff --git a/print.go b/print.go index 82cfe02..b2b60f7 100644 --- a/print.go +++ b/print.go @@ -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}} } diff --git a/search.go b/search.go index ed3c5e7..78e2fac 100644 --- a/search.go +++ b/search.go @@ -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 } diff --git a/search_test.go b/search_test.go index 2c96744..b1f8e66 100644 --- a/search_test.go +++ b/search_test.go @@ -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) +} diff --git a/stringify.go b/stringify.go index 87aa304..0a93903 100644 --- a/stringify.go +++ b/stringify.go @@ -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 } diff --git a/stringify_test.go b/stringify_test.go index c0cc420..8ac775a 100644 --- a/stringify_test.go +++ b/stringify_test.go @@ -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) }