Highlight trailing whitespace
This commit is contained in:
@@ -4,6 +4,7 @@ import os
|
||||
import sys
|
||||
from typing import List
|
||||
from typing import NamedTuple
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
from unittest import mock
|
||||
|
||||
@@ -36,6 +37,7 @@ class Screen:
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.lines = [' ' * self.width for _ in range(self.height)]
|
||||
self.attrs = [[(0, 0, 0)] * self.width for _ in range(self.height)]
|
||||
self.x = self.y = 0
|
||||
self._prev_screenshot = None
|
||||
|
||||
@@ -48,10 +50,17 @@ class Screen:
|
||||
self._prev_screenshot = ret
|
||||
return ret
|
||||
|
||||
def insstr(self, y, x, s):
|
||||
def insstr(self, y, x, s, attr):
|
||||
line = self.lines[y]
|
||||
self.lines[y] = (line[:x] + s + line[x:])[:self.width]
|
||||
|
||||
line_attr = self.attrs[y]
|
||||
new = [attr] * len(s)
|
||||
self.attrs[y] = (line_attr[:x] + new + line_attr[x:])[:self.width]
|
||||
|
||||
def chgat(self, y, x, n, attr):
|
||||
self.attrs[y][x:x + n] = [attr] * n
|
||||
|
||||
def move(self, y, x):
|
||||
assert 0 <= y < self.height
|
||||
assert 0 <= x < self.width
|
||||
@@ -113,6 +122,14 @@ class AssertScreenLineEquals(NamedTuple):
|
||||
assert screen.lines[self.n].rstrip() == self.line
|
||||
|
||||
|
||||
class AssertScreenAttrEquals(NamedTuple):
|
||||
n: int
|
||||
attr: List[Tuple[int, int, int]]
|
||||
|
||||
def __call__(self, screen: Screen) -> None:
|
||||
assert screen.attrs[self.n] == self.attr
|
||||
|
||||
|
||||
class AssertFullContents(NamedTuple):
|
||||
contents: str
|
||||
|
||||
@@ -144,6 +161,19 @@ class CursesError(NamedTuple):
|
||||
class CursesScreen:
|
||||
def __init__(self, runner):
|
||||
self._runner = runner
|
||||
self._bkgd_attr = (-1, -1, 0)
|
||||
|
||||
def _to_attr(self, attr):
|
||||
if attr == 0:
|
||||
return self._bkgd_attr
|
||||
else:
|
||||
pair = (attr & (0xff << 8)) >> 8
|
||||
if pair == 0:
|
||||
fg, bg, _ = self._bkgd_attr
|
||||
else:
|
||||
fg, bg = self._runner.color_pairs[pair]
|
||||
attr = attr & ~(0xff << 8)
|
||||
return (fg, bg, attr)
|
||||
|
||||
def keypad(self, val):
|
||||
pass
|
||||
@@ -152,14 +182,14 @@ class CursesScreen:
|
||||
self._runner.screen.nodelay = val
|
||||
|
||||
def insstr(self, y, x, s, attr=0):
|
||||
self._runner.screen.insstr(y, x, s)
|
||||
self._runner.screen.insstr(y, x, s, self._to_attr(attr))
|
||||
|
||||
def clrtoeol(self):
|
||||
s = self._runner.screen.width * ' '
|
||||
self.insstr(self._runner.screen.y, self._runner.screen.x, s)
|
||||
|
||||
def chgat(self, y, x, n, color):
|
||||
pass
|
||||
def chgat(self, y, x, n, attr):
|
||||
self._runner.screen.chgat(y, x, n, self._to_attr(attr))
|
||||
|
||||
def move(self, y, x):
|
||||
self._runner.screen.move(y, x)
|
||||
@@ -234,6 +264,7 @@ class DeferredRunner:
|
||||
self.command = command
|
||||
self._i = 0
|
||||
self._ops: List[Op] = []
|
||||
self.color_pairs = {0: (7, 0)}
|
||||
self.screen = Screen(width, height)
|
||||
self._n_colors, self._can_change_color = {
|
||||
'screen': (8, False),
|
||||
@@ -270,6 +301,9 @@ class DeferredRunner:
|
||||
def assert_screen_line_equals(self, n, line):
|
||||
self._ops.append(AssertScreenLineEquals(n, line))
|
||||
|
||||
def assert_screen_attr_equals(self, n, attr):
|
||||
self._ops.append(AssertScreenAttrEquals(n, attr))
|
||||
|
||||
def assert_full_contents(self, contents):
|
||||
self._ops.append(AssertFullContents(contents))
|
||||
|
||||
@@ -319,8 +353,8 @@ class DeferredRunner:
|
||||
def _curses__noop(self, *_, **__):
|
||||
pass
|
||||
|
||||
_curses_cbreak = _curses_init_pair = _curses_noecho = _curses__noop
|
||||
_curses_nonl = _curses_raw = _curses_use_default_colors = _curses__noop
|
||||
_curses_cbreak = _curses_noecho = _curses_nonl = _curses__noop
|
||||
_curses_raw = _curses_use_default_colors = _curses__noop
|
||||
|
||||
_curses_error = curses.error # so we don't mock the exception
|
||||
|
||||
@@ -334,6 +368,13 @@ class DeferredRunner:
|
||||
def _curses_start_color(self):
|
||||
curses.COLORS = self._n_colors
|
||||
|
||||
def _curses_init_pair(self, pair, fg, bg):
|
||||
self.color_pairs[pair] = (fg, bg)
|
||||
|
||||
def _curses_color_pair(self, pair):
|
||||
assert pair in self.color_pairs
|
||||
return pair << 8
|
||||
|
||||
def _curses_initscr(self):
|
||||
self._curses_update_lines_cols()
|
||||
self.screen.disabled = False
|
||||
|
||||
23
tests/features/trailing_whitespace_test.py
Normal file
23
tests/features/trailing_whitespace_test.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import curses
|
||||
|
||||
from testing.runner import and_exit
|
||||
|
||||
|
||||
def test_trailing_whitespace_highlighting(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('0123456789 \n')
|
||||
|
||||
with run(str(f), term='screen-256color', width=20) as h, and_exit(h):
|
||||
h.await_text('123456789')
|
||||
h.assert_screen_attr_equals(0, [(-1, -1, curses.A_REVERSE)] * 20)
|
||||
attrs = [(-1, -1, 0)] * 10 + [(-1, 1, 0)] * 5 + [(-1, -1, 0)] * 5
|
||||
h.assert_screen_attr_equals(1, attrs)
|
||||
|
||||
|
||||
def test_trailing_whitespace_does_not_highlight_line_continuation(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write(f'{" " * 30}\nhello\n')
|
||||
|
||||
with run(str(f), term='screen-256color', width=20) as h, and_exit(h):
|
||||
h.await_text('hello')
|
||||
h.assert_screen_attr_equals(1, [(-1, 1, 0)] * 19 + [(-1, -1, 0)])
|
||||
@@ -7,23 +7,8 @@ from babi.file import get_lines
|
||||
|
||||
|
||||
def test_position_repr():
|
||||
ret = repr(File('f.txt'))
|
||||
assert ret == (
|
||||
'File(\n'
|
||||
" filename='f.txt',\n"
|
||||
' modified=False,\n'
|
||||
' lines=[],\n'
|
||||
" nl='\\n',\n"
|
||||
' file_y=0,\n'
|
||||
' y=0,\n'
|
||||
' x=0,\n'
|
||||
' x_hint=0,\n'
|
||||
' sha256=None,\n'
|
||||
' undo_stack=[],\n'
|
||||
' redo_stack=[],\n'
|
||||
' select_start=None,\n'
|
||||
')'
|
||||
)
|
||||
ret = repr(File('f.txt', ()))
|
||||
assert ret == "<File 'f.txt'>"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user