Skip to content

Commit e46bc8b

Browse files
the-vampiireclaudeMaxteabag
authored
Fix JSON value viewer crash and add interactive tree view (#78)
* fix: use Syntax highlighting for JSON in value viewer to prevent markup parsing crash JSON data containing bracket patterns like [0,23] was being interpreted as Rich markup tags, causing MarkupError crashes in Preview (v) and View (V). - Use rich.Syntax for JSON content with syntax highlighting - Set markup=False on Static widgets for non-JSON content - Store raw value separately in ValueViewScreen for copy functionality * feat: add interactive JSON tree viewer for cell preview Add collapsible tree view for JSON data in Preview (v) and View (V) modes: - New JSONTreeView widget with expand/collapse support - Tree view is default for valid JSON, with syntax highlighting fallback - Key bindings: t (toggle tree/syntax), E (expand all), Z (collapse all) - Proper focus management and scrolling in both view modes - Simplified footer hints to avoid duplication with app bindings The tree view makes exploring nested JSON (like Twitter API responses) much easier than scrolling through syntax-highlighted text. * chore: fix ruff lint errors (unused imports, extraneous f-strings) * refactor: integrate JSON tree viewer with hierarchical state machine Address maintainer feedback on PR #78: - Remove manual key handling from InlineValueView widget - Create ValueViewTreeModeState and ValueViewSyntaxModeState child states - Register actions via _setup_actions() using hierarchical state machine - Add shared parse_json_value() to eliminate duplicate JSON detection - Display keybindings in footer via get_display_bindings() - Use lowercase 'z' for collapse all (per maintainer preference) - Remove 'e'/'E' expand binding (enter is textual default) - Show contextual toggle labels: "Syntax View" / "Tree View" - Add value_view_tree_mode and value_view_is_json to InputContext Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add copy submenu for JSON tree view with visual feedback Add contextual copy options when viewing JSON in tree mode: - yy: Copy current node's value - yf: Copy field as "key": value format - ya: Copy entire JSON Follows existing 'ry' (results yank) menu pattern. Includes visual flash feedback on copy (cursor flash for value/field, background flash for all). Syntax mode retains direct copy behavior (no submenu). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add regression tests for JSON tree view feature Add tests for parse_json_value function and state machine guards to ensure JSON tree view only activates for valid JSON content. Fix bug in ValueViewSyntaxModeState.is_active() that was missing the value_view_is_json check, which would have allowed toggle for non-JSON content. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Peter Adams <18162810+Maxteabag@users.noreply.github.com>
1 parent 3f3265e commit e46bc8b

File tree

12 files changed

+695
-103
lines changed

12 files changed

+695
-103
lines changed

sqlit/core/input_context.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class InputContext:
2121
autocomplete_visible: bool
2222
results_filter_active: bool
2323
value_view_active: bool
24+
value_view_tree_mode: bool
25+
value_view_is_json: bool
2426
query_executing: bool
2527
modal_open: bool
2628
has_connection: bool

sqlit/core/keymap.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ def _build_leader_commands(self) -> list[LeaderCommandDef]:
286286
# rye results export menu
287287
LeaderCommandDef("c", "csv", "Export as CSV", "Export", menu="rye"),
288288
LeaderCommandDef("j", "json", "Export as JSON", "Export", menu="rye"),
289+
# vy value view yank menu (tree mode)
290+
LeaderCommandDef("y", "value", "Copy value", "Copy", menu="vy"),
291+
LeaderCommandDef("f", "field", "Copy field", "Copy", menu="vy"),
292+
LeaderCommandDef("a", "all", "Copy all", "Copy", menu="vy"),
289293
]
290294

291295
def _build_action_keys(self) -> list[ActionKeyDef]:
@@ -380,6 +384,8 @@ def _build_action_keys(self) -> list[ActionKeyDef]:
380384
ActionKeyDef("q", "close_value_view", "value_view"),
381385
ActionKeyDef("escape", "close_value_view", "value_view"),
382386
ActionKeyDef("y", "copy_value_view", "value_view"),
387+
ActionKeyDef("t", "toggle_value_view_mode", "value_view"),
388+
ActionKeyDef("z", "collapse_all_json_nodes", "value_view"),
383389
]
384390

