repl-toolkit 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- repl_toolkit/__init__.py +70 -0
- repl_toolkit/actions/__init__.py +24 -0
- repl_toolkit/actions/action.py +223 -0
- repl_toolkit/actions/registry.py +564 -0
- repl_toolkit/async_repl.py +374 -0
- repl_toolkit/completion/__init__.py +15 -0
- repl_toolkit/completion/prefix.py +109 -0
- repl_toolkit/completion/shell_expansion.py +453 -0
- repl_toolkit/formatting.py +152 -0
- repl_toolkit/headless_repl.py +251 -0
- repl_toolkit/ptypes.py +122 -0
- repl_toolkit/tests/__init__.py +5 -0
- repl_toolkit/tests/conftest.py +79 -0
- repl_toolkit/tests/test_actions.py +578 -0
- repl_toolkit/tests/test_async_repl.py +381 -0
- repl_toolkit/tests/test_completion.py +656 -0
- repl_toolkit/tests/test_formatting.py +232 -0
- repl_toolkit/tests/test_headless.py +677 -0
- repl_toolkit/tests/test_types.py +174 -0
- repl_toolkit-1.2.0.dist-info/METADATA +761 -0
- repl_toolkit-1.2.0.dist-info/RECORD +24 -0
- repl_toolkit-1.2.0.dist-info/WHEEL +5 -0
- repl_toolkit-1.2.0.dist-info/licenses/LICENSE +21 -0
- repl_toolkit-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for completion utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from unittest.mock import Mock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from prompt_toolkit.document import Document
|
|
11
|
+
|
|
12
|
+
from repl_toolkit.completion import PrefixCompleter, ShellExpansionCompleter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_text(formatted_text):
|
|
16
|
+
"""Helper to extract plain text from FormattedText."""
|
|
17
|
+
if hasattr(formatted_text, "__iter__"):
|
|
18
|
+
return "".join(text for style, text in formatted_text)
|
|
19
|
+
return str(formatted_text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestShellExpansionCompleter:
|
|
23
|
+
"""Test ShellExpansionCompleter class."""
|
|
24
|
+
|
|
25
|
+
def setup_method(self):
|
|
26
|
+
"""Set up test fixtures."""
|
|
27
|
+
self.completer = ShellExpansionCompleter()
|
|
28
|
+
self.complete_event = Mock()
|
|
29
|
+
|
|
30
|
+
def test_completer_initialization(self):
|
|
31
|
+
"""Test completer initialization."""
|
|
32
|
+
completer = ShellExpansionCompleter()
|
|
33
|
+
assert completer.timeout == 2.0
|
|
34
|
+
|
|
35
|
+
completer = ShellExpansionCompleter(timeout=5.0)
|
|
36
|
+
assert completer.timeout == 5.0
|
|
37
|
+
|
|
38
|
+
def test_environment_variable_expansion(self):
|
|
39
|
+
"""Test basic environment variable expansion."""
|
|
40
|
+
# Set test environment variable
|
|
41
|
+
os.environ["TEST_VAR"] = "test_value"
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Cursor after variable pattern
|
|
45
|
+
document = Document(text="Hello ${TEST_VAR}", cursor_position=17)
|
|
46
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
47
|
+
|
|
48
|
+
assert len(completions) == 1
|
|
49
|
+
assert completions[0].text == "test_value"
|
|
50
|
+
assert completions[0].start_position == -11 # Position to replace ${TEST_VAR}
|
|
51
|
+
assert "${TEST_VAR}" in _get_text(completions[0].display) or "TEST_VAR" in _get_text(
|
|
52
|
+
completions[0].display
|
|
53
|
+
)
|
|
54
|
+
assert "test_value" in str(completions[0].display)
|
|
55
|
+
finally:
|
|
56
|
+
del os.environ["TEST_VAR"]
|
|
57
|
+
|
|
58
|
+
def test_environment_variable_cursor_inside(self):
|
|
59
|
+
"""Test expansion when cursor is inside the variable pattern."""
|
|
60
|
+
os.environ["USER_VAR"] = "inside_value"
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Cursor in the middle of ${USER_VAR}
|
|
64
|
+
document = Document(text="Value: ${USER_VAR}", cursor_position=12)
|
|
65
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
66
|
+
|
|
67
|
+
assert len(completions) == 1
|
|
68
|
+
assert completions[0].text == "inside_value"
|
|
69
|
+
finally:
|
|
70
|
+
del os.environ["USER_VAR"]
|
|
71
|
+
|
|
72
|
+
def test_environment_variable_not_found(self):
|
|
73
|
+
"""Test behavior when environment variable doesn't exist."""
|
|
74
|
+
document = Document(text="${NONEXISTENT_VAR}", cursor_position=16)
|
|
75
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
76
|
+
|
|
77
|
+
# Should not offer completion for nonexistent variables
|
|
78
|
+
assert len(completions) == 0
|
|
79
|
+
|
|
80
|
+
def test_environment_variable_cursor_outside(self):
|
|
81
|
+
"""Test no completion when cursor is outside the pattern."""
|
|
82
|
+
os.environ["OUTSIDE_VAR"] = "value"
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Cursor before the pattern
|
|
86
|
+
document = Document(text="Hello ${OUTSIDE_VAR}", cursor_position=0)
|
|
87
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
88
|
+
|
|
89
|
+
# Should not complete when cursor is outside
|
|
90
|
+
assert len(completions) == 0
|
|
91
|
+
finally:
|
|
92
|
+
del os.environ["OUTSIDE_VAR"]
|
|
93
|
+
|
|
94
|
+
def test_multiple_environment_variables(self):
|
|
95
|
+
"""Test multiple environment variables in same text."""
|
|
96
|
+
os.environ["VAR1"] = "value1"
|
|
97
|
+
os.environ["VAR2"] = "value2"
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Cursor on second variable
|
|
101
|
+
document = Document(text="${VAR1} and ${VAR2}", cursor_position=19)
|
|
102
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
103
|
+
|
|
104
|
+
assert len(completions) == 1
|
|
105
|
+
assert completions[0].text == "value2"
|
|
106
|
+
finally:
|
|
107
|
+
del os.environ["VAR1"]
|
|
108
|
+
del os.environ["VAR2"]
|
|
109
|
+
|
|
110
|
+
def test_command_execution_simple(self):
|
|
111
|
+
"""Test simple command execution."""
|
|
112
|
+
# Use echo command which is available on most systems
|
|
113
|
+
document = Document(text="Result: $(echo test)", cursor_position=20)
|
|
114
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
115
|
+
|
|
116
|
+
assert len(completions) == 1
|
|
117
|
+
assert completions[0].text == "test"
|
|
118
|
+
assert "$(echo test)" in str(completions[0].display)
|
|
119
|
+
|
|
120
|
+
def test_command_execution_cursor_inside(self):
|
|
121
|
+
"""Test command execution when cursor is inside pattern."""
|
|
122
|
+
document = Document(text="$(echo hello)", cursor_position=8)
|
|
123
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
124
|
+
|
|
125
|
+
assert len(completions) == 1
|
|
126
|
+
assert completions[0].text == "hello"
|
|
127
|
+
|
|
128
|
+
def test_command_execution_with_output_trimming(self):
|
|
129
|
+
"""Test that command output is trimmed."""
|
|
130
|
+
document = Document(text='$(printf " spaces ")', cursor_position=22)
|
|
131
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
132
|
+
|
|
133
|
+
assert len(completions) == 1
|
|
134
|
+
assert completions[0].text == "spaces"
|
|
135
|
+
|
|
136
|
+
def test_command_execution_empty_command(self):
|
|
137
|
+
"""Test handling of empty command pattern."""
|
|
138
|
+
document = Document(text="Empty: $()", cursor_position=10)
|
|
139
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
140
|
+
|
|
141
|
+
# Should not complete empty commands
|
|
142
|
+
assert len(completions) == 0
|
|
143
|
+
|
|
144
|
+
def test_command_execution_whitespace_only(self):
|
|
145
|
+
"""Test handling of whitespace-only command."""
|
|
146
|
+
document = Document(text="$( )", cursor_position=6)
|
|
147
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
148
|
+
|
|
149
|
+
# Should not complete whitespace-only commands
|
|
150
|
+
assert len(completions) == 0
|
|
151
|
+
|
|
152
|
+
def test_command_execution_not_found(self):
|
|
153
|
+
"""Test handling of command not found error."""
|
|
154
|
+
document = Document(text="$(nonexistent_command_xyz)", cursor_position=26)
|
|
155
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
156
|
+
|
|
157
|
+
# Should offer completion with error message
|
|
158
|
+
assert len(completions) == 1
|
|
159
|
+
assert completions[0].text == "" # Empty replacement
|
|
160
|
+
assert "Error" in str(completions[0].display)
|
|
161
|
+
|
|
162
|
+
def test_command_execution_failure(self):
|
|
163
|
+
"""Test handling of command execution failure."""
|
|
164
|
+
# Command that will fail
|
|
165
|
+
document = Document(text="$(ls /nonexistent/path)", cursor_position=23)
|
|
166
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
167
|
+
|
|
168
|
+
# Should offer completion with error message
|
|
169
|
+
assert len(completions) == 1
|
|
170
|
+
assert completions[0].text == ""
|
|
171
|
+
assert "Error" in str(completions[0].display)
|
|
172
|
+
|
|
173
|
+
def test_command_execution_timeout(self):
|
|
174
|
+
"""Test command execution timeout."""
|
|
175
|
+
completer = ShellExpansionCompleter(timeout=0.1)
|
|
176
|
+
|
|
177
|
+
# Command that will timeout (sleep longer than timeout)
|
|
178
|
+
document = Document(text="$(sleep 5)", cursor_position=10)
|
|
179
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
180
|
+
|
|
181
|
+
# Should offer completion with timeout message
|
|
182
|
+
assert len(completions) == 1
|
|
183
|
+
assert completions[0].text == ""
|
|
184
|
+
assert "Timeout" in str(completions[0].display)
|
|
185
|
+
|
|
186
|
+
def test_mixed_patterns(self):
|
|
187
|
+
"""Test combination of environment variables and commands."""
|
|
188
|
+
os.environ["MIX_VAR"] = "envvalue"
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Text with both patterns, cursor on command
|
|
192
|
+
document = Document(text="${MIX_VAR} and $(echo cmd)", cursor_position=26)
|
|
193
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
194
|
+
|
|
195
|
+
# Should only complete the pattern at cursor
|
|
196
|
+
assert len(completions) == 1
|
|
197
|
+
assert completions[0].text == "cmd"
|
|
198
|
+
finally:
|
|
199
|
+
del os.environ["MIX_VAR"]
|
|
200
|
+
|
|
201
|
+
def test_cursor_outside_all_patterns(self):
|
|
202
|
+
"""Test no completion when cursor is outside all patterns."""
|
|
203
|
+
os.environ["OUT_VAR"] = "value"
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Cursor in plain text area
|
|
207
|
+
document = Document(text="Plain ${OUT_VAR} text $(echo test)", cursor_position=19)
|
|
208
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
209
|
+
|
|
210
|
+
# Should not complete
|
|
211
|
+
assert len(completions) == 0
|
|
212
|
+
finally:
|
|
213
|
+
del os.environ["OUT_VAR"]
|
|
214
|
+
|
|
215
|
+
def test_variable_pattern_validation(self):
|
|
216
|
+
"""Test that only valid variable names are matched."""
|
|
217
|
+
os.environ["_VALID_VAR"] = "valid"
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
# Valid variable name
|
|
221
|
+
document = Document(text="${_VALID_VAR}", cursor_position=13)
|
|
222
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
223
|
+
assert len(completions) == 1
|
|
224
|
+
|
|
225
|
+
# Invalid patterns should not be matched by regex
|
|
226
|
+
# (testing the pattern itself, not the completer behavior)
|
|
227
|
+
assert ShellExpansionCompleter.VAR_PATTERN.match("${123INVALID}") is None
|
|
228
|
+
assert ShellExpansionCompleter.VAR_PATTERN.match("${-INVALID}") is None
|
|
229
|
+
finally:
|
|
230
|
+
del os.environ["_VALID_VAR"]
|
|
231
|
+
|
|
232
|
+
def test_start_position_calculation(self):
|
|
233
|
+
"""Test correct calculation of start_position."""
|
|
234
|
+
os.environ["POS_VAR"] = "replacement"
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
text = "Prefix ${POS_VAR} suffix"
|
|
238
|
+
cursor_position = 17 # End of ${POS_VAR}
|
|
239
|
+
document = Document(text=text, cursor_position=cursor_position)
|
|
240
|
+
|
|
241
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
242
|
+
|
|
243
|
+
assert len(completions) == 1
|
|
244
|
+
# start_position should be negative, pointing back to start of pattern
|
|
245
|
+
# Pattern starts at position 7, cursor at 17
|
|
246
|
+
# start_position = 7 - 17 = -10
|
|
247
|
+
assert completions[0].start_position == -10
|
|
248
|
+
finally:
|
|
249
|
+
del os.environ["POS_VAR"]
|
|
250
|
+
|
|
251
|
+
def test_completer_with_real_environment_variables(self):
|
|
252
|
+
"""Test with actual system environment variables."""
|
|
253
|
+
# USER or USERNAME should exist on most systems
|
|
254
|
+
var_name = (
|
|
255
|
+
"USER" if "USER" in os.environ else "USERNAME" if "USERNAME" in os.environ else None
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if var_name:
|
|
259
|
+
document = Document(text=f"${{{var_name}}}", cursor_position=len(var_name) + 3)
|
|
260
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
261
|
+
|
|
262
|
+
assert len(completions) == 1
|
|
263
|
+
assert completions[0].text == os.environ[var_name]
|
|
264
|
+
else:
|
|
265
|
+
pytest.skip("No USER or USERNAME environment variable available")
|
|
266
|
+
|
|
267
|
+
def test_display_meta_fields(self):
|
|
268
|
+
"""Test that display_meta fields are set correctly."""
|
|
269
|
+
os.environ["META_VAR"] = "value"
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Test environment variable
|
|
273
|
+
document = Document(text="${META_VAR}", cursor_position=11)
|
|
274
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
275
|
+
assert _get_text(completions[0].display_meta) == "Environment variable"
|
|
276
|
+
|
|
277
|
+
# Test command
|
|
278
|
+
document = Document(text="$(echo test)", cursor_position=12)
|
|
279
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
280
|
+
assert _get_text(completions[0].display_meta) == "Shell command"
|
|
281
|
+
finally:
|
|
282
|
+
del os.environ["META_VAR"]
|
|
283
|
+
|
|
284
|
+
def test_patterns_are_non_greedy(self):
|
|
285
|
+
"""Test that command pattern is non-greedy."""
|
|
286
|
+
# Multiple command patterns
|
|
287
|
+
document = Document(text="$(echo a) and $(echo b)", cursor_position=9)
|
|
288
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
289
|
+
|
|
290
|
+
# Should only complete the first command where cursor is
|
|
291
|
+
assert len(completions) == 1
|
|
292
|
+
assert completions[0].text == "a"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class TestShellExpansionCompleterIntegration:
|
|
296
|
+
"""Integration tests for ShellExpansionCompleter."""
|
|
297
|
+
|
|
298
|
+
def test_import_from_package(self):
|
|
299
|
+
"""Test that completer can be imported from main package."""
|
|
300
|
+
from repl_toolkit import ShellExpansionCompleter
|
|
301
|
+
|
|
302
|
+
completer = ShellExpansionCompleter()
|
|
303
|
+
assert completer is not None
|
|
304
|
+
|
|
305
|
+
def test_use_with_merge_completers(self):
|
|
306
|
+
"""Test integration with prompt_toolkit's merge_completers."""
|
|
307
|
+
from prompt_toolkit.completion import WordCompleter, merge_completers
|
|
308
|
+
|
|
309
|
+
from repl_toolkit import ShellExpansionCompleter
|
|
310
|
+
|
|
311
|
+
env_completer = ShellExpansionCompleter()
|
|
312
|
+
word_completer = WordCompleter(["exit", "help"])
|
|
313
|
+
|
|
314
|
+
combined = merge_completers([env_completer, word_completer])
|
|
315
|
+
|
|
316
|
+
assert combined is not None
|
|
317
|
+
|
|
318
|
+
def test_completer_protocol_compliance(self):
|
|
319
|
+
"""Test that ShellExpansionCompleter implements Completer protocol."""
|
|
320
|
+
from prompt_toolkit.completion import Completer
|
|
321
|
+
|
|
322
|
+
from repl_toolkit import ShellExpansionCompleter
|
|
323
|
+
|
|
324
|
+
completer = ShellExpansionCompleter()
|
|
325
|
+
|
|
326
|
+
# Should have get_completions method
|
|
327
|
+
assert hasattr(completer, "get_completions")
|
|
328
|
+
assert callable(completer.get_completions)
|
|
329
|
+
|
|
330
|
+
# Should be instance of Completer
|
|
331
|
+
assert isinstance(completer, Completer)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestMultilineCommandCompletion:
|
|
335
|
+
"""Test multi-line command output completion."""
|
|
336
|
+
|
|
337
|
+
def setup_method(self):
|
|
338
|
+
"""Set up test fixtures."""
|
|
339
|
+
self.completer = ShellExpansionCompleter(multiline_all=True)
|
|
340
|
+
self.complete_event = Mock()
|
|
341
|
+
|
|
342
|
+
def test_single_line_command(self):
|
|
343
|
+
"""Test command with single line output."""
|
|
344
|
+
document = Document(text="$(echo single)", cursor_position=14)
|
|
345
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
346
|
+
|
|
347
|
+
# Should have one completion for single line
|
|
348
|
+
assert len(completions) == 1
|
|
349
|
+
assert completions[0].text == "single"
|
|
350
|
+
assert "single" in str(completions[0].display)
|
|
351
|
+
|
|
352
|
+
def test_multiline_command_with_all(self):
|
|
353
|
+
"""Test command with multi-line output offers ALL + individual lines."""
|
|
354
|
+
# Command that produces multiple lines
|
|
355
|
+
document = Document(text='$(printf "line1\\nline2\\nline3")', cursor_position=31)
|
|
356
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
357
|
+
|
|
358
|
+
# Should have ALL + 3 lines = 4 completions
|
|
359
|
+
assert len(completions) == 4
|
|
360
|
+
|
|
361
|
+
# First should be ALL
|
|
362
|
+
assert "ALL" in str(completions[0].display)
|
|
363
|
+
assert completions[0].text == "line1\nline2\nline3"
|
|
364
|
+
|
|
365
|
+
# Then individual lines
|
|
366
|
+
assert completions[1].text == "line1"
|
|
367
|
+
assert "Line 1" in str(completions[1].display)
|
|
368
|
+
|
|
369
|
+
assert completions[2].text == "line2"
|
|
370
|
+
assert "Line 2" in str(completions[2].display)
|
|
371
|
+
|
|
372
|
+
assert completions[3].text == "line3"
|
|
373
|
+
assert "Line 3" in str(completions[3].display)
|
|
374
|
+
|
|
375
|
+
def test_multiline_command_without_all(self):
|
|
376
|
+
"""Test multi-line output without ALL option."""
|
|
377
|
+
completer = ShellExpansionCompleter(multiline_all=False)
|
|
378
|
+
|
|
379
|
+
document = Document(text='$(printf "a\\nb\\nc")', cursor_position=19)
|
|
380
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
381
|
+
|
|
382
|
+
# Should have only 3 lines (no ALL)
|
|
383
|
+
assert len(completions) == 3
|
|
384
|
+
|
|
385
|
+
# All should be individual lines
|
|
386
|
+
assert completions[0].text == "a"
|
|
387
|
+
assert completions[1].text == "b"
|
|
388
|
+
assert completions[2].text == "c"
|
|
389
|
+
|
|
390
|
+
# None should be ALL
|
|
391
|
+
for comp in completions:
|
|
392
|
+
assert "ALL" not in str(comp.display)
|
|
393
|
+
|
|
394
|
+
def test_command_with_empty_lines(self):
|
|
395
|
+
"""Test that empty lines are filtered out."""
|
|
396
|
+
document = Document(text='$(printf "a\\n\\nb\\n\\n")', cursor_position=22)
|
|
397
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
398
|
+
|
|
399
|
+
# Should have ALL + 2 non-empty lines = 3 completions
|
|
400
|
+
assert len(completions) == 3
|
|
401
|
+
|
|
402
|
+
# ALL contains original with empty lines
|
|
403
|
+
assert "ALL" in str(completions[0].display)
|
|
404
|
+
|
|
405
|
+
# But individual lines skip empty ones
|
|
406
|
+
assert completions[1].text == "a"
|
|
407
|
+
assert completions[2].text == "b"
|
|
408
|
+
|
|
409
|
+
def test_command_no_output(self):
|
|
410
|
+
"""Test command with no output."""
|
|
411
|
+
# Command that produces no output
|
|
412
|
+
document = Document(text="$(true)", cursor_position=7)
|
|
413
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
414
|
+
|
|
415
|
+
# Should have one completion indicating no output
|
|
416
|
+
assert len(completions) == 1
|
|
417
|
+
assert completions[0].text == ""
|
|
418
|
+
assert "no output" in str(completions[0].display)
|
|
419
|
+
|
|
420
|
+
def test_multiline_preserves_order(self):
|
|
421
|
+
"""Test that lines are offered in order."""
|
|
422
|
+
document = Document(text='$(printf "first\\nsecond\\nthird")', cursor_position=32)
|
|
423
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
424
|
+
|
|
425
|
+
# Skip ALL, check line order
|
|
426
|
+
assert completions[1].text == "first"
|
|
427
|
+
assert completions[2].text == "second"
|
|
428
|
+
assert completions[3].text == "third"
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TestPrefixCompleter:
|
|
432
|
+
"""Test PrefixCompleter class."""
|
|
433
|
+
|
|
434
|
+
def setup_method(self):
|
|
435
|
+
"""Set up test fixtures."""
|
|
436
|
+
self.completer = PrefixCompleter(["/help", "/exit", "/quit"], prefix="/")
|
|
437
|
+
self.complete_event = Mock()
|
|
438
|
+
|
|
439
|
+
def test_command_at_start_of_line(self):
|
|
440
|
+
"""Test command completion at start of line."""
|
|
441
|
+
document = Document(text="/he", cursor_position=3)
|
|
442
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
443
|
+
|
|
444
|
+
assert len(completions) == 1
|
|
445
|
+
assert completions[0].text == "/help"
|
|
446
|
+
assert completions[0].start_position == -3
|
|
447
|
+
|
|
448
|
+
def test_command_after_whitespace(self):
|
|
449
|
+
"""Test command completion after whitespace."""
|
|
450
|
+
document = Document(text="some text /he", cursor_position=13)
|
|
451
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
452
|
+
|
|
453
|
+
assert len(completions) == 1
|
|
454
|
+
assert completions[0].text == "/help"
|
|
455
|
+
|
|
456
|
+
def test_no_completion_in_middle_of_text(self):
|
|
457
|
+
"""Test no completion when / is in middle of word."""
|
|
458
|
+
document = Document(text="path/to/file", cursor_position=12)
|
|
459
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
460
|
+
|
|
461
|
+
# Should not complete file paths
|
|
462
|
+
assert len(completions) == 0
|
|
463
|
+
|
|
464
|
+
def test_multiple_matching_commands(self):
|
|
465
|
+
"""Test multiple matching commands."""
|
|
466
|
+
document = Document(text="/e", cursor_position=2)
|
|
467
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
468
|
+
|
|
469
|
+
# Should match /exit (not /help or /quit)
|
|
470
|
+
assert len(completions) == 1
|
|
471
|
+
assert completions[0].text == "/exit"
|
|
472
|
+
|
|
473
|
+
def test_exact_match(self):
|
|
474
|
+
"""Test exact command match."""
|
|
475
|
+
document = Document(text="/help", cursor_position=5)
|
|
476
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
477
|
+
|
|
478
|
+
# Should still offer completion even for exact match
|
|
479
|
+
assert len(completions) == 1
|
|
480
|
+
assert completions[0].text == "/help"
|
|
481
|
+
|
|
482
|
+
def test_no_match(self):
|
|
483
|
+
"""Test no matching commands."""
|
|
484
|
+
document = Document(text="/xyz", cursor_position=4)
|
|
485
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
486
|
+
|
|
487
|
+
assert len(completions) == 0
|
|
488
|
+
|
|
489
|
+
def test_case_insensitive(self):
|
|
490
|
+
"""Test case-insensitive matching."""
|
|
491
|
+
completer = PrefixCompleter(["/Help", "/Exit"], ignore_case=True)
|
|
492
|
+
|
|
493
|
+
document = Document(text="/he", cursor_position=3)
|
|
494
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
495
|
+
|
|
496
|
+
assert len(completions) == 1
|
|
497
|
+
assert completions[0].text == "/Help"
|
|
498
|
+
|
|
499
|
+
def test_case_sensitive(self):
|
|
500
|
+
"""Test case-sensitive matching."""
|
|
501
|
+
completer = PrefixCompleter(["/Help", "/Exit"], ignore_case=False)
|
|
502
|
+
|
|
503
|
+
document = Document(text="/he", cursor_position=3)
|
|
504
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
505
|
+
|
|
506
|
+
# Should not match /Help (capital H)
|
|
507
|
+
assert len(completions) == 0
|
|
508
|
+
|
|
509
|
+
def test_commands_without_leading_slash(self):
|
|
510
|
+
"""Test that commands without / get it added."""
|
|
511
|
+
completer = PrefixCompleter(["help", "exit"], prefix="/")
|
|
512
|
+
|
|
513
|
+
document = Document(text="/he", cursor_position=3)
|
|
514
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
515
|
+
|
|
516
|
+
assert len(completions) == 1
|
|
517
|
+
assert completions[0].text == "/help"
|
|
518
|
+
|
|
519
|
+
def test_command_after_newline(self):
|
|
520
|
+
"""Test command completion after newline."""
|
|
521
|
+
document = Document(text="line1\n/he", cursor_position=9)
|
|
522
|
+
completions = list(self.completer.get_completions(document, self.complete_event))
|
|
523
|
+
|
|
524
|
+
assert len(completions) == 1
|
|
525
|
+
assert completions[0].text == "/help"
|
|
526
|
+
|
|
527
|
+
def test_import_from_package(self):
|
|
528
|
+
"""Test that PrefixCompleter can be imported."""
|
|
529
|
+
from repl_toolkit import PrefixCompleter
|
|
530
|
+
|
|
531
|
+
completer = PrefixCompleter(["/test"])
|
|
532
|
+
assert completer is not None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class TestShellExpansionCompleterLimits:
|
|
536
|
+
"""Test limit features of ShellExpansionCompleter."""
|
|
537
|
+
|
|
538
|
+
def setup_method(self):
|
|
539
|
+
"""Set up test fixtures."""
|
|
540
|
+
self.complete_event = Mock()
|
|
541
|
+
|
|
542
|
+
def test_default_limits(self):
|
|
543
|
+
"""Test default limit values."""
|
|
544
|
+
completer = ShellExpansionCompleter()
|
|
545
|
+
assert completer.max_lines == 50
|
|
546
|
+
assert completer.max_display_length == 80
|
|
547
|
+
|
|
548
|
+
def test_line_limit_enforcement(self):
|
|
549
|
+
"""Test that max_lines limit is enforced in menu."""
|
|
550
|
+
completer = ShellExpansionCompleter(max_lines=5)
|
|
551
|
+
|
|
552
|
+
# Command that produces 10 lines
|
|
553
|
+
cmd = 'printf "1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n10"'
|
|
554
|
+
document = Document(text=f"$({cmd})", cursor_position=len(f"$({cmd})"))
|
|
555
|
+
|
|
556
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
557
|
+
|
|
558
|
+
# Should have: ALL + 5 lines + "more lines" indicator = 7
|
|
559
|
+
assert len(completions) == 7
|
|
560
|
+
|
|
561
|
+
# ALL should have full output
|
|
562
|
+
all_comp = completions[0]
|
|
563
|
+
assert "10" in all_comp.text
|
|
564
|
+
assert "ALL (10 lines)" in str(all_comp.display)
|
|
565
|
+
|
|
566
|
+
# Check individual lines shown (should be 5)
|
|
567
|
+
line_comps = [c for c in completions[1:] if "Line " in str(c.display) and c.text]
|
|
568
|
+
assert len(line_comps) == 5
|
|
569
|
+
|
|
570
|
+
# Check "more lines" indicator
|
|
571
|
+
more_indicator = [c for c in completions if "more lines" in str(c.display)]
|
|
572
|
+
assert len(more_indicator) == 1
|
|
573
|
+
assert "5 more lines" in str(more_indicator[0].display)
|
|
574
|
+
|
|
575
|
+
def test_display_length_truncation(self):
|
|
576
|
+
"""Test that display is truncated but completion text is full."""
|
|
577
|
+
completer = ShellExpansionCompleter(max_display_length=20)
|
|
578
|
+
|
|
579
|
+
# Command with long output
|
|
580
|
+
long_text = "A" * 100
|
|
581
|
+
cmd = f'printf "{long_text}"'
|
|
582
|
+
document = Document(text=f"$({cmd})", cursor_position=len(f"$({cmd})"))
|
|
583
|
+
|
|
584
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
585
|
+
|
|
586
|
+
assert len(completions) == 1
|
|
587
|
+
completion = completions[0]
|
|
588
|
+
|
|
589
|
+
# Full text in completion
|
|
590
|
+
assert len(completion.text) == 100
|
|
591
|
+
assert completion.text == long_text
|
|
592
|
+
|
|
593
|
+
# Truncated display
|
|
594
|
+
assert "..." in str(completion.display)
|
|
595
|
+
|
|
596
|
+
def test_env_variable_display_truncation(self):
|
|
597
|
+
"""Test display truncation for long environment variables."""
|
|
598
|
+
long_value = "X" * 100
|
|
599
|
+
os.environ["TEST_LONG_VAR"] = long_value
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
completer = ShellExpansionCompleter(max_display_length=25)
|
|
603
|
+
|
|
604
|
+
document = Document(text="${TEST_LONG_VAR}", cursor_position=16)
|
|
605
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
606
|
+
|
|
607
|
+
assert len(completions) == 1
|
|
608
|
+
completion = completions[0]
|
|
609
|
+
|
|
610
|
+
# Full value in completion
|
|
611
|
+
assert len(completion.text) == 100
|
|
612
|
+
assert completion.text == long_value
|
|
613
|
+
|
|
614
|
+
# Truncated in display
|
|
615
|
+
assert "..." in str(completion.display)
|
|
616
|
+
finally:
|
|
617
|
+
del os.environ["TEST_LONG_VAR"]
|
|
618
|
+
|
|
619
|
+
def test_combined_limits(self):
|
|
620
|
+
"""Test combination of line limit and display length limit."""
|
|
621
|
+
completer = ShellExpansionCompleter(max_lines=3, max_display_length=30)
|
|
622
|
+
|
|
623
|
+
# Command with 5 long lines
|
|
624
|
+
long_line = "B" * 60
|
|
625
|
+
cmd = f'printf "{long_line}\\n{long_line}\\n{long_line}\\n{long_line}\\n{long_line}"'
|
|
626
|
+
document = Document(text=f"$({cmd})", cursor_position=len(f"$({cmd})"))
|
|
627
|
+
|
|
628
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
629
|
+
|
|
630
|
+
# ALL + 3 lines + "more lines" indicator = 5
|
|
631
|
+
assert len(completions) == 5
|
|
632
|
+
|
|
633
|
+
# ALL has full content
|
|
634
|
+
all_comp = completions[0]
|
|
635
|
+
assert long_line in all_comp.text
|
|
636
|
+
|
|
637
|
+
# Check individual lines
|
|
638
|
+
line_comps = [c for c in completions[1:] if "Line " in str(c.display) and c.text]
|
|
639
|
+
assert len(line_comps) == 3
|
|
640
|
+
|
|
641
|
+
# Each line should have full text but truncated display
|
|
642
|
+
for comp in line_comps:
|
|
643
|
+
assert len(comp.text) == 60
|
|
644
|
+
assert "..." in str(comp.display)
|
|
645
|
+
|
|
646
|
+
def test_no_truncation_when_under_limits(self):
|
|
647
|
+
"""Test that short content is not truncated."""
|
|
648
|
+
completer = ShellExpansionCompleter(max_lines=10, max_display_length=50)
|
|
649
|
+
|
|
650
|
+
# Short command output
|
|
651
|
+
document = Document(text='$(printf "short")', cursor_position=16)
|
|
652
|
+
completions = list(completer.get_completions(document, self.complete_event))
|
|
653
|
+
|
|
654
|
+
assert len(completions) == 1
|
|
655
|
+
assert "..." not in str(completions[0].display)
|
|
656
|
+
assert completions[0].text == "short"
|