Skip to content

Commit acfe8d6

Browse files
committed
fix(pt_utils): fix history synchronization and completion word detection
Fixed an issue where prompt_toolkit history was not being cleared/updated when cmd2 history changed. Restored consecutive deduplication logic in history. Improved completion word detection to use correct delimiters matching cmd2 logic.
1 parent b3bfe66 commit acfe8d6

File tree

2 files changed

+40
-27
lines changed

2 files changed

+40
-27
lines changed

cmd2/pt_utils.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Utilities for integrating prompt_toolkit with cmd2."""
22

3+
import re
34
from collections.abc import Iterable
45
from typing import (
56
TYPE_CHECKING,
@@ -12,7 +13,10 @@
1213
from prompt_toolkit.document import Document
1314
from prompt_toolkit.history import History
1415

15-
from . import utils
16+
from . import (
17+
constants,
18+
utils,
19+
)
1620

1721
if TYPE_CHECKING:
1822
from .cmd2 import Cmd
@@ -28,7 +32,15 @@ def __init__(self, cmd_app: 'Cmd', custom_settings: utils.CustomCompletionSettin
2832

2933
def get_completions(self, document: Document, _complete_event: object) -> Iterable[Completion]:
3034
"""Get completions for the current input."""
31-
text = document.get_word_before_cursor(WORD=True)
35+
# Define delimiters for completion to match cmd2/readline behavior
36+
delimiters = " \t\n" + "".join(constants.QUOTES) + "".join(constants.REDIRECTION_CHARS)
37+
if hasattr(self.cmd_app, 'statement_parser'):
38+
delimiters += "".join(self.cmd_app.statement_parser.terminators)
39+
40+
# Regex pattern for a word: one or more characters that are NOT delimiters
41+
pattern = re.compile(f"[^{re.escape(delimiters)}]+")
42+
43+
text = document.get_word_before_cursor(pattern=pattern)
3244

3345
# We need the full line and indexes for cmd2
3446
line = document.text
@@ -84,18 +96,20 @@ def __init__(self, cmd_app: 'Cmd') -> None:
8496

8597
def load_history_strings(self) -> Iterable[str]:
8698
"""Yield strings from cmd2's history to prompt_toolkit."""
99+
for item in self.cmd_app.history:
100+
yield item.statement.raw
101+
102+
def get_strings(self) -> list[str]:
103+
"""Get the strings from the history."""
104+
# We override this to always get the latest history from cmd2
105+
# instead of caching it like the base class does.
106+
strings: list[str] = []
87107
last_item = None
88-
for item in reversed(self.cmd_app.history):
108+
for item in self.cmd_app.history:
89109
if item.statement.raw != last_item:
90-
yield item.statement.raw
110+
strings.append(item.statement.raw)
91111
last_item = item.statement.raw
92-
93-
def get_strings(self) -> list[str]:
94-
"""Get the strings from the history that are loaded so far."""
95-
if not self._loaded:
96-
self._loaded_strings = list(self.load_history_strings())
97-
self._loaded = True
98-
return super().get_strings()
112+
return strings
99113

100114
def store_string(self, string: str) -> None:
101115
"""prompt_toolkit calls this when a line is accepted.

tests/test_pt_utils.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def make_history_item(self, text):
150150
return item
151151

152152
def test_load_history_strings(self, mock_cmd_app):
153-
"""Test loading history strings in reverse order with deduping."""
153+
"""Test loading history strings yields all items in forward order."""
154154
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
155155

156156
# Setup history items
@@ -163,10 +163,10 @@ def test_load_history_strings(self, mock_cmd_app):
163163
]
164164
mock_cmd_app.history = items
165165

166-
# Expected: cmd3, cmd2, cmd1 (duplicates removed)
166+
# Expected: cmd1, cmd2, cmd2, cmd3 (raw iteration)
167167
result = list(history.load_history_strings())
168168

169-
assert result == ["cmd3", "cmd2", "cmd1"]
169+
assert result == ["cmd1", "cmd2", "cmd2", "cmd3"]
170170

171171
def test_load_history_strings_empty(self, mock_cmd_app):
172172
"""Test loading history strings with empty history."""
@@ -179,26 +179,25 @@ def test_load_history_strings_empty(self, mock_cmd_app):
179179
assert result == []
180180

181181
def test_get_strings(self, mock_cmd_app):
182-
"""Test get_strings uses lazy loading."""
182+
"""Test get_strings returns deduped strings and does not cache."""
183183
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
184184

185-
items = [self.make_history_item("test")]
185+
items = [
186+
self.make_history_item("cmd1"),
187+
self.make_history_item("cmd2"),
188+
self.make_history_item("cmd2"), # Duplicate
189+
self.make_history_item("cmd3"),
190+
]
186191
mock_cmd_app.history = items
187192

188-
# Initially not loaded
189-
assert not history._loaded
190-
193+
# Expect deduped: cmd1, cmd2, cmd3
191194
strings = history.get_strings()
195+
assert strings == ["cmd1", "cmd2", "cmd3"]
192196

193-
assert strings == ["test"]
194-
assert history._loaded
195-
assert history._loaded_strings == ["test"]
196-
197-
# Call again, should return cached strings
198-
# Modify underlying history to prove it uses cache
199-
mock_cmd_app.history = []
197+
# Modify underlying history to prove it does NOT use cache
198+
mock_cmd_app.history.append(self.make_history_item("cmd4"))
200199
strings2 = history.get_strings()
201-
assert strings2 == ["test"]
200+
assert strings2 == ["cmd1", "cmd2", "cmd3", "cmd4"]
202201

203202
def test_store_string(self, mock_cmd_app):
204203
"""Test store_string does nothing."""

0 commit comments

Comments
 (0)