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 {
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) {

@ -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"),

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

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

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

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

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

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

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

Loading…
Cancel
Save