Skip to content

Commit 734d392

Browse files
committed
Improve explorer filtering
1 parent 865ac0a commit 734d392

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

sqlit/ui/mixins/tree.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ def _on_columns_loaded(
351351
if self._get_node_kind(child) == "loading":
352352
child.remove()
353353

354+
if not columns:
355+
empty_child = node.add_leaf("[dim](Empty)[/]")
356+
empty_child.data = LoadingNode()
357+
return
358+
354359
for col in columns:
355360
col_name = escape_markup(col.name)
356361
col_type = escape_markup(col.data_type)
@@ -418,6 +423,10 @@ def _on_folder_loaded(self: AppProtocol, node: Any, db_name: str | None, folder_
418423
return
419424

420425
adapter = self._session.adapter
426+
if not items:
427+
empty_child = node.add_leaf("[dim](Empty)[/]")
428+
empty_child.data = LoadingNode()
429+
return
421430

422431
if folder_type in ("tables", "views"):
423432
self._add_schema_grouped_items(node, db_name, folder_type, items, adapter.default_schema)

sqlit/ui/mixins/tree_filter.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class TreeFilterMixin:
1818

1919
_tree_filter_visible: bool = False
2020
_tree_filter_text: str = ""
21+
_tree_filter_query: str = ""
22+
_tree_filter_fuzzy: bool = False
23+
_tree_filter_typing: bool = False
2124
_tree_filter_matches: list[Any] = []
2225
_tree_filter_match_index: int = 0
2326
_tree_original_labels: dict[int, str] = {}
@@ -29,6 +32,9 @@ def action_tree_filter(self: AppProtocol) -> None:
2932

3033
self._tree_filter_visible = True
3134
self._tree_filter_text = ""
35+
self._tree_filter_query = ""
36+
self._tree_filter_fuzzy = False
37+
self._tree_filter_typing = True
3238
self._tree_filter_matches = []
3339
self._tree_filter_match_index = 0
3440
self._tree_original_labels = {}
@@ -41,14 +47,19 @@ def action_tree_filter_close(self: AppProtocol) -> None:
4147
"""Close the tree filter and restore tree."""
4248
self._tree_filter_visible = False
4349
self._tree_filter_text = ""
50+
self._tree_filter_query = ""
51+
self._tree_filter_fuzzy = False
52+
self._tree_filter_typing = False
4453
self.tree_filter_input.hide()
4554
self._restore_tree_labels()
4655
self._show_all_tree_nodes()
4756
self._update_footer_bindings()
4857

4958
def action_tree_filter_accept(self: AppProtocol) -> None:
50-
"""Accept current filter selection and close."""
51-
self.action_tree_filter_close()
59+
"""Accept current filter selection and switch to navigation mode."""
60+
self._tree_filter_typing = False
61+
self.tree_filter_input.hide()
62+
self._update_footer_bindings()
5263

5364
def action_tree_filter_next(self: AppProtocol) -> None:
5465
"""Move to next filter match."""
@@ -97,15 +108,40 @@ def on_key(self: AppProtocol, event: Any) -> None:
97108
return
98109

99110
key = event.key
111+
if key == "enter":
112+
self.action_tree_filter_accept()
113+
event.prevent_default()
114+
event.stop()
115+
return
116+
117+
if not self._tree_filter_typing:
118+
if key in ("n", "j"):
119+
self.action_tree_filter_next()
120+
event.prevent_default()
121+
event.stop()
122+
return
123+
124+
if key in ("N", "k"):
125+
self.action_tree_filter_prev()
126+
event.prevent_default()
127+
event.stop()
128+
return
129+
130+
if key == "/":
131+
self.action_tree_filter()
132+
event.prevent_default()
133+
event.stop()
134+
return
100135

101136
# Handle backspace
102137
if key == "backspace":
103-
if self._tree_filter_text:
104-
self._tree_filter_text = self._tree_filter_text[:-1]
105-
self._update_tree_filter()
106-
else:
107-
# Exit filter when backspacing with no text
108-
self.action_tree_filter_close()
138+
if self._tree_filter_typing:
139+
if self._tree_filter_text:
140+
self._tree_filter_text = self._tree_filter_text[:-1]
141+
self._update_tree_filter()
142+
else:
143+
# Exit filter when backspacing with no text
144+
self.action_tree_filter_close()
109145
event.prevent_default()
110146
event.stop()
111147
return
@@ -114,6 +150,14 @@ def on_key(self: AppProtocol, event: Any) -> None:
114150
# event.key might be "shift+?" but event.character will be "?"
115151
char = getattr(event, "character", None)
116152
if char and char.isprintable():
153+
if char == "/" and not self._tree_filter_typing:
154+
self.action_tree_filter()
155+
event.prevent_default()
156+
event.stop()
157+
return
158+
if not self._tree_filter_typing:
159+
super().on_key(event) # type: ignore[misc]
160+
return
117161
self._tree_filter_text += char
118162
self._update_tree_filter()
119163
event.prevent_default()
@@ -127,8 +171,11 @@ def _update_tree_filter(self: AppProtocol) -> None:
127171
"""Update the tree based on current filter text."""
128172
self._restore_tree_labels()
129173
total = self._count_all_nodes()
174+
raw_text = self._tree_filter_text
175+
self._tree_filter_fuzzy = raw_text.startswith("~")
176+
self._tree_filter_query = raw_text[1:] if self._tree_filter_fuzzy else raw_text
130177

131-
if not self._tree_filter_text:
178+
if not self._tree_filter_query:
132179
self._show_all_tree_nodes()
133180
self._tree_filter_matches = []
134181
self.tree_filter_input.set_filter("", 0, total)
@@ -171,7 +218,15 @@ def _find_matching_nodes(
171218
# Get node label text for matching
172219
label_text = self._get_node_label_text(node)
173220
if label_text:
174-
matched, indices = fuzzy_match(self._tree_filter_text, label_text)
221+
if self._tree_filter_fuzzy:
222+
matched, indices = fuzzy_match(self._tree_filter_query, label_text)
223+
else:
224+
label_lower = label_text.lower()
225+
query_lower = self._tree_filter_query.lower()
226+
start = label_lower.find(query_lower)
227+
matched = start >= 0
228+
indices = list(range(start, start + len(self._tree_filter_query))) if matched else []
229+
175230
if matched:
176231
node_matches = True
177232
matches.append(node)
@@ -231,12 +286,12 @@ def _set_node_visibility(
231286
child_id = id(child)
232287
is_match = child_id in match_ids
233288
is_ancestor = child_id in ancestor_ids
234-
should_show = is_match or is_ancestor or not self._tree_filter_text
289+
should_show = is_match or is_ancestor or not self._tree_filter_query
235290

236291
# Use display style to hide/show
237292
# Note: Textual Tree doesn't have per-node visibility,
238293
# so we'll dim non-matching nodes instead
239-
if not should_show and self._tree_filter_text:
294+
if not should_show and self._tree_filter_query:
240295
# Dim non-matching nodes
241296
original = self._tree_original_labels.get(child_id, str(child.label))
242297
if child_id not in self._tree_original_labels:

0 commit comments

Comments
 (0)