385391

sqlit/domains/results/state/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
from .results_filter_active import ResultsFilterActiveState
44
from .results_focused import ResultsFocusedState
5-
from .value_view_active import ValueViewActiveState
5+
from .value_view_active import (
6+
ValueViewActiveState,
7+
ValueViewSyntaxModeState,
8+
ValueViewTreeModeState,
9+
)
610

711
__all__ = [
812
"ResultsFilterActiveState",
913
"ResultsFocusedState",
1014
"ValueViewActiveState",
15+
"ValueViewSyntaxModeState",
16+
"ValueViewTreeModeState",
1117
]

sqlit/domains/results/state/value_view_active.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Inline value view state."""
1+
"""Value view states for tree and syntax modes."""
22

33
from __future__ import annotations
44

@@ -7,14 +7,21 @@
77

88

99
class ValueViewActiveState(State):
10-
"""Inline value view is active (viewing a cell's full content)."""
10+
"""Base state for inline value view (viewing a cell's full content)."""
1111

1212
help_category = "Value View"
1313

1414
def _setup_actions(self) -> None:
1515
self.allows("close_value_view", key="escape", label="Close", help="Close value view")
1616
self.allows("close_value_view", key="q", label="Close", help="Close value view")
1717
self.allows("copy_value_view", key="y", label="Copy", help="Copy value")
18+
self.allows(
19+
"toggle_value_view_mode",
20+
lambda app: app.value_view_is_json,
21+
key="t",
22+
label="Toggle",
23+
help="Toggle tree/syntax view",
24+
)
1825

1926
def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding], list[DisplayBinding]]:
2027
left: list[DisplayBinding] = [
@@ -25,3 +32,65 @@ def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding],
2532

2633
def is_active(self, app: InputContext) -> bool:
2734
return app.value_view_active
35+
36+
37+
class ValueViewTreeModeState(State):
38+
"""Value view is in tree mode (JSON tree viewer)."""
39+
40+
help_category = "Value View"
41+
42+
def _setup_actions(self) -> None:
43+
self.allows("collapse_all_json_nodes", key="z", label="Collapse", help="Collapse all nodes")
44+
self.allows("toggle_value_view_mode", key="t", label="Syntax View", help="Switch to syntax view")
45+
46+
def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding], list[DisplayBinding]]:
47+
left: list[DisplayBinding] = [
48+
DisplayBinding(key="z", label="Collapse", action="collapse_all_json_nodes"),
49+
DisplayBinding(key="t", label="Syntax View", action="toggle_value_view_mode"),
50+
]
51+
52+
# Get parent bindings
53+
if self.parent:
54+
parent_left, parent_right = self.parent.get_display_bindings(app)
55+
seen = {b.action for b in left}
56+
for binding in parent_left:
57+
if binding.action not in seen:
58+
left.append(binding)
59+
seen.add(binding.action)
60+
return left, parent_right
61+
62+
return left, []
63+
64+
def is_active(self, app: InputContext) -> bool:
65+
return app.value_view_active and app.value_view_is_json and app.value_view_tree_mode
66+
67+
68+
class ValueViewSyntaxModeState(State):
69+
"""Value view is in syntax mode (syntax-highlighted text)."""
70+
71+
help_category = "Value View"
72+
73+
def _setup_actions(self) -> None:
74+
self.allows("toggle_value_view_mode", key="t", label="Tree View", help="Switch to tree view")
75+
76+
def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding], list[DisplayBinding]]:
77+
left: list[DisplayBinding] = []
78+
79+
# Only show Tree toggle for JSON content
80+
if app.value_view_is_json:
81+
left.append(DisplayBinding(key="t", label="Tree View", action="toggle_value_view_mode"))
82+
83+
# Get parent bindings
84+
if self.parent:
85+
parent_left, parent_right = self.parent.get_display_bindings(app)
86+
seen = {b.action for b in left}
87+
for binding in parent_left:
88+
if binding.action not in seen:
89+
left.append(binding)
90+
seen.add(binding.action)
91+
return left, parent_right
92+
93+
return left, []
94+
95+
def is_active(self, app: InputContext) -> bool:
96+
return app.value_view_active and app.value_view_is_json and not app.value_view_tree_mode

