Skip to content

Commit 69aeee7

Browse files
authored
Merge pull request #618 from bpython/size-dependent-scroll
completion box size changes based on terminal size
2 parents f11bccb + aa4ff42 commit 69aeee7

File tree

2 files changed

+192
-18
lines changed

2 files changed

+192
-18
lines changed

bpython/curtsiesfrontend/repl.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,16 +1256,22 @@ def current_output_line(self, value):
12561256
self.current_stdouterr_line = ''
12571257
self.stdin.current_line = '\n'
12581258

1259-
def paint(self, about_to_exit=False, user_quit=False):
1259+
def paint(self, about_to_exit=False, user_quit=False,
1260+
try_preserve_history_height=30,
1261+
min_infobox_height=5):
12601262
"""Returns an array of min_height or more rows and width columns, plus
12611263
cursor position
12621264
12631265
Paints the entire screen - ideally the terminal display layer will take
12641266
a diff and only write to the screen in portions that have changed, but
12651267
the idea is that we don't need to worry about that here, instead every
12661268
frame is completely redrawn because less state is cool!
1269+
1270+
try_preserve_history_height is the the number of rows of content that
1271+
must be visible before the suggestion box scrolls the terminal in order
1272+
to display more than min_infobox_height rows of suggestions, docs etc.
12671273
"""
1268-
# The hairiest function in the curtsies - a cleanup would be great.
1274+
# The hairiest function in the curtsies
12691275
if about_to_exit:
12701276
# exception to not changing state!
12711277
self.clean_up_current_line_for_exit()
@@ -1415,10 +1421,18 @@ def move_screen_up(current_line_start_row):
14151421
if self.config.curtsies_list_above:
14161422
info_max_rows = max(visible_space_above, visible_space_below)
14171423
else:
1424+
# Logic for determining size of completion box
14181425
# smallest allowed over-full completion box
1419-
minimum_possible_height = 20
1426+
preferred_height = max(
1427+
# always make infobox at least this height
1428+
min_infobox_height,
1429+
1430+
# use this value if there's so much space that we can
1431+
# preserve this try_preserve_history_height rows history
1432+
min_height - try_preserve_history_height)
1433+
14201434
info_max_rows = min(max(visible_space_below,
1421-
minimum_possible_height),
1435+
preferred_height),
14221436
min_height - current_line_height - 1)
14231437
infobox = paint.paint_infobox(
14241438
info_max_rows,
@@ -1506,6 +1520,7 @@ def _set_current_line(self, line, update_completion=True,
15061520
if clear_special_mode:
15071521
self.special_mode = None
15081522
self.unhighlight_paren()
1523+
15091524
current_line = property(_get_current_line, _set_current_line, None,
15101525
"The current line")
15111526

bpython/test/test_curtsies_painting.py

Lines changed: 173 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# coding: utf8
22
from __future__ import unicode_literals
3-
import sys
3+
import itertools
4+
import string
45
import os
6+
import sys
57
from contextlib import contextmanager
68

79
from curtsies.formatstringarray import FormatStringTest, fsarray
@@ -21,6 +23,7 @@
2123
def setup_config():
2224
config_struct = config.Struct()
2325
config.loadini(config_struct, os.devnull)
26+
config_struct.cli_suggestion_width = 1
2427
return config_struct
2528

2629

@@ -48,13 +51,18 @@ def _request_refresh(inner_self):
4851
self.repl.rl_history = History()
4952
self.repl.height, self.repl.width = (5, 10)
5053

54+
@property
55+
def locals(self):
56+
return self.repl.coderunner.interp.locals
57+
5158
def assert_paint(self, screen, cursor_row_col):
5259
array, cursor_pos = self.repl.paint()
5360
self.assertFSArraysEqual(array, screen)
5461
self.assertEqual(cursor_pos, cursor_row_col)
5562

56-
def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None):
57-
array, cursor_pos = self.repl.paint()
63+
def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None,
64+
**paint_kwargs):
65+
array, cursor_pos = self.repl.paint(**paint_kwargs)
5866
self.assertFSArraysEqualIgnoringFormatting(array, screen)
5967
if cursor_row_col is not None:
6068
self.assertEqual(cursor_pos, cursor_row_col)
@@ -96,15 +104,15 @@ def test_completion(self):
96104
self.cursor_offset = 2
97105
if config.supports_box_chars():
98106
screen = ['>>> an',
99-
'┌───────────────────────┐',
100-
'│ and any( │',
101-
'└───────────────────────┘',
107+
'┌──────────────────────────────┐',
108+
'│ and any( │',
109+
'└──────────────────────────────┘',
102110
'Welcome to bpython! Press <F1> f']
103111
else:
104112
screen = ['>>> an',
105-
'+-----------------------+',
106-
'| and any( |',
107-
'+-----------------------+',
113+
'+------------------------------+',
114+
'| and any( |',
115+
'+------------------------------+',
108116
'Welcome to bpython! Press <F1> f']
109117
self.assert_paint_ignoring_formatting(screen, (0, 4))
110118

