Skip to content

completion box size changes based on terminal size #618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions bpython/curtsiesfrontend/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,16 +1251,22 @@ def current_output_line(self, value):
self.current_stdouterr_line = ''
self.stdin.current_line = '\n'

def paint(self, about_to_exit=False, user_quit=False):
def paint(self, about_to_exit=False, user_quit=False,
try_preserve_history_height=30,
min_infobox_height=5):
"""Returns an array of min_height or more rows and width columns, plus
cursor position

Paints the entire screen - ideally the terminal display layer will take
a diff and only write to the screen in portions that have changed, but
the idea is that we don't need to worry about that here, instead every
frame is completely redrawn because less state is cool!

try_preserve_history_height is the the number of rows of content that
must be visible before the suggestion box scrolls the terminal in order
to display more than min_infobox_height rows of suggestions, docs etc.
"""
# The hairiest function in the curtsies - a cleanup would be great.
# The hairiest function in the curtsies
if about_to_exit:
# exception to not changing state!
self.clean_up_current_line_for_exit()
Expand Down Expand Up @@ -1410,10 +1416,18 @@ def move_screen_up(current_line_start_row):
if self.config.curtsies_list_above:
info_max_rows = max(visible_space_above, visible_space_below)
else:
# Logic for determining size of completion box
# smallest allowed over-full completion box
minimum_possible_height = 20
preferred_height = max(
# always make infobox at least this height
min_infobox_height,

# use this value if there's so much space that we can
# preserve this try_preserve_history_height rows history
min_height - try_preserve_history_height)

info_max_rows = min(max(visible_space_below,
minimum_possible_height),
preferred_height),
min_height - current_line_height - 1)
infobox = paint.paint_infobox(
info_max_rows,
Expand Down Expand Up @@ -1501,6 +1515,7 @@ def _set_current_line(self, line, update_completion=True,
if clear_special_mode:
self.special_mode = None
self.unhighlight_paren()

current_line = property(_get_current_line, _set_current_line, None,
"The current line")

Expand Down
187 changes: 173 additions & 14 deletions bpython/test/test_curtsies_painting.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# coding: utf8
from __future__ import unicode_literals
import sys
import itertools
import string
import os
import sys
from contextlib import contextmanager

from curtsies.formatstringarray import FormatStringTest, fsarray
Expand All @@ -21,6 +23,7 @@
def setup_config():
config_struct = config.Struct()
config.loadini(config_struct, os.devnull)
config_struct.cli_suggestion_width = 1
return config_struct


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

@property
def locals(self):
return self.repl.coderunner.interp.locals

def assert_paint(self, screen, cursor_row_col):
array, cursor_pos = self.repl.paint()
self.assertFSArraysEqual(array, screen)
self.assertEqual(cursor_pos, cursor_row_col)

def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None):
array, cursor_pos = self.repl.paint()
def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None,
**paint_kwargs):
array, cursor_pos = self.repl.paint(**paint_kwargs)
self.assertFSArraysEqualIgnoringFormatting(array, screen)
if cursor_row_col is not None:
self.assertEqual(cursor_pos, cursor_row_col)
Expand Down Expand Up @@ -96,15 +104,15 @@ def test_completion(self):
self.cursor_offset = 2
if config.supports_box_chars():
screen = ['>>> an',
'┌───────────────────────┐',
'│ and any( │',
'└───────────────────────┘',
'┌──────────────────────────────┐',
'│ and any( │',
'└──────────────────────────────┘',
'Welcome to bpython! Press <F1> f']
else:
screen = ['>>> an',
'+-----------------------+',
'| and any( |',
'+-----------------------+',
'+------------------------------+',
'| and any( |',
'+------------------------------+',
'Welcome to bpython! Press <F1> f']
self.assert_paint_ignoring_formatting(screen, (0, 4))

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


class TestCurtsiesRewindRedraw(CurtsiesPaintingTest):
class HigherLevelCurtsiesPaintingTest(CurtsiesPaintingTest):
def refresh(self):
self.refresh_requests.append(RefreshRequestEvent())

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

def send_key(self, key):
self.repl.process_event('<SPACE>' if key == ' ' else key)
self.repl.paint() # has some side effects we need to be wary of


