Skip to content

Commit f2c8194

Browse files
authored
Merge pull request #232 from Maxteabag/feat/history-jk-nav
feat(history): j/k nav + bracket/star fixes
2 parents 9805603 + 4ff2b53 commit f2c8194

2 files changed

Lines changed: 365 additions & 19 deletions

File tree

‎sqlit/domains/query/ui/screens/query_history.py‎

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any
88

99
from textual.app import ComposeResult
10-
from rich.markup import escape
10+
from rich.text import Text
1111
from textual.binding import Binding
1212
from textual.containers import VerticalScroll
1313
from textual.screen import ModalScreen
@@ -112,17 +112,32 @@ def __init__(
112112
self._filtered_entries: list[QueryHistoryEntry] = []
113113

114114
def _merge_entries(self) -> list[QueryHistoryEntry]:
115-
"""Merge history entries with starred-only queries."""
115+
"""Merge history entries with starred-only queries.
116+
117+
Same-text rows collapse to a single most-recent entry so the row
118+
count matches the starring model (which is keyed by query text):
119+
starring one row never marks two visually-identical rows. In
120+
multi-connection (telescope) mode the dedupe key is
121+
(connection_name, query); in single-connection mode it's just
122+
the query.
123+
"""
116124
result: list[QueryHistoryEntry] = []
117125

118126
if self._multi_connection:
119-
history_queries_by_connection: dict[str, set[str]] = {}
127+
seen: dict[tuple[str, str], QueryHistoryEntry] = {}
120128
for entry in self.history:
121129
starred_queries = self._starred_by_connection.get(entry.connection_name, set())
122130
entry.is_starred = entry.query.strip() in starred_queries
123131
entry.is_starred_only = False
124-
history_queries_by_connection.setdefault(entry.connection_name, set()).add(entry.query.strip())
125-
result.append(entry)
132+
key = (entry.connection_name, entry.query.strip())
133+
existing = seen.get(key)
134+
if existing is None or entry.timestamp > existing.timestamp:
135+
seen[key] = entry
136+
result.extend(seen.values())
137+
138+
history_queries_by_connection: dict[str, set[str]] = {}
139+
for (conn_name, query_text) in seen:
140+
history_queries_by_connection.setdefault(conn_name, set()).add(query_text)
126141

127142
for connection_name, starred_queries in self._starred_by_connection.items():
128143
history_queries = history_queries_by_connection.get(connection_name, set())
@@ -137,15 +152,17 @@ def _merge_entries(self) -> list[QueryHistoryEntry]:
137152
)
138153
result.append(entry)
139154
else:
140-
# Mark history entries that are starred
141-
history_queries: set[str] = set()
155+
seen_single: dict[str, QueryHistoryEntry] = {}
142156
for entry in self.history:
143157
entry.is_starred = entry.query.strip() in self.starred
144158
entry.is_starred_only = False
145-
history_queries.add(entry.query.strip())
146-
result.append(entry)
159+
key = entry.query.strip()
160+
existing = seen_single.get(key)
161+
if existing is None or entry.timestamp > existing.timestamp:
162+
seen_single[key] = entry
163+
result.extend(seen_single.values())
147164

148-
# Add starred-only entries (queries that were starred but not in history)
165+
history_queries = set(seen_single.keys())
149166
for starred_query in self.starred:
150167
if starred_query not in history_queries:
151168
entry = QueryHistoryEntry(
@@ -221,7 +238,7 @@ def _update_preview(self, idx: int) -> None:
221238
entries = self._get_display_entries()
222239
if idx < len(entries):
223240
preview = self.query_one("#history-preview", Static)
224-
preview.update(escape(entries[idx].query))
241+
preview.update(Text(entries[idx].query))
225242

226243
def action_select(self) -> None:
227244
entries = self._get_display_entries()
@@ -287,6 +304,17 @@ def action_cancel(self) -> None:
287304

288305
def on_key(self, event: Any) -> None:
289306
if not self._filter_active:
307+
if event.key in ("j", "k"):
308+
try:
309+
option_list = self.query_one("#history-list", OptionList)
310+
except Exception:
311+
return
312+
if event.key == "j":
313+
option_list.action_cursor_down()
314+
else:
315+
option_list.action_cursor_up()
316+
event.prevent_default()
317+
event.stop()
290318
return
291319

292320
key = event.key
@@ -468,19 +496,25 @@ def _build_action_result(self, action: str, entry: QueryHistoryEntry) -> tuple[s
468496
return action, entry.query
469497

470498
def _build_option(self, entry: QueryHistoryEntry) -> Option:
471-
star = "[yellow]*[/] " if entry.is_starred else " "
472-
# Collapse all whitespace to single spaces and strip
499+
# Build a Rich Text (not a markup string) so identifiers like
500+
# [dbo].[ServiceFeatures] aren't eaten by the markup parser:
501+
# rich.markup.escape only escapes "[a-z#/@…]" tags, so uppercase
502+
# bracket identifiers slip through and get consumed as fake tags.
473503
query_single_line = re.sub(r"\s+", " ", entry.query).strip()
474504
max_len = 40 if self._multi_connection else 55
475505
if len(query_single_line) > max_len:
476506
query_preview = query_single_line[:max_len] + "..."
477507
else:
478508
query_preview = query_single_line
479-
query_preview = escape(query_preview)
509+
510+
text = Text()
511+
if entry.is_starred:
512+
text.append("* ", style="yellow")
513+
else:
514+
text.append(" ")
480515
if self._multi_connection:
481516
conn_label = self._truncate_label(self._entry_connection_label(entry), 24)
482-
conn_label = escape(conn_label)
483-
option_id = self._entry_option_id(entry)
484-
if self._multi_connection:
485-
return Option(f"{star}[dim]{conn_label}[/]/{query_preview}", id=option_id)
486-
return Option(f"{star}{query_preview}", id=option_id)
517+
text.append(conn_label, style="dim")
518+
text.append("/")
519+
text.append(query_preview)
520+
return Option(text, id=self._entry_option_id(entry))

0 commit comments

Comments
 (0)