sqlit/domains/results/ui/mixins/results.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,96 @@ def action_close_value_view(self: ResultsMixinHost) -> None:
214214
pass
215215

216216
def action_copy_value_view(self: ResultsMixinHost) -> None:
217-
"""Copy the value from the inline value view."""
217+
"""Copy the value from the inline value view.
218+
219+
In tree mode with JSON, opens the vy yank menu.
220+
In syntax mode, copies the full value directly.
221+
"""
222+
from sqlit.shared.ui.widgets import InlineValueView, flash_widget
223+
224+
try:
225+
value_view = self.query_one("#value-view", InlineValueView)
226+
if not value_view.is_visible:
227+
return
228+
# In tree mode with JSON, open the yank menu
229+
if value_view._is_json and value_view._tree_mode:
230+
self._start_leader_pending("vy")
231+
return
232+
# In syntax mode, copy directly
233+
self._copy_text(value_view.value)
234+
flash_widget(value_view)
235+
except Exception:
236+
pass
237+
238+
def action_vy_value(self: ResultsMixinHost) -> None:
239+
"""Copy the current node's value (from yank menu)."""
240+
from sqlit.shared.ui.widgets import InlineValueView, flash_widget
241+
242+
self._clear_leader_pending()
243+
try:
244+
value_view = self.query_one("#value-view", InlineValueView)
245+
if value_view.is_visible:
246+
text = value_view.get_cursor_value_json()
247+
if text:
248+
self._copy_text(text)
249+
tree = value_view.get_tree_widget()
250+
if tree:
251+
flash_widget(tree, "flash-cursor")
252+
except Exception:
253+
pass
254+
255+
def action_vy_field(self: ResultsMixinHost) -> None:
256+
"""Copy the current field as 'key': value (from yank menu)."""
257+
from sqlit.shared.ui.widgets import InlineValueView, flash_widget
258+
259+
self._clear_leader_pending()
260+
try:
261+
value_view = self.query_one("#value-view", InlineValueView)
262+
if value_view.is_visible:
263+
text = value_view.get_cursor_field_json()
264+
if text:
265+
self._copy_text(text)
266+
tree = value_view.get_tree_widget()
267+
if tree:
268+
flash_widget(tree, "flash-cursor")
269+
except Exception:
270+
pass
271+
272+
def action_vy_all(self: ResultsMixinHost) -> None:
273+
"""Copy the full JSON (from yank menu)."""
218274
from sqlit.shared.ui.widgets import InlineValueView, flash_widget
219275

276+
self._clear_leader_pending()
220277
try:
221278
value_view = self.query_one("#value-view", InlineValueView)
222279
if value_view.is_visible:
223280
self._copy_text(value_view.value)
224-
flash_widget(value_view)
281+
tree = value_view.get_tree_widget()
282+
if tree:
283+
flash_widget(tree, "flash-all")
284+
except Exception:
285+
pass
286+
287+
def action_toggle_value_view_mode(self: ResultsMixinHost) -> None:
288+
"""Toggle between tree and syntax view in the inline value view."""
289+
from sqlit.shared.ui.widgets import InlineValueView
290+
291+
try:
292+
value_view = self.query_one("#value-view", InlineValueView)
293+
if value_view.is_visible:
294+
value_view.toggle_view_mode()
295+
self._update_footer_bindings()
296+
except Exception:
297+
pass
298+
299+
def action_collapse_all_json_nodes(self: ResultsMixinHost) -> None:
300+
"""Collapse all nodes in the JSON tree view."""
301+
from sqlit.shared.ui.widgets import InlineValueView
302+
303+
try:
304+
value_view = self.query_one("#value-view", InlineValueView)
305+
if value_view.is_visible:
306+
value_view.collapse_all_nodes()
225307
except Exception:
226308
pass
227309

0 commit comments

Comments
 (0)