@@ -155,7 +163,7 @@ def output_to_repl(repl):
155163
sys.stdout, sys.stderr = old_out, old_err
156164

157165

158-
class TestCurtsiesRewindRedraw(CurtsiesPaintingTest):
166+
class HigherLevelCurtsiesPaintingTest(CurtsiesPaintingTest):
159167
def refresh(self):
160168
self.refresh_requests.append(RefreshRequestEvent())
161169

@@ -194,6 +202,12 @@ def _request_refresh(inner_self):
194202
self.repl.rl_history = History()
195203
self.repl.height, self.repl.width = (5, 32)
196204

205+
def send_key(self, key):
206+
self.repl.process_event('<SPACE>' if key == ' ' else key)
207+
self.repl.paint() # has some side effects we need to be wary of
208+
209+
210+
class TestCurtsiesRewindRedraw(HigherLevelCurtsiesPaintingTest):
197211
def test_rewind(self):
198212
self.repl.current_line = '1 + 1'
199213
self.enter()
@@ -564,10 +578,6 @@ def test_unhighlight_paren_bugs(self):
564578
green("... ") + yellow(')') + bold(cyan(" "))])
565579
self.assert_paint(screen, (1, 6))
566580

567-
def send_key(self, key):
568-
self.repl.process_event('<SPACE>' if key == ' ' else key)
569-
self.repl.paint() # has some side effects we need to be wary of
570-
571581
def test_472(self):
572582
[self.send_key(c) for c in "(1, 2, 3)"]
573583
with output_to_repl(self.repl):
@@ -586,3 +596,152 @@ def test_472(self):
586596
'(1, 4, 3)',
587597
'>>> ']
588598
self.assert_paint_ignoring_formatting(screen, (4, 4))
599+
600+
601+
def completion_target(num_names, chars_in_first_name=1):
602+
class Class(object):
603+
pass
604+
605+
if chars_in_first_name < 1:
606+
raise ValueError('need at least one char in each name')
607+
elif chars_in_first_name == 1 and num_names > len(string.ascii_letters):
608+
raise ValueError('need more chars to make so many names')
609+
610+
names = gen_names()
611+
if num_names > 0:
612+
setattr(Class, 'a' * chars_in_first_name, 1)
613+
next(names) # use the above instead of first name
614+
for _, name in zip(range(num_names - 1), names):
615+
setattr(Class, name, 0)
616+
617+
return Class()
618+
619+
620+
def gen_names():
621+
for letters in itertools.chain(
622+
itertools.combinations_with_replacement(string.ascii_letters, 1),
623+
itertools.combinations_with_replacement(string.ascii_letters, 2)):
624+
yield ''.join(letters)
625+
626+
627+
class TestCompletionHelpers(TestCase):
628+
def test_gen_names(self):
629+
self.assertEqual(list(zip([1, 2, 3], gen_names())),
630+
[(1, 'a'), (2, 'b'), (3, 'c')])
631+
632+
def test_completion_target(self):
633+
target = completion_target(14)
634+
self.assertEqual(len([x for x in dir(target)
635+
if not x.startswith('_')]),
636+
14)
637+
638+
639+
class TestCurtsiesInfoboxPaint(HigherLevelCurtsiesPaintingTest):
640+
def test_simple(self):
641+
self.repl.width, self.repl.height = (20, 30)
642+
self.locals['abc'] = completion_target(3, 50)
643+
self.repl.current_line = 'abc'
644+
self.repl.cursor_offset = 3
645+
self.repl.process_event('.')
646+
screen = ['>>> abc.',
647+
'+------------------+',
648+
'| aaaaaaaaaaaaaaaa |',
649+
'| b |',
650+
'| c |',
651+
'+------------------+']
652+
self.assert_paint_ignoring_formatting(screen, (0, 8))
653+
654+
def test_fill_screen(self):
655+
self.repl.width, self.repl.height = (20, 15)
656+
self.locals['abc'] = completion_target(20, 100)
657+
self.repl.current_line = 'abc'
658+
self.repl.cursor_offset = 3
659+
self.repl.process_event('.')
660+
screen = ['>>> abc.',
661+
'+------------------+',
662+
'| aaaaaaaaaaaaaaaa |',
663+
'| b |',
664+
'| c |',
665+
'| d |',
666+
'| e |',
667+
'| f |',
668+
'| g |',
669+
'| h |',
670+
'| i |',
671+
'| j |',
672+
'| k |',
673+
'| l |',
674+
'+------------------+']
675+
self.assert_paint_ignoring_formatting(screen, (0, 8))
676+
677+
def test_lower_on_screen(self):
678+
self.repl.get_top_usable_line = lambda: 10 # halfway down terminal
679+
self.repl.width, self.repl.height = (20, 15)
680+
self.locals['abc'] = completion_target(20, 100)
681+
self.repl.current_line = 'abc'
682+
self.repl.cursor_offset = 3
683+
self.repl.process_event('.')
684+
screen = ['>>> abc.',
685+
'+------------------+',
686+
'| aaaaaaaaaaaaaaaa |',
687+
'| b |',
688+
'| c |',
689+
'| d |',
690+
'| e |',
691+
'| f |',
692+
'| g |',
693+
'| h |',
694+
'| i |',
695+
'| j |',
696+
'| k |',
697+
'| l |',
698+
'+------------------+']
699+
# behavior before issue #466
700+
self.assert_paint_ignoring_formatting(
701+
screen, try_preserve_history_height=0)
702+
self.assert_paint_ignoring_formatting(
703+
screen, min_infobox_height=100)
704+
# behavior after issue #466
705+
screen = ['>>> abc.',
706+
'+------------------+',
707+
'| aaaaaaaaaaaaaaaa |',
708+
'| b |',
709+
'| c |',
710+
'+------------------+']
711+
self.assert_paint_ignoring_formatting(screen)
712+
713+
def test_at_bottom_of_screen(self):
714+
self.repl.get_top_usable_line = lambda: 17 # two lines from bottom
715+
self.repl.width, self.repl.height = (20, 15)
716+
self.locals['abc'] = completion_target(20, 100)
717+
self.repl.current_line = 'abc'
718+
self.repl.cursor_offset = 3
719+
self.repl.process_event('.')
720+
screen = ['>>> abc.',
721+
'+------------------+',
722+
'| aaaaaaaaaaaaaaaa |',
723+
'| b |',
724+
'| c |',
725+
'| d |',
726+
'| e |',
727+
'| f |',
728+
'| g |',
729+
'| h |',
730+
'| i |',
731+
'| j |',
732+
'| k |',
733+
'| l |',
734+
'+------------------+']
735+
# behavior before issue #466
736+
self.assert_paint_ignoring_formatting(
737+
screen, try_preserve_history_height=0)
738+
self.assert_paint_ignoring_formatting(
739+
screen, min_infobox_height=100)
740+
# behavior after issue #466
741+
screen = ['>>> abc.',
742+
'+------------------+',
743+
'| aaaaaaaaaaaaaaaa |',
744+
'| b |',
745+
'| c |',
746+
'+------------------+']
747+
self.assert_paint_ignoring_formatting(screen)

0 commit comments

Comments
 (0)