Refactor search result printing

sleep-stdin-bug
Anton Medvedev 2 years ago
parent afd7b96af5
commit d682c0b49f

@ -7,11 +7,15 @@ type dict struct {
} }
func newDict() *dict { func newDict() *dict {
d := dict{} return newDictOfCapacity(0)
d.keys = []string{} }
d.indexes = map[string]int{}
d.values = map[string]interface{}{} func newDictOfCapacity(capacity int) *dict {
return &d 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) { func (d *dict) get(key string) (interface{}, bool) {

@ -9,19 +9,19 @@ type KeyMap struct {
PageUp key.Binding PageUp key.Binding
HalfPageUp key.Binding HalfPageUp key.Binding
HalfPageDown key.Binding HalfPageDown key.Binding
GotoTop key.Binding
GotoBottom key.Binding
Down key.Binding Down key.Binding
Up key.Binding Up key.Binding
Expand key.Binding Expand key.Binding
Collapse key.Binding Collapse key.Binding
NextSibling key.Binding
PrevSibling key.Binding
ExpandRecursively key.Binding ExpandRecursively key.Binding
CollapseRecursively key.Binding CollapseRecursively key.Binding
GotoTop key.Binding
GotoBottom key.Binding
ToggleWrap key.Binding
ExpandAll key.Binding ExpandAll key.Binding
CollapseAll key.Binding CollapseAll key.Binding
NextSibling key.Binding
PrevSibling key.Binding
ToggleWrap key.Binding
Search key.Binding Search key.Binding
Next key.Binding Next key.Binding
Prev key.Binding Prev key.Binding
@ -53,6 +53,14 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("d", "ctrl+d"), key.WithKeys("d", "ctrl+d"),
key.WithHelp("", "half page down"), 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( Down: key.NewBinding(
key.WithKeys("down", "j"), key.WithKeys("down", "j"),
key.WithHelp("", "down"), key.WithHelp("", "down"),
@ -69,14 +77,6 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("left", "h"), key.WithKeys("left", "h"),
key.WithHelp("", "collapse"), 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( ExpandRecursively: key.NewBinding(
key.WithKeys("L"), key.WithKeys("L"),
key.WithHelp("", "expand recursively"), key.WithHelp("", "expand recursively"),
@ -85,18 +85,6 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("H"), key.WithKeys("H"),
key.WithHelp("", "collapse recursively"), 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( ExpandAll: key.NewBinding(
key.WithKeys("e"), key.WithKeys("e"),
key.WithHelp("", "expand all"), key.WithHelp("", "expand all"),
@ -105,6 +93,18 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("E"), key.WithKeys("E"),
key.WithHelp("", "collapse all"), 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( Search: key.NewBinding(
key.WithKeys("/"), key.WithKeys("/"),
key.WithHelp("", "search regexp"), key.WithHelp("", "search regexp"),

@ -13,9 +13,11 @@ import (
"strings" "strings"
) )
type number = json.Number
var colors = struct { var colors = struct {
cursor lipgloss.Style cursor lipgloss.Style
bracket lipgloss.Style syntax lipgloss.Style
key lipgloss.Style key lipgloss.Style
null lipgloss.Style null lipgloss.Style
boolean lipgloss.Style boolean lipgloss.Style
@ -26,7 +28,7 @@ var colors = struct {
search lipgloss.Style search lipgloss.Style
}{ }{
cursor: lipgloss.NewStyle().Reverse(true), cursor: lipgloss.NewStyle().Reverse(true),
bracket: lipgloss.NewStyle(), syntax: lipgloss.NewStyle(),
key: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")), key: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")),
null: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")), null: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")),
boolean: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")), boolean: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")),
@ -39,7 +41,7 @@ var colors = struct {
func main() { func main() {
filePath := "" filePath := ""
args := []string{} var args []string
var dec *json.Decoder var dec *json.Decoder
if term.IsTerminal(int(os.Stdin.Fd())) { if term.IsTerminal(int(os.Stdin.Fd())) {
if len(os.Args) >= 2 { if len(os.Args) >= 2 {
@ -85,9 +87,10 @@ func main() {
parents[it.path] = it.parent parents[it.path] = it.parent
children[it.parent] = append(children[it.parent], it.path) children[it.parent] = append(children[it.parent], it.path)
switch it.object.(type) { switch it.object.(type) {
case *dict, array: case *dict:
canBeExpanded[it.path] = true canBeExpanded[it.path] = len(it.object.(*dict).keys) > 0
case array:
canBeExpanded[it.path] = len(it.object.(array)) > 0
} }
}) })
input := textinput.New() input := textinput.New()
@ -103,15 +106,18 @@ func main() {
canBeExpanded: canBeExpanded, canBeExpanded: canBeExpanded,
parents: parents, parents: parents,
children: children, children: children,
nextSiblings: map[string]string{},
prevSiblings: map[string]string{},
wrap: true, wrap: true,
searchInput: input, searchInput: input,
} }
m.collectSiblings(m.json, "")
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil { if err := p.Start(); err != nil {
panic(err) panic(err)
} }
os.Exit(m.exitCode) //os.Exit(m.exitCode)
} }
type model struct { type model struct {
@ -119,6 +125,7 @@ type model struct {
width, height int width, height int
windowHeight int windowHeight int
footerHeight int footerHeight int
wrap bool
fileName string fileName string
json interface{} json interface{}
@ -130,21 +137,22 @@ type model struct {
keyMap KeyMap keyMap KeyMap
showHelp bool showHelp bool
expandedPaths map[string]bool // a set with expanded paths 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) canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array)
pathToLineNumber *dict // dict with path => line number pathToLineNumber *dict // dict with path => line number
lineNumberToPath map[int]string // map of line number => path lineNumberToPath map[int]string // map of line number => path
parents map[string]string // map of subpath => parent path parents map[string]string // map of subpath => parent path
children map[string][]string // map of path => child paths children map[string][]string // map of path => child paths
cursor int // cursor in range of m.pathToLineNumber.keys slice nextSiblings, prevSiblings map[string]string // map of path => sibling path
showCursor bool cursor int // cursor in range of m.pathToLineNumber.keys slice
showCursor bool
wrap bool
searchInput textinput.Model searchInput textinput.Model
searchRegexCompileError string searchRegexCompileError string
searchResults *dict // path => searchResult
showSearchResults bool showSearchResults bool
resultsCursor int // [0, searchResults length) searchResults []*searchResult
searchResultsCursor int
searchResultsIndex map[string]searchResultGroup
} }
func (m *model) Init() tea.Cmd { func (m *model) Init() tea.Cmd {
@ -174,7 +182,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.Type { switch msg.Type {
case tea.KeyEsc: case tea.KeyEsc:
m.searchInput.Blur() m.searchInput.Blur()
m.searchResults = newDict() //m.searchResults = newDict()
m.render() m.render()
case tea.KeyEnter: case tea.KeyEnter:
@ -233,91 +241,82 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.render() m.render()
case key.Matches(msg, m.keyMap.Down): case key.Matches(msg, m.keyMap.Down):
m.showCursor = true m.down()
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.render() m.render()
at := m.cursorLineNumber() m.scrollDownToCursor()
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)
}
case key.Matches(msg, m.keyMap.NextSibling): case key.Matches(msg, m.keyMap.Up):
m.showCursor = true m.up()
// TODO: write code for collecting siblings,
// and write code to getting sibling and jumping to it.
m.render() m.render()
at := m.cursorLineNumber() m.scrollUpToCursor()
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)
}
case key.Matches(msg, m.keyMap.Up): case key.Matches(msg, m.keyMap.NextSibling):
m.showCursor = true nextSiblingPath, ok := m.nextSiblings[m.cursorPath()]
if m.cursor > 0 { if ok {
m.cursor-- index, _ := m.pathToLineNumber.indexes[nextSiblingPath]
} m.showCursor = true
if m.cursor >= len(m.pathToLineNumber.keys) { m.cursor = index
m.cursor = len(m.pathToLineNumber.keys) - 1 } else {
m.down()
} }
m.render() m.render()
at := m.cursorLineNumber() m.scrollDownToCursor()
if at < m.offset+m.height { // cursor is above
m.LineUp(max(0, m.offset-at)) 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 { } else {
m.SetOffset(at) m.up()
} }
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.Expand): case key.Matches(msg, m.keyMap.Expand):
m.showCursor = true m.showCursor = true
m.expandedPaths[m.cursorPath()] = true if m.canBeExpanded[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = true
}
m.render() m.render()
case key.Matches(msg, m.keyMap.ExpandRecursively): case key.Matches(msg, m.keyMap.ExpandRecursively):
m.showCursor = true m.showCursor = true
m.expandRecursively(m.cursorPath()) if m.canBeExpanded[m.cursorPath()] {
m.expandRecursively(m.cursorPath())
}
m.render() m.render()
case key.Matches(msg, m.keyMap.Collapse, m.keyMap.CollapseRecursively): case key.Matches(msg, m.keyMap.Collapse):
m.showCursor = true m.showCursor = true
if m.expandedPaths[m.cursorPath()] { if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
if key.Matches(msg, m.keyMap.CollapseRecursively) { m.expandedPaths[m.cursorPath()] = false
m.collapseRecursively(m.cursorPath())
} else {
m.expandedPaths[m.cursorPath()] = false
}
} else { } else {
parentPath, ok := m.parents[m.cursorPath()] parentPath, ok := m.parents[m.cursorPath()]
if ok { if ok {
if key.Matches(msg, m.keyMap.CollapseRecursively) { m.expandedPaths[parentPath] = false
m.collapseRecursively(m.cursorPath())
} else {
m.expandedPaths[m.cursorPath()] = false
}
index, _ := m.pathToLineNumber.index(parentPath) index, _ := m.pathToLineNumber.index(parentPath)
m.cursor = index m.cursor = index
} }
} }
m.render() m.render()
at := m.cursorLineNumber() m.scrollUpToCursor()
if at < m.offset+m.height { // cursor is above
m.LineUp(max(0, m.offset-at)) case key.Matches(msg, m.keyMap.CollapseRecursively):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
m.collapseRecursively(m.cursorPath())
} else { } 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): case key.Matches(msg, m.keyMap.ToggleWrap):
m.wrap = !m.wrap m.wrap = !m.wrap
@ -379,10 +378,13 @@ func (m *model) View() string {
if len(lines) < m.height { if len(lines) < m.height {
extraLines = strings.Repeat("\n", max(0, m.height-len(lines))) extraLines = strings.Repeat("\n", max(0, m.height-len(lines)))
} }
statusBar := m.cursorPath() + " "
if m.showHelp { 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 += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
statusBar += m.fileName statusBar += m.fileName
statusBar = colors.statusBar.Render(statusBar) statusBar = colors.statusBar.Render(statusBar)
@ -393,27 +395,29 @@ func (m *model) View() string {
if len(m.searchRegexCompileError) > 0 { if len(m.searchRegexCompileError) > 0 {
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError) output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
} }
if m.showSearchResults { //if m.showSearchResults {
if len(m.searchResults.keys) == 0 { // if len(m.searchResults.keys) == 0 {
output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value()) // output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
} else { // } else {
output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.resultsCursor+1, len(m.searchResults.keys)) // output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults.keys))
} // }
} //}
return output return output
} }
func (m *model) recalculateViewportHeight() { func (m *model) recalculateViewportHeight() {
m.height = m.windowHeight m.height = m.windowHeight
m.height-- // status bar m.height-- // status bar
if m.searchInput.Focused() { if !m.showHelp {
m.height-- if m.searchInput.Focused() {
} m.height--
if m.showSearchResults { }
m.height-- if m.showSearchResults {
} m.height--
if len(m.searchRegexCompileError) > 0 { }
m.height-- if len(m.searchRegexCompileError) > 0 {
m.height--
}
} }
} }
@ -425,9 +429,17 @@ func (m *model) render() {
return return
} }
m.pathToLineNumber = newDict() if m.pathToLineNumber == nil {
m.lineNumberToPath = map[int]string{} m.pathToLineNumber = newDict()
m.lines = m.print(m.json, 1, 0, 0, "", false) } 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 { if m.offset > len(m.lines)-1 {
m.GotoBottom() 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
}
}

@ -1,192 +1,92 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"strings" "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) ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1) 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) { switch v.(type) {
case nil: case nil:
return colors.null.Render("null") return []string{merge(m.explode("null", sri.value, colors.null, path, selectableValues))}
case bool: case bool:
if v.(bool) { if v.(bool) {
return colors.boolean.Render("true") return []string{merge(m.explode("true", sri.value, colors.boolean, path, selectableValues))}
} else { } else {
return colors.boolean.Render("false") return []string{merge(m.explode("false", sri.value, colors.boolean, path, selectableValues))}
} }
case json.Number: case number:
return colors.number.Render(v.(json.Number).String()) return []string{merge(m.explode(v.(number).String(), sri.value, colors.number, path, selectableValues))}
case string: 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) 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 { 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: case *dict:
m.pathToLineNumber.set(path, lineNumber) connect(path, lineNumber)
m.lineNumberToPath[lineNumber] = path
bracketStyle := cursorOr(colors.bracket).Render
if len(v.(*dict).keys) == 0 {
return []string{bracketStyle("{}")}
}
if !m.expandedPaths[path] { 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 lineNumber++ // bracket is on separate line
keys := v.(*dict).keys keys := v.(*dict).keys
for i, k := range keys { for i, k := range keys {
subpath := path + "." + k subpath := path + "." + k
m.pathToLineNumber.set(subpath, lineNumber) s := m.searchResultsIndex[subpath]
m.lineNumberToPath[lineNumber] = subpath connect(subpath, lineNumber)
keyStyle := colors.key.Render
if m.cursorPath() == subpath && m.showCursor {
keyStyle = colors.cursor.Render
}
key := fmt.Sprintf("%q", k) key := fmt.Sprintf("%q", k)
{ key = merge(m.explode(key, s.key, colors.key, subpath, true))
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)
}
value, _ := v.(*dict).get(k) value, _ := v.(*dict).get(k)
delim := ": " delim := m.printDelim(": ", s)
keyEndPos := width(ident) + width(key) + width(delim) 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] lines[0] = ident + key + delim + lines[0]
if i < len(keys)-1 { if i < len(keys)-1 {
lines[len(lines)-1] += "," lines[len(lines)-1] += m.printComma(",", s)
} }
output = append(output, lines...) output = append(output, lines...)
lineNumber += len(lines) lineNumber += len(lines)
} }
output = append(output, subident+colors.bracket.Render("}")) output = append(output, subident+m.printCloseBracket("}", sri, path, false))
return output return output
case array: case array:
m.pathToLineNumber.set(path, lineNumber) connect(path, lineNumber)
m.lineNumberToPath[lineNumber] = path
bracketStyle := cursorOr(colors.bracket).Render
if len(v.(array)) == 0 {
return []string{bracketStyle("[]")}
}
if !m.expandedPaths[path] { 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 lineNumber++ // bracket is on separate line
slice := v.(array) slice := v.(array)
for i, value := range slice { for i, value := range slice {
subpath := fmt.Sprintf("%v[%v]", path, i) subpath := fmt.Sprintf("%v[%v]", path, i)
m.pathToLineNumber.set(subpath, lineNumber) s := m.searchResultsIndex[subpath]
m.lineNumberToPath[lineNumber] = subpath connect(subpath, lineNumber)
lines := m.print(value, level+1, lineNumber, width(ident), subpath, false) lines := m.print(value, level+1, lineNumber, width(ident), subpath, true)
lines[0] = ident + lines[0] lines[0] = ident + lines[0]
if i < len(slice)-1 { if i < len(slice)-1 {
lines[len(lines)-1] += "," lines[len(lines)-1] += m.printComma(",", s)
} }
lineNumber += len(lines) lineNumber += len(lines)
output = append(output, lines...) output = append(output, lines...)
} }
output = append(output, subident+colors.bracket.Render("]")) output = append(output, subident+m.printCloseBracket("]", sri, path, false))
return output return output
default: 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 { func (m *model) preview(v interface{}, path string, selectableValues bool) string {
cursorOr := func(style lipgloss.Style) lipgloss.Style { searchResult := m.searchResultsIndex[path]
if m.cursorPath() == path && !dontHighlightCursor { previewStyle := colors.preview
return colors.cursor if selectableValues && m.cursorPath() == path {
} previewStyle = colors.cursor
return style
} }
bracketStyle := cursorOr(colors.bracket)
previewStyle := cursorOr(colors.preview)
printValue := func(value interface{}) string { printValue := func(value interface{}) string {
switch value.(type) { switch value.(type) {
case nil, bool, json.Number: case nil, bool, number:
return previewStyle.Render(fmt.Sprintf("%v", value)) return previewStyle.Render(fmt.Sprintf("%v", value))
case string: case string:
return previewStyle.Render(fmt.Sprintf("%q", value)) 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) { switch v.(type) {
case *dict: case *dict:
output := bracketStyle.Render("{") output := m.printOpenBracket("{", searchResult, path, selectableValues)
keys := v.(*dict).keys keys := v.(*dict).keys
for _, k := range keys { for _, k := range keys {
key := fmt.Sprintf("%q", k) key := fmt.Sprintf("%q", k)
@ -230,47 +125,39 @@ func (m *model) preview(v interface{}, path string, dontHighlightCursor bool) st
output += printValue(value) output += printValue(value)
break break
} }
if len(keys) == 1 { if len(keys) > 1 {
output += bracketStyle.Render("}") output += previewStyle.Render(", \u2026")
} else {
output += bracketStyle.Render(", \u2026}")
} }
output += m.printCloseBracket("}", searchResult, path, selectableValues)
return output return output
case array: case array:
output := bracketStyle.Render("[") output := m.printOpenBracket("[", searchResult, path, selectableValues)
slice := v.(array) slice := v.(array)
for _, value := range slice { for _, value := range slice {
output += printValue(value) output += printValue(value)
break break
} }
if len(slice) == 1 { if len(slice) > 1 {
output += bracketStyle.Render("]") output += previewStyle.Render(", \u2026")
} else {
output += bracketStyle.Render(", \u2026]")
} }
output += m.printCloseBracket("]", searchResult, path, selectableValues)
return output return output
} }
return "?" 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) wrappedLines := make([]string, 0)
currentLine := "" currentLine := ""
ident := "" // First line stays on the same line with a "key", ident := "" // First line stays on the same line with a "key",
pos := keyEndPos // so no ident is needed. Start counting from the "key" offset. pos := keyEndPos // so no ident is needed. Start counting from the "key" offset.
style := stringStyle for _, chunk := range chunks {
for i, chunk := range chunks {
if i%2 == 0 {
style = stringStyle
} else {
style = searchStyle
}
buffer := "" buffer := ""
for _, ch := range chunk { for _, ch := range chunk.value {
buffer += string(ch) buffer += string(ch)
if pos == mWidth-1 { if pos == mWidth-1 {
wrappedLines = append(wrappedLines, ident+currentLine+style(buffer)) wrappedLines = append(wrappedLines, ident+currentLine+chunk.Render(buffer))
currentLine = "" currentLine = ""
buffer = "" buffer = ""
pos = width(subident) // Start counting from ident. pos = width(subident) // Start counting from ident.
@ -279,7 +166,7 @@ func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringSt
pos++ pos++
} }
} }
currentLine += style(buffer) currentLine += chunk.Render(buffer)
} }
if width(currentLine) > 0 { if width(currentLine) > 0 {
wrappedLines = append(wrappedLines, subident+currentLine) wrappedLines = append(wrappedLines, subident+currentLine)
@ -287,37 +174,102 @@ func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringSt
return wrappedLines return wrappedLines
} }
func (m *model) split(line, path string) []string { func (w withStyle) Render(s string) string {
var indexes [][]int return w.style.Render(s)
if m.searchResults != nil { }
sr, ok := m.searchResults.get(path)
if ok { func (m *model) printOpenBracket(line string, s searchResultGroup, path string, selectableValues bool) string {
indexes = sr.(searchResult).value 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 { func (m *model) printCloseBracket(line string, s searchResultGroup, path string, selectableValues bool) string {
out := make([]string, 0) if selectableValues && m.cursorPath() == path {
pos := 0 return colors.cursor.Render(line)
for _, l := range indexes { }
out = append(out, s[pos:l[0]]) if s.closeBracket != nil {
out = append(out, s[l[0]:l[1]]) if s.closeBracket.index == m.searchResultsCursor {
pos = l[1] 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 { func (m *model) printDelim(line string, s searchResultGroup) string {
currentLine := "" if s.delim != nil {
for i, chunk := range chunks { if s.delim.index == m.searchResultsCursor {
if i%2 == 0 { return colors.cursor.Render(line)
currentLine += stringStyle(chunk)
} else { } 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
} }

@ -136,3 +136,65 @@ func reduce(object interface{}, args []string) {
os.Exit(exitCode) 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"
}
}

@ -1,95 +1,103 @@
package main package main
import (
"encoding/json"
"fmt"
"regexp"
)
type searchResult struct { type searchResult struct {
key, value [][]int path string
index int
start, end int
} }
func (m *model) doSearch(s string) { type searchResultGroup struct {
re, err := regexp.Compile("(?i)" + s) key []*searchResult
if err != nil { value []*searchResult
m.searchRegexCompileError = err.Error() delim *searchResult
m.searchInput.Blur() openBracket *searchResult
return closeBracket *searchResult
} comma *searchResult
m.searchRegexCompileError = "" }
results := newDict()
addSearchResult := func(path string, indexes [][]int) { // TODO: Implement search.
if indexes != nil { // TODO: Uncomment all code blocks.
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) { func (m *model) doSearch(s string) {
switch it.object.(type) { //re, err := regexp.Compile("(?i)" + s)
case nil: //if err != nil {
line := "null" // m.searchRegexCompileError = err.Error()
found := re.FindAllStringIndex(line, -1) // m.searchInput.Blur()
addSearchResult(it.path, found) // return
case bool: //}
line := stringify(it.object) //m.searchRegexCompileError = ""
found := re.FindAllStringIndex(line, -1) //results := newDict()
addSearchResult(it.path, found) //addSearchResult := func(path string, indexes [][]int) {
case json.Number: // if indexes != nil {
line := it.object.(json.Number).String() // sr := searchResult{}
found := re.FindAllStringIndex(line, -1) // prev, ok := results.get(path)
addSearchResult(it.path, found) // if ok {
case string: // sr = prev.(searchResult)
line := fmt.Sprintf("%q", it.object) // }
found := re.FindAllStringIndex(line, -1) // sr.value = indexes
addSearchResult(it.path, found) // results.set(path, sr)
case *dict: // }
keys := it.object.(*dict).keys //}
for _, key := range keys { //
line := fmt.Sprintf("%q", key) //dfs(m.json, func(it iterator) {
subpath := it.path + "." + key // switch it.object.(type) {
indexes := re.FindAllStringIndex(line, -1) // case nil:
if indexes != nil { // line := "null"
sr := searchResult{} // found := re.FindAllStringIndex(line, -1)
prev, ok := results.get(subpath) // addSearchResult(it.path, found)
if ok { // case bool:
sr = prev.(searchResult) // line := stringify(it.object)
} // found := re.FindAllStringIndex(line, -1)
sr.key = indexes // addSearchResult(it.path, found)
results.set(subpath, sr) // case number:
} // line := it.object.(number).String()
} // found := re.FindAllStringIndex(line, -1)
} // addSearchResult(it.path, found)
}) // case string:
m.searchResults = results // line := fmt.Sprintf("%q", it.object)
m.searchInput.Blur() // found := re.FindAllStringIndex(line, -1)
m.showSearchResults = true // addSearchResult(it.path, found)
m.jumpToSearchResult(0) // 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) { func (m *model) jumpToSearchResult(at int) {
if m.searchResults == nil || len(m.searchResults.keys) == 0 { //if m.searchResults == nil || len(m.searchResults.keys) == 0 {
return // return
} //}
m.showCursor = false //m.showCursor = false
m.resultsCursor = at % len(m.searchResults.keys) //m.searchResultsCursor = at % len(m.searchResults.keys)
desiredPath := m.searchResults.keys[m.resultsCursor] //desiredPath := m.searchResults.keys[m.searchResultsCursor]
lineNumber, ok := m.pathToLineNumber.get(desiredPath) //lineNumber, ok := m.pathToLineNumber.get(desiredPath)
if ok { //if ok {
m.cursor = m.pathToLineNumber.indexes[desiredPath] // m.cursor = m.pathToLineNumber.indexes[desiredPath]
m.SetOffset(lineNumber.(int)) // m.SetOffset(lineNumber.(int))
m.render() // m.render()
} else { //} else {
m.expandToPath(desiredPath) // m.expandToPath(desiredPath)
m.render() // m.render()
m.jumpToSearchResult(at) // m.jumpToSearchResult(at)
} //}
} }
func (m *model) expandToPath(path string) { func (m *model) expandToPath(path string) {
@ -100,20 +108,21 @@ func (m *model) expandToPath(path string) {
} }
func (m *model) nextSearchResult() { 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() { func (m *model) prevSearchResult() {
i := m.resultsCursor - 1 //i := m.searchResultsCursor - 1
if i < 0 { //if i < 0 {
i = len(m.searchResults.keys) - 1 // i = len(m.searchResults.keys) - 1
} //}
m.jumpToSearchResult(i) //m.jumpToSearchResult(i)
} }
func (m *model) resultsCursorPath() string { func (m *model) resultsCursorPath() string {
if m.searchResults == nil || len(m.searchResults.keys) == 0 { //if m.searchResults == nil || len(m.searchResults.keys) == 0 {
return "?" // return "?"
} //}
return m.searchResults.keys[m.resultsCursor] //return m.searchResults.keys[m.searchResultsCursor]
return ""
} }

@ -1,42 +1 @@
package main 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),
)
}
}

@ -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"
}
}

@ -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)
}
})
}

@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -27,51 +26,6 @@ func max(a, b int) int {
return b 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 { func width(s string) int {
return lipgloss.Width(s) return lipgloss.Width(s)
} }

@ -108,3 +108,21 @@ func (m *model) GotoTop() {
func (m *model) GotoBottom() { func (m *model) GotoBottom() {
m.SetOffset(m.maxYOffset()) 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)
}
}

Loading…
Cancel
Save