class TestCurtsiesRewindRedraw(HigherLevelCurtsiesPaintingTest):
def test_rewind(self):
self.repl.current_line = '1 + 1'
self.enter()
Expand Down Expand Up @@ -564,10 +578,6 @@ def test_unhighlight_paren_bugs(self):
green("... ") + yellow(')') + bold(cyan(" "))])
self.assert_paint(screen, (1, 6))

def send_key(self, key):
self.repl.process_event('<SPACE>' if key == ' ' else key)
self.repl.paint() # has some side effects we need to be wary of

def test_472(self):
[self.send_key(c) for c in "(1, 2, 3)"]
with output_to_repl(self.repl):
Expand All @@ -586,3 +596,152 @@ def test_472(self):
'(1, 4, 3)',
'>>> ']
self.assert_paint_ignoring_formatting(screen, (4, 4))


def completion_target(num_names, chars_in_first_name=1):
class Class(object):
pass

if chars_in_first_name < 1:
raise ValueError('need at least one char in each name')
elif chars_in_first_name == 1 and num_names > len(string.ascii_letters):
raise ValueError('need more chars to make so many names')

names = gen_names()
if num_names > 0:
setattr(Class, 'a' * chars_in_first_name, 1)
next(names) # use the above instead of first name
for _, name in zip(range(num_names - 1), names):
setattr(Class, name, 0)

return Class()


def gen_names():
for letters in itertools.chain(
itertools.combinations_with_replacement(string.ascii_letters, 1),
itertools.combinations_with_replacement(string.ascii_letters, 2)):
yield ''.join(letters)


class TestCompletionHelpers(TestCase):
def test_gen_names(self):
self.assertEqual(list(zip([1, 2, 3], gen_names())),
[(1, 'a'), (2, 'b'), (3, 'c')])

def test_completion_target(self):
target = completion_target(14)
self.assertEqual(len([x for x in dir(target)
if not x.startswith('_')]),
14)


class TestCurtsiesInfoboxPaint(HigherLevelCurtsiesPaintingTest):
def test_simple(self):
self.repl.width, self.repl.height = (20, 30)
self.locals['abc'] = completion_target(3, 50)
self.repl.current_line = 'abc'
self.repl.cursor_offset = 3
self.repl.process_event('.')
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'+------------------+']
self.assert_paint_ignoring_formatting(screen, (0, 8))

def test_fill_screen(self):
self.repl.width, self.repl.height = (20, 15)
self.locals['abc'] = completion_target(20, 100)
self.repl.current_line = 'abc'
self.repl.cursor_offset = 3
self.repl.process_event('.')
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'| d |',
'| e |',
'| f |',
'| g |',
'| h |',
'| i |',
'| j |',
'| k |',
'| l |',
'+------------------+']
self.assert_paint_ignoring_formatting(screen, (0, 8))

def test_lower_on_screen(self):
self.repl.get_top_usable_line = lambda: 10 # halfway down terminal
self.repl.width, self.repl.height = (20, 15)
self.locals['abc'] = completion_target(20, 100)
self.repl.current_line = 'abc'
self.repl.cursor_offset = 3
self.repl.process_event('.')
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'| d |',
'| e |',
'| f |',
'| g |',
'| h |',
'| i |',
'| j |',
'| k |',
'| l |',
'+------------------+']
# behavior before issue #466
self.assert_paint_ignoring_formatting(
screen, try_preserve_history_height=0)
self.assert_paint_ignoring_formatting(
screen, min_infobox_height=100)
# behavior after issue #466
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'+------------------+']
self.assert_paint_ignoring_formatting(screen)

def test_at_bottom_of_screen(self):
self.repl.get_top_usable_line = lambda: 17 # two lines from bottom
self.repl.width, self.repl.height = (20, 15)
self.locals['abc'] = completion_target(20, 100)
self.repl.current_line = 'abc'
self.repl.cursor_offset = 3
self.repl.process_event('.')
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'| d |',
'| e |',
'| f |',
'| g |',
'| h |',
'| i |',
'| j |',
'| k |',
'| l |',
'+------------------+']
# behavior before issue #466
self.assert_paint_ignoring_formatting(
screen, try_preserve_history_height=0)
self.assert_paint_ignoring_formatting(
screen, min_infobox_height=100)
# behavior after issue #466
screen = ['>>> abc.',
'+------------------+',
'| aaaaaaaaaaaaaaaa |',
'| b |',
'| c |',
'+------------------+']
self.assert_paint_ignoring_formatting(screen)