diff --git a/dict.go b/dict.go index 04a4d37..efa03cd 100644 --- a/dict.go +++ b/dict.go @@ -7,11 +7,15 @@ type dict struct { } func newDict() *dict { - d := dict{} - d.keys = []string{} - d.indexes = map[string]int{} - d.values = map[string]interface{}{} - return &d + return newDictOfCapacity(0) +} + +func newDictOfCapacity(capacity int) *dict { + return &dict{ + keys: make([]string, 0, capacity), + indexes: make(map[string]int, capacity), + values: make(map[string]interface{}, capacity), + } } func (d *dict) get(key string) (interface{}, bool) { diff --git a/keymap.go b/keymap.go index 4057399..3b6c619 100644 --- a/keymap.go +++ b/keymap.go @@ -9,19 +9,19 @@ type KeyMap struct { PageUp key.Binding HalfPageUp key.Binding HalfPageDown key.Binding + GotoTop key.Binding + GotoBottom key.Binding Down key.Binding Up key.Binding Expand key.Binding Collapse key.Binding - NextSibling key.Binding - PrevSibling key.Binding ExpandRecursively key.Binding CollapseRecursively key.Binding - GotoTop key.Binding - GotoBottom key.Binding - ToggleWrap key.Binding ExpandAll key.Binding CollapseAll key.Binding + NextSibling key.Binding + PrevSibling key.Binding + ToggleWrap key.Binding Search key.Binding Next key.Binding Prev key.Binding @@ -53,6 +53,14 @@ func DefaultKeyMap() KeyMap { key.WithKeys("d", "ctrl+d"), key.WithHelp("", "half page down"), ), + GotoTop: key.NewBinding( + key.WithKeys("g"), + key.WithHelp("", "goto top"), + ), + GotoBottom: key.NewBinding( + key.WithKeys("G"), + key.WithHelp("", "goto bottom"), + ), Down: key.NewBinding( key.WithKeys("down", "j"), key.WithHelp("", "down"), @@ -69,14 +77,6 @@ func DefaultKeyMap() KeyMap { key.WithKeys("left", "h"), key.WithHelp("", "collapse"), ), - NextSibling: key.NewBinding( - key.WithKeys("J"), - key.WithHelp("", "next sibling"), - ), - PrevSibling: key.NewBinding( - key.WithKeys("K"), - key.WithHelp("", "previous sibling"), - ), ExpandRecursively: key.NewBinding( key.WithKeys("L"), key.WithHelp("", "expand recursively"), @@ -85,18 +85,6 @@ func DefaultKeyMap() KeyMap { key.WithKeys("H"), key.WithHelp("", "collapse recursively"), ), - GotoTop: key.NewBinding( - key.WithKeys("g"), - key.WithHelp("", "goto top"), - ), - GotoBottom: key.NewBinding( - key.WithKeys("G"), - key.WithHelp("", "goto bottom"), - ), - ToggleWrap: key.NewBinding( - key.WithKeys("z"), - key.WithHelp("", "toggle strings wrap"), - ), ExpandAll: key.NewBinding( key.WithKeys("e"), key.WithHelp("", "expand all"), @@ -105,6 +93,18 @@ func DefaultKeyMap() KeyMap { key.WithKeys("E"), key.WithHelp("", "collapse all"), ), + NextSibling: key.NewBinding( + key.WithKeys("J"), + key.WithHelp("", "next sibling"), + ), + PrevSibling: key.NewBinding( + key.WithKeys("K"), + key.WithHelp("", "previous sibling"), + ), + ToggleWrap: key.NewBinding( + key.WithKeys("z"), + key.WithHelp("", "toggle strings wrap"), + ), Search: key.NewBinding( key.WithKeys("/"), key.WithHelp("", "search regexp"), diff --git a/main.go b/main.go index 5db0487..934d235 100644 --- a/main.go +++ b/main.go @@ -13,9 +13,11 @@ import ( "strings" ) +type number = json.Number + var colors = struct { cursor lipgloss.Style - bracket lipgloss.Style + syntax lipgloss.Style key lipgloss.Style null lipgloss.Style boolean lipgloss.Style @@ -26,7 +28,7 @@ var colors = struct { search lipgloss.Style }{ cursor: lipgloss.NewStyle().Reverse(true), - bracket: lipgloss.NewStyle(), + syntax: lipgloss.NewStyle(), key: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")), null: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")), boolean: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")), @@ -39,7 +41,7 @@ var colors = struct { func main() { filePath := "" - args := []string{} + var args []string var dec *json.Decoder if term.IsTerminal(int(os.Stdin.Fd())) { if len(os.Args) >= 2 { @@ -85,9 +87,10 @@ func main() { parents[it.path] = it.parent children[it.parent] = append(children[it.parent], it.path) switch it.object.(type) { - case *dict, array: - canBeExpanded[it.path] = true - + case *dict: + canBeExpanded[it.path] = len(it.object.(*dict).keys) > 0 + case array: + canBeExpanded[it.path] = len(it.object.(array)) > 0 } }) input := textinput.New() @@ -103,15 +106,18 @@ func main() { canBeExpanded: canBeExpanded, parents: parents, children: children, + nextSiblings: map[string]string{}, + prevSiblings: map[string]string{}, wrap: true, searchInput: input, } + m.collectSiblings(m.json, "") 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 { @@ -119,6 +125,7 @@ type model struct { width, height int windowHeight int footerHeight int + wrap bool fileName string json interface{} @@ -130,21 +137,22 @@ type model struct { keyMap KeyMap showHelp bool - expandedPaths map[string]bool // a set with expanded paths - canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array) - pathToLineNumber *dict // dict with path => line number - lineNumberToPath map[int]string // map of line number => path - parents map[string]string // map of subpath => parent path - children map[string][]string // map of path => child paths - cursor int // cursor in range of m.pathToLineNumber.keys slice - showCursor bool + expandedPaths map[string]bool // a set with expanded paths + canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array) + pathToLineNumber *dict // dict with path => line number + lineNumberToPath map[int]string // map of line number => path + parents map[string]string // map of subpath => parent path + children map[string][]string // map of path => child paths + nextSiblings, prevSiblings map[string]string // map of path => sibling path + cursor int // cursor in range of m.pathToLineNumber.keys slice + showCursor bool - wrap bool searchInput textinput.Model searchRegexCompileError string - searchResults *dict // path => searchResult showSearchResults bool - resultsCursor int // [0, searchResults length) + searchResults []*searchResult + searchResultsCursor int + searchResultsIndex map[string]searchResultGroup } func (m *model) Init() tea.Cmd { @@ -174,7 +182,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.searchResults = newDict() m.render() case tea.KeyEnter: @@ -233,91 +241,82 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.render() case key.Matches(msg, m.keyMap.Down): - m.showCursor = true - if m.cursor < len(m.pathToLineNumber.keys)-1 { // scroll till last element in m.pathToLineNumber - m.cursor++ - } else { - // at the bottom of viewport maybe some hidden brackets, lets scroll to see them - if !m.AtBottom() { - m.LineDown(1) - } - } - if m.cursor >= len(m.pathToLineNumber.keys) { - m.cursor = len(m.pathToLineNumber.keys) - 1 - } + m.down() m.render() - at := m.cursorLineNumber() - if m.offset <= at { // cursor is lower - m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0 - } else { - m.SetOffset(at) - } + m.scrollDownToCursor() - case key.Matches(msg, m.keyMap.NextSibling): - m.showCursor = true - // TODO: write code for collecting siblings, - // and write code to getting sibling and jumping to it. + case key.Matches(msg, m.keyMap.Up): + m.up() m.render() - at := m.cursorLineNumber() - if m.offset <= at { // cursor is lower - m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0 - } else { - m.SetOffset(at) - } + m.scrollUpToCursor() - case key.Matches(msg, m.keyMap.Up): - m.showCursor = true - if m.cursor > 0 { - m.cursor-- - } - if m.cursor >= len(m.pathToLineNumber.keys) { - m.cursor = len(m.pathToLineNumber.keys) - 1 + case key.Matches(msg, m.keyMap.NextSibling): + nextSiblingPath, ok := m.nextSiblings[m.cursorPath()] + if ok { + index, _ := m.pathToLineNumber.indexes[nextSiblingPath] + m.showCursor = true + m.cursor = index + } else { + m.down() } m.render() - at := m.cursorLineNumber() - if at < m.offset+m.height { // cursor is above - m.LineUp(max(0, m.offset-at)) + m.scrollDownToCursor() + + case key.Matches(msg, m.keyMap.PrevSibling): + prevSiblingPath, ok := m.prevSiblings[m.cursorPath()] + if ok { + index, _ := m.pathToLineNumber.indexes[prevSiblingPath] + m.showCursor = true + m.cursor = index } else { - m.SetOffset(at) + m.up() } + m.render() + m.scrollUpToCursor() case key.Matches(msg, m.keyMap.Expand): m.showCursor = true - m.expandedPaths[m.cursorPath()] = true + if m.canBeExpanded[m.cursorPath()] { + m.expandedPaths[m.cursorPath()] = true + } m.render() case key.Matches(msg, m.keyMap.ExpandRecursively): m.showCursor = true - m.expandRecursively(m.cursorPath()) + if m.canBeExpanded[m.cursorPath()] { + m.expandRecursively(m.cursorPath()) + } m.render() - case key.Matches(msg, m.keyMap.Collapse, m.keyMap.CollapseRecursively): + case key.Matches(msg, m.keyMap.Collapse): m.showCursor = true - if m.expandedPaths[m.cursorPath()] { - if key.Matches(msg, m.keyMap.CollapseRecursively) { - m.collapseRecursively(m.cursorPath()) - } else { - m.expandedPaths[m.cursorPath()] = false - } + if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] { + m.expandedPaths[m.cursorPath()] = false } else { parentPath, ok := m.parents[m.cursorPath()] if ok { - if key.Matches(msg, m.keyMap.CollapseRecursively) { - m.collapseRecursively(m.cursorPath()) - } else { - m.expandedPaths[m.cursorPath()] = false - } + m.expandedPaths[parentPath] = false index, _ := m.pathToLineNumber.index(parentPath) m.cursor = index } } m.render() - at := m.cursorLineNumber() - if at < m.offset+m.height { // cursor is above - m.LineUp(max(0, m.offset-at)) + m.scrollUpToCursor() + + case key.Matches(msg, m.keyMap.CollapseRecursively): + m.showCursor = true + if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] { + m.collapseRecursively(m.cursorPath()) } else { - m.SetOffset(at) + parentPath, ok := m.parents[m.cursorPath()] + if ok { + m.collapseRecursively(parentPath) + index, _ := m.pathToLineNumber.index(parentPath) + m.cursor = index + } } + m.render() + m.scrollUpToCursor() case key.Matches(msg, m.keyMap.ToggleWrap): m.wrap = !m.wrap @@ -379,10 +378,13 @@ func (m *model) View() string { if len(lines) < m.height { extraLines = strings.Repeat("\n", max(0, m.height-len(lines))) } - statusBar := m.cursorPath() + " " if m.showHelp { - statusBar = "Press Esc or q to close help." + statusBar := "Press Esc or q to close help." + statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar))) + statusBar = colors.statusBar.Render(statusBar) + return strings.Join(lines, "\n") + extraLines + "\n" + statusBar } + statusBar := m.cursorPath() + " " statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName))) statusBar += m.fileName statusBar = colors.statusBar.Render(statusBar) @@ -393,27 +395,29 @@ 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.resultsCursor+1, len(m.searchResults.keys)) - } - } + //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)) + // } + //} return output } func (m *model) recalculateViewportHeight() { m.height = m.windowHeight m.height-- // status bar - if m.searchInput.Focused() { - m.height-- - } - if m.showSearchResults { - m.height-- - } - if len(m.searchRegexCompileError) > 0 { - m.height-- + if !m.showHelp { + if m.searchInput.Focused() { + m.height-- + } + if m.showSearchResults { + m.height-- + } + if len(m.searchRegexCompileError) > 0 { + m.height-- + } } } @@ -425,9 +429,17 @@ func (m *model) render() { return } - m.pathToLineNumber = newDict() - m.lineNumberToPath = map[int]string{} - m.lines = m.print(m.json, 1, 0, 0, "", false) + if m.pathToLineNumber == nil { + m.pathToLineNumber = newDict() + } else { + m.pathToLineNumber = newDictOfCapacity(cap(m.pathToLineNumber.keys)) + } + if m.lineNumberToPath == nil { + m.lineNumberToPath = make(map[int]string, 0) + } else { + m.lineNumberToPath = make(map[int]string, len(m.lineNumberToPath)) + } + m.lines = m.print(m.json, 1, 0, 0, "", true) if m.offset > len(m.lines)-1 { m.GotoBottom() @@ -470,3 +482,57 @@ func (m *model) collapseRecursively(path string) { } } } + +func (m *model) collectSiblings(v interface{}, path string) { + switch v.(type) { + case *dict: + prev := "" + for _, k := range v.(*dict).keys { + subpath := path + "." + k + if prev != "" { + m.nextSiblings[prev] = subpath + m.prevSiblings[subpath] = prev + } + prev = subpath + value, _ := v.(*dict).get(k) + m.collectSiblings(value, subpath) + } + + case array: + prev := "" + for i, value := range v.(array) { + subpath := fmt.Sprintf("%v[%v]", path, i) + if prev != "" { + m.nextSiblings[prev] = subpath + m.prevSiblings[subpath] = prev + } + prev = subpath + m.collectSiblings(value, subpath) + } + } +} + +func (m *model) down() { + m.showCursor = true + if m.cursor < len(m.pathToLineNumber.keys)-1 { // scroll till last element in m.pathToLineNumber + m.cursor++ + } else { + // at the bottom of viewport maybe some hidden brackets, lets scroll to see them + if !m.AtBottom() { + m.LineDown(1) + } + } + if m.cursor >= len(m.pathToLineNumber.keys) { + m.cursor = len(m.pathToLineNumber.keys) - 1 + } +} + +func (m *model) up() { + m.showCursor = true + if m.cursor > 0 { + m.cursor-- + } + if m.cursor >= len(m.pathToLineNumber.keys) { + m.cursor = len(m.pathToLineNumber.keys) - 1 + } +} diff --git a/print.go b/print.go index 6be0252..724a37e 100644 --- a/print.go +++ b/print.go @@ -1,192 +1,92 @@ package main import ( - "encoding/json" "fmt" "github.com/charmbracelet/lipgloss" "strings" ) -func prettyPrint(v interface{}, level int) string { +func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, selectableValues bool) []string { ident := strings.Repeat(" ", level) subident := strings.Repeat(" ", level-1) + connect := func(path string, lineNumber int) { + m.pathToLineNumber.set(path, lineNumber) + m.lineNumberToPath[lineNumber] = path + } + sri := m.searchResultsIndex[path] + switch v.(type) { case nil: - return colors.null.Render("null") + return []string{merge(m.explode("null", sri.value, colors.null, path, selectableValues))} case bool: if v.(bool) { - return colors.boolean.Render("true") + return []string{merge(m.explode("true", sri.value, colors.boolean, path, selectableValues))} } else { - return colors.boolean.Render("false") + return []string{merge(m.explode("false", sri.value, colors.boolean, path, selectableValues))} } - case json.Number: - return colors.number.Render(v.(json.Number).String()) + case number: + return []string{merge(m.explode(v.(number).String(), sri.value, colors.number, path, selectableValues))} case string: - return colors.string.Render(fmt.Sprintf("%q", v)) - - case *dict: - keys := v.(*dict).keys - if len(keys) == 0 { - return colors.bracket.Render("{}") - } - output := colors.bracket.Render("{\n") - for i, k := range keys { - key := colors.key.Render(fmt.Sprintf("%q", k)) - value, _ := v.(*dict).get(k) - delim := ": " - line := ident + key + delim + prettyPrint(value, level+1) - if i < len(keys)-1 { - line += ",\n" - } else { - line += "\n" - } - output += line - } - return output + subident + colors.bracket.Render("}") - - case array: - slice := v.(array) - if len(slice) == 0 { - return colors.bracket.Render("[]") - } - output := colors.bracket.Render("[\n") - for i, value := range v.(array) { - line := ident + prettyPrint(value, level+1) - if i < len(slice)-1 { - line += ",\n" - } else { - line += "\n" - } - output += line - } - return output + subident + colors.bracket.Render("]") - - default: - return "unknown type" - } -} - -func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, dontHighlightCursor bool) []string { - ident := strings.Repeat(" ", level) - subident := strings.Repeat(" ", level-1) - - cursorOr := func(style lipgloss.Style) lipgloss.Style { - if m.cursorPath() == path && !dontHighlightCursor && m.showCursor { - return colors.cursor - } - return style - } - searchStyle := colors.search.Render - if m.resultsCursorPath() == path { - searchStyle = colors.cursor.Render - } - highlight := func(line string, style func(s string) string) string { - chunks := m.split(line, path) - return mergeChunks(chunks, style, searchStyle) - } - - switch v.(type) { - case nil: - line := highlight("null", cursorOr(colors.null).Render) - return []string{line} - - case bool: - line := highlight(stringify(v), cursorOr(colors.boolean).Render) - return []string{line} - - case json.Number: - line := highlight(v.(json.Number).String(), cursorOr(colors.number).Render) - return []string{line} - - case string: - stringStyle := cursorOr(colors.string).Render line := fmt.Sprintf("%q", v) - chunks := m.split(line, path) + chunks := m.explode(line, sri.value, colors.string, path, selectableValues) if m.wrap && keyEndPos+width(line) > m.width { - return wrapLines(chunks, keyEndPos, m.width, subident, stringStyle, searchStyle) + return wrapLines(chunks, keyEndPos, m.width, subident) } - return []string{mergeChunks(chunks, stringStyle, searchStyle)} + // No wrap + return []string{merge(chunks)} case *dict: - m.pathToLineNumber.set(path, lineNumber) - m.lineNumberToPath[lineNumber] = path - bracketStyle := cursorOr(colors.bracket).Render - if len(v.(*dict).keys) == 0 { - return []string{bracketStyle("{}")} - } + connect(path, lineNumber) if !m.expandedPaths[path] { - return []string{m.preview(v, path, dontHighlightCursor)} + return []string{m.preview(v, path, selectableValues)} } - output := []string{bracketStyle("{")} + output := []string{m.printOpenBracket("{", sri, path, selectableValues)} lineNumber++ // bracket is on separate line keys := v.(*dict).keys for i, k := range keys { subpath := path + "." + k - m.pathToLineNumber.set(subpath, lineNumber) - m.lineNumberToPath[lineNumber] = subpath - keyStyle := colors.key.Render - if m.cursorPath() == subpath && m.showCursor { - keyStyle = colors.cursor.Render - } + s := m.searchResultsIndex[subpath] + connect(subpath, lineNumber) key := fmt.Sprintf("%q", k) - { - var indexes [][]int - if m.searchResults != nil { - sr, ok := m.searchResults.get(subpath) - if ok { - indexes = sr.(searchResult).key - } - } - chunks := explode(key, indexes) - searchStyle := colors.search.Render - if m.resultsCursorPath() == subpath && !m.showCursor { - searchStyle = colors.cursor.Render - } - key = mergeChunks(chunks, keyStyle, searchStyle) - } + key = merge(m.explode(key, s.key, colors.key, subpath, true)) value, _ := v.(*dict).get(k) - delim := ": " + delim := m.printDelim(": ", s) keyEndPos := width(ident) + width(key) + width(delim) - lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, true) + 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] += "," + lines[len(lines)-1] += m.printComma(",", s) } output = append(output, lines...) lineNumber += len(lines) } - output = append(output, subident+colors.bracket.Render("}")) + output = append(output, subident+m.printCloseBracket("}", sri, path, false)) return output case array: - m.pathToLineNumber.set(path, lineNumber) - m.lineNumberToPath[lineNumber] = path - bracketStyle := cursorOr(colors.bracket).Render - if len(v.(array)) == 0 { - return []string{bracketStyle("[]")} - } + connect(path, lineNumber) if !m.expandedPaths[path] { - return []string{bracketStyle(m.preview(v, path, dontHighlightCursor))} + return []string{m.preview(v, path, selectableValues)} } - output := []string{bracketStyle("[")} + output := []string{m.printOpenBracket("[", sri, path, selectableValues)} lineNumber++ // bracket is on separate line slice := v.(array) for i, value := range slice { subpath := fmt.Sprintf("%v[%v]", path, i) - m.pathToLineNumber.set(subpath, lineNumber) - m.lineNumberToPath[lineNumber] = subpath - lines := m.print(value, level+1, lineNumber, width(ident), subpath, false) + s := m.searchResultsIndex[subpath] + connect(subpath, lineNumber) + lines := m.print(value, level+1, lineNumber, width(ident), subpath, true) lines[0] = ident + lines[0] if i < len(slice)-1 { - lines[len(lines)-1] += "," + lines[len(lines)-1] += m.printComma(",", s) } lineNumber += len(lines) output = append(output, lines...) } - output = append(output, subident+colors.bracket.Render("]")) + output = append(output, subident+m.printCloseBracket("]", sri, path, false)) return output default: @@ -194,20 +94,15 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri } } -func (m *model) preview(v interface{}, path string, dontHighlightCursor bool) string { - cursorOr := func(style lipgloss.Style) lipgloss.Style { - if m.cursorPath() == path && !dontHighlightCursor { - return colors.cursor - } - return style +func (m *model) preview(v interface{}, path string, selectableValues bool) string { + searchResult := m.searchResultsIndex[path] + previewStyle := colors.preview + if selectableValues && m.cursorPath() == path { + previewStyle = colors.cursor } - - bracketStyle := cursorOr(colors.bracket) - previewStyle := cursorOr(colors.preview) - printValue := func(value interface{}) string { switch value.(type) { - case nil, bool, json.Number: + case nil, bool, number: return previewStyle.Render(fmt.Sprintf("%v", value)) case string: return previewStyle.Render(fmt.Sprintf("%q", value)) @@ -221,7 +116,7 @@ func (m *model) preview(v interface{}, path string, dontHighlightCursor bool) st switch v.(type) { case *dict: - output := bracketStyle.Render("{") + output := m.printOpenBracket("{", searchResult, path, selectableValues) keys := v.(*dict).keys for _, k := range keys { key := fmt.Sprintf("%q", k) @@ -230,47 +125,39 @@ func (m *model) preview(v interface{}, path string, dontHighlightCursor bool) st output += printValue(value) break } - if len(keys) == 1 { - output += bracketStyle.Render("}") - } else { - output += bracketStyle.Render(", \u2026}") + if len(keys) > 1 { + output += previewStyle.Render(", \u2026") } + output += m.printCloseBracket("}", searchResult, path, selectableValues) return output case array: - output := bracketStyle.Render("[") + output := m.printOpenBracket("[", searchResult, path, selectableValues) slice := v.(array) for _, value := range slice { output += printValue(value) break } - if len(slice) == 1 { - output += bracketStyle.Render("]") - } else { - output += bracketStyle.Render(", \u2026]") + if len(slice) > 1 { + output += previewStyle.Render(", \u2026") } + output += m.printCloseBracket("]", searchResult, path, selectableValues) return output } return "?" } -func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringStyle, searchStyle func(s string) string) []string { +func wrapLines(chunks []withStyle, keyEndPos, mWidth int, subident string) []string { wrappedLines := make([]string, 0) currentLine := "" ident := "" // First line stays on the same line with a "key", pos := keyEndPos // so no ident is needed. Start counting from the "key" offset. - style := stringStyle - for i, chunk := range chunks { - if i%2 == 0 { - style = stringStyle - } else { - style = searchStyle - } + for _, chunk := range chunks { buffer := "" - for _, ch := range chunk { + for _, ch := range chunk.value { buffer += string(ch) if pos == mWidth-1 { - wrappedLines = append(wrappedLines, ident+currentLine+style(buffer)) + wrappedLines = append(wrappedLines, ident+currentLine+chunk.Render(buffer)) currentLine = "" buffer = "" pos = width(subident) // Start counting from ident. @@ -279,7 +166,7 @@ func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringSt pos++ } } - currentLine += style(buffer) + currentLine += chunk.Render(buffer) } if width(currentLine) > 0 { wrappedLines = append(wrappedLines, subident+currentLine) @@ -287,37 +174,102 @@ func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringSt return wrappedLines } -func (m *model) split(line, path string) []string { - var indexes [][]int - if m.searchResults != nil { - sr, ok := m.searchResults.get(path) - if ok { - indexes = sr.(searchResult).value +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 { + if selectableValues && m.cursorPath() == path { + return colors.cursor.Render(line) + } + if s.openBracket != nil { + if s.openBracket.index == m.searchResultsCursor { + return colors.cursor.Render(line) + } else { + return colors.search.Render(line) } + } else { + return colors.syntax.Render(line) } - return explode(line, indexes) } -func explode(s string, indexes [][]int) []string { - out := make([]string, 0) - pos := 0 - for _, l := range indexes { - out = append(out, s[pos:l[0]]) - out = append(out, s[l[0]:l[1]]) - pos = l[1] +func (m *model) printCloseBracket(line string, s searchResultGroup, 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 { + return colors.cursor.Render(line) + } else { + return colors.search.Render(line) + } + } else { + return colors.syntax.Render(line) } - out = append(out, s[pos:]) - return out } -func mergeChunks(chunks []string, stringStyle, searchStyle func(s string) string) string { - currentLine := "" - for i, chunk := range chunks { - if i%2 == 0 { - currentLine += stringStyle(chunk) +func (m *model) printDelim(line string, s searchResultGroup) string { + if s.delim != nil { + if s.delim.index == m.searchResultsCursor { + return colors.cursor.Render(line) } else { - currentLine += searchStyle(chunk) + return colors.search.Render(line) } + } else { + return colors.syntax.Render(line) } - return currentLine +} + +func (m *model) printComma(line string, s searchResultGroup) string { + if s.comma != nil { + if s.comma.index == m.searchResultsCursor { + return colors.cursor.Render(line) + } else { + return colors.search.Render(line) + } + } else { + return colors.syntax.Render(line) + } +} + +type withStyle struct { + value string + style lipgloss.Style +} + +func (m *model) explode(line string, searchResults []*searchResult, 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 { + style := colors.search + if sr.index == m.searchResultsCursor { + style = colors.cursor + } + out = append(out, withStyle{ + value: line[pos:sr.start], + style: defaultStyle, + }) + out = append(out, withStyle{ + value: line[sr.start:sr.end], + style: style, + }) + pos = sr.end + } + out = append(out, withStyle{ + value: line[pos:], + style: defaultStyle, + }) + return out +} + +func merge(chunks []withStyle) string { + out := "" + for _, chunk := range chunks { + out += chunk.Render(chunk.value) + } + return out } diff --git a/reduce.go b/reduce.go index d700cca..7440905 100644 --- a/reduce.go +++ b/reduce.go @@ -136,3 +136,65 @@ func reduce(object interface{}, args []string) { os.Exit(exitCode) } } + +func prettyPrint(v interface{}, level int) string { + ident := strings.Repeat(" ", level) + subident := strings.Repeat(" ", level-1) + switch v.(type) { + case nil: + return colors.null.Render("null") + + case bool: + if v.(bool) { + return colors.boolean.Render("true") + } else { + return colors.boolean.Render("false") + } + + case number: + return colors.number.Render(v.(number).String()) + + case string: + return colors.string.Render(fmt.Sprintf("%q", v)) + + case *dict: + keys := v.(*dict).keys + if len(keys) == 0 { + return colors.syntax.Render("{}") + } + output := colors.syntax.Render("{\n") + for i, k := range keys { + key := colors.key.Render(fmt.Sprintf("%q", k)) + value, _ := v.(*dict).get(k) + delim := ": " + line := ident + key + delim + prettyPrint(value, level+1) + if i < len(keys)-1 { + line += ",\n" + } else { + line += "\n" + } + output += line + } + return output + subident + colors.syntax.Render("}") + + case array: + slice := v.(array) + if len(slice) == 0 { + return colors.syntax.Render("[]") + } + output := colors.syntax.Render("[\n") + for i, value := range v.(array) { + line := ident + prettyPrint(value, level+1) + if i < len(slice)-1 { + line += ",\n" + } else { + line += "\n" + } + output += line + } + return output + subident + colors.syntax.Render("]") + + default: + return "unknown type" + } +} diff --git a/search.go b/search.go index 988ff6d..ada7e30 100644 --- a/search.go +++ b/search.go @@ -1,95 +1,103 @@ package main -import ( - "encoding/json" - "fmt" - "regexp" -) - type searchResult struct { - key, value [][]int + path string + index int + start, end int } -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) - } - } +type searchResultGroup struct { + key []*searchResult + value []*searchResult + delim *searchResult + openBracket *searchResult + closeBracket *searchResult + comma *searchResult +} + +// TODO: Implement search. +// TODO: Uncomment all code blocks. - 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 json.Number: - line := it.object.(json.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) +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) } func (m *model) jumpToSearchResult(at int) { - if m.searchResults == nil || len(m.searchResults.keys) == 0 { - return - } - m.showCursor = false - m.resultsCursor = at % len(m.searchResults.keys) - desiredPath := m.searchResults.keys[m.resultsCursor] - 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 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) + //} } func (m *model) expandToPath(path string) { @@ -100,20 +108,21 @@ func (m *model) expandToPath(path string) { } func (m *model) nextSearchResult() { - m.jumpToSearchResult((m.resultsCursor + 1) % len(m.searchResults.keys)) + //m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults.keys)) } func (m *model) prevSearchResult() { - i := m.resultsCursor - 1 - if i < 0 { - i = len(m.searchResults.keys) - 1 - } - m.jumpToSearchResult(i) + //i := m.searchResultsCursor - 1 + //if i < 0 { + // i = len(m.searchResults.keys) - 1 + //} + //m.jumpToSearchResult(i) } func (m *model) resultsCursorPath() string { - if m.searchResults == nil || len(m.searchResults.keys) == 0 { - return "?" - } - return m.searchResults.keys[m.resultsCursor] + //if m.searchResults == nil || len(m.searchResults.keys) == 0 { + // return "?" + //} + //return m.searchResults.keys[m.searchResultsCursor] + return "" } diff --git a/search_test.go b/search_test.go index 3cf32b7..06ab7d0 100644 --- a/search_test.go +++ b/search_test.go @@ -1,42 +1 @@ package main - -import ( - "fmt" - "reflect" - "regexp" - "testing" -) - -func Test_splitByFoundIndexes(t *testing.T) { - s := fmt.Sprintf("%q", "0 aaa 123 \"bbb 44\" ccc 5") - re := regexp.MustCompile("\"?\\d+\"?") - indexes := re.FindAllStringIndex(s, -1) - chunks := explode(s, indexes) - expected := []string{"", "\"0", " aaa ", "123", " \\\"bbb ", "44", "\\\" ccc ", "5\"", ""} - ok := reflect.DeepEqual(chunks, expected) - if !ok { - t.Errorf( - "split error:\n"+ - " got %v,\n"+ - " expected %v", - stringify(chunks), - stringify(expected), - ) - } -} - -func Test_splitByFoundIndexes_empty(t *testing.T) { - s := fmt.Sprintf("%q", "foo") - chunks := explode(s, nil) - expected := []string{"\"foo\""} - ok := reflect.DeepEqual(chunks, expected) - if !ok { - t.Errorf( - "split error:\n"+ - " got %v,\n"+ - " expected %v", - stringify(chunks), - stringify(expected), - ) - } -} diff --git a/stringify.go b/stringify.go new file mode 100644 index 0000000..87aa304 --- /dev/null +++ b/stringify.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" +) + +func stringify(v interface{}) string { + switch v.(type) { + case nil: + return "null" + + case bool: + if v.(bool) { + return "true" + } else { + return "false" + } + + case number: + return v.(number).String() + + case string: + return fmt.Sprintf("%q", v) + + case *dict: + result := "{" + 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 += ", " + } + result += line + } + return result + "}" + + case array: + result := "[" + for i, value := range v.(array) { + line := stringify(value) + if i < len(v.(array))-1 { + line += ", " + } + result += line + } + return result + "]" + + default: + return "unknown type" + } +} diff --git a/stringify_test.go b/stringify_test.go new file mode 100644 index 0000000..c0cc420 --- /dev/null +++ b/stringify_test.go @@ -0,0 +1,29 @@ +package main + +import "testing" + +func Test_stringify(t *testing.T) { + t.Run("dict", func(t *testing.T) { + arg := newDict() + arg.set("a", number("1")) + arg.set("b", number("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]` + 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 := `[{}, []]` + if got := stringify(arg); got != want { + t.Errorf("stringify() = %v, want %v", got, want) + } + }) +} diff --git a/util.go b/util.go index 0914f5e..a2b8e2d 100644 --- a/util.go +++ b/util.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "github.com/charmbracelet/lipgloss" ) @@ -27,51 +26,6 @@ func max(a, b int) int { return b } -func stringify(v interface{}) string { - switch v.(type) { - case nil: - return "null" - - case bool: - if v.(bool) { - return "true" - } else { - return "false" - } - - case json.Number: - return v.(json.Number).String() - - case string: - return fmt.Sprintf("%q", v) - - case *dict: - result := "{" - 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 += "," - } - result += line - } - return result + "}" - - case array: - result := "[" - for i, value := range v.(array) { - line := stringify(value) - if i < len(v.(array))-1 { - line += "," - } - result += line - } - return result + "]" - - default: - return "unknown type" - } -} - func width(s string) int { return lipgloss.Width(s) } diff --git a/viewport.go b/viewport.go index 4f58789..fa57bb6 100644 --- a/viewport.go +++ b/viewport.go @@ -108,3 +108,21 @@ func (m *model) GotoTop() { func (m *model) GotoBottom() { m.SetOffset(m.maxYOffset()) } + +func (m *model) scrollDownToCursor() { + at := m.cursorLineNumber() + if m.offset <= at { // cursor is lower + m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0 + } else { + m.SetOffset(at) + } +} + +func (m *model) scrollUpToCursor() { + at := m.cursorLineNumber() + if at < m.offset+m.height { // cursor is above + m.LineUp(max(0, m.offset-at)) + } else { + m.SetOffset(at) + } +}