tfdbg: Fix a bug related to regex search and scrolling when there is line wrapping
The issue: Prior to this CL, if there are is wrapping and you do regex search and scrolling, the output pad will scroll according to line indices in the original (unwrapped) text lines instead of the wrapped text lines, which misses the actual matches. This CL fixes this issue. Change: 138191265
This commit is contained in:
parent
59daeca315
commit
9ec30b9727
@ -168,7 +168,7 @@ class CursesUI(object):
|
||||
|
||||
# Regex search state.
|
||||
self._curr_search_regex = None
|
||||
self._regex_match_lines = None
|
||||
self._unwrapped_regex_match_lines = []
|
||||
|
||||
# Size of view port on screen, which is always smaller or equal to the
|
||||
# screen size.
|
||||
@ -433,7 +433,7 @@ class CursesUI(object):
|
||||
|
||||
self._curr_search_regex = regex
|
||||
self._display_output(self._curr_unwrapped_output, highlight_regex=regex)
|
||||
elif self._regex_match_lines:
|
||||
elif self._unwrapped_regex_match_lines:
|
||||
# Command is "/". Continue scrolling down matching lines.
|
||||
self._display_output(
|
||||
self._curr_unwrapped_output,
|
||||
@ -682,11 +682,15 @@ class CursesUI(object):
|
||||
output: (RichTextLines) text lines to display on the screen. These lines
|
||||
may have widths exceeding the screen width. This method will take care
|
||||
of the wrapping.
|
||||
|
||||
Returns:
|
||||
(List of int) A list of line indices, in the wrapped output, where there
|
||||
are regex matches.
|
||||
"""
|
||||
|
||||
# Wrap the output lines according to screen width.
|
||||
self._curr_wrapped_output = debugger_cli_common.wrap_rich_text_lines(
|
||||
output, self._max_x - 1)
|
||||
self._curr_wrapped_output, wrapped_line_indices = (
|
||||
debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 1))
|
||||
|
||||
# Append lines to curr_wrapped_output so that the user can scroll to a
|
||||
# state where the last text line is on the top of the output area.
|
||||
@ -706,13 +710,20 @@ class CursesUI(object):
|
||||
self._output_pad_width) = self._display_lines(self._curr_wrapped_output,
|
||||
self._output_num_rows)
|
||||
|
||||
# The indices of lines with regex matches (if any) need to be mapped to
|
||||
# indices of wrapped lines.
|
||||
return [
|
||||
wrapped_line_indices[line]
|
||||
for line in self._unwrapped_regex_match_lines
|
||||
]
|
||||
|
||||
def _display_output(self, output, is_refresh=False, highlight_regex=None):
|
||||
"""Display text output in a scrollable text pad.
|
||||
|
||||
This method does some preprocessing on the text lines, render them on the
|
||||
screen and scroll to the appropriate line. These are done according to regex
|
||||
highlighting requests (if any), scroll-to-next-match requests (if any),
|
||||
and screen refrexh requests (if any).
|
||||
and screen refresh requests (if any).
|
||||
|
||||
TODO(cais): Separate these unrelated request to increase clarity and
|
||||
maintainability.
|
||||
@ -734,16 +745,17 @@ class CursesUI(object):
|
||||
|
||||
if not is_refresh:
|
||||
# Perform new regex search on the current output.
|
||||
self._regex_match_lines = output.annotations[
|
||||
self._unwrapped_regex_match_lines = output.annotations[
|
||||
debugger_cli_common.REGEX_MATCH_LINES_KEY]
|
||||
else:
|
||||
# Continue scrolling down.
|
||||
self._output_pad_row += 1
|
||||
else:
|
||||
self._curr_unwrapped_output = output
|
||||
self._unwrapped_regex_match_lines = []
|
||||
|
||||
# Display output on the screen.
|
||||
self._screen_display_output(output)
|
||||
wrapped_regex_match_lines = self._screen_display_output(output)
|
||||
|
||||
# Now that the text lines are displayed on the screen scroll to the
|
||||
# appropriate line according to previous scrolling state and regex search
|
||||
@ -751,7 +763,7 @@ class CursesUI(object):
|
||||
|
||||
if highlight_regex:
|
||||
next_match_line = -1
|
||||
for match_line in self._regex_match_lines:
|
||||
for match_line in wrapped_regex_match_lines:
|
||||
if match_line >= self._output_pad_row:
|
||||
next_match_line = match_line
|
||||
break
|
||||
@ -1099,7 +1111,7 @@ class CursesUI(object):
|
||||
0: [(len(candidates_prefix), len(candidates_line), "yellow")]
|
||||
})
|
||||
|
||||
candidates_output = debugger_cli_common.wrap_rich_text_lines(
|
||||
candidates_output, _ = debugger_cli_common.wrap_rich_text_lines(
|
||||
candidates_output, self._max_x - 2)
|
||||
|
||||
# Calculate how many lines the candidate text should occupy. Limit it to
|
||||
|
@ -201,10 +201,17 @@ class CursesTest(test_util.TensorFlowTestCase):
|
||||
type=int,
|
||||
default=60,
|
||||
help="How many times to babble")
|
||||
ap.add_argument(
|
||||
"-l",
|
||||
"--line",
|
||||
dest="line",
|
||||
type=str,
|
||||
default="bar",
|
||||
help="The content of each line")
|
||||
|
||||
parsed = ap.parse_args(args)
|
||||
|
||||
return debugger_cli_common.RichTextLines(["bar"] * parsed.num_times)
|
||||
return debugger_cli_common.RichTextLines([parsed.line] * parsed.num_times)
|
||||
|
||||
def _print_ones(self, args, screen_info=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
@ -726,7 +733,7 @@ class CursesTest(test_util.TensorFlowTestCase):
|
||||
"babble", self._babble, "babble some", prefix_aliases=["b"])
|
||||
ui.run_ui()
|
||||
|
||||
# The unwrapped (original) output should never have any highglighting.
|
||||
# The unwrapped (original) output should never have any highlighting.
|
||||
self.assertEqual(3, len(ui.unwrapped_outputs))
|
||||
for i in range(3):
|
||||
self.assertEqual(["bar"] * 3, ui.unwrapped_outputs[i].lines)
|
||||
@ -794,6 +801,32 @@ class CursesTest(test_util.TensorFlowTestCase):
|
||||
|
||||
self.assertEqual([0, 0, 1, 2], ui.output_pad_rows)
|
||||
|
||||
def testRegexSearchUnderLineWrapping(self):
|
||||
ui = MockCursesUI(
|
||||
40,
|
||||
5, # Use a narrow window to trigger line wrapping
|
||||
command_sequence=[
|
||||
string_to_codes("babble -n 3 -l foo-bar-baz-qux\n"),
|
||||
string_to_codes("/foo\n"), # Regex search and highlight.
|
||||
string_to_codes("/\n"), # Continue scrolling down: 1st time.
|
||||
string_to_codes("/\n"), # Continue scrolling down: 2nd time.
|
||||
string_to_codes("/\n"), # Continue scrolling down: 3rd time.
|
||||
string_to_codes("/\n"), # Continue scrolling down: 4th time.
|
||||
self._EXIT
|
||||
])
|
||||
|
||||
ui.register_command_handler(
|
||||
"babble", self._babble, "babble some")
|
||||
ui.run_ui()
|
||||
|
||||
self.assertEqual(4, len(ui.wrapped_outputs))
|
||||
for wrapped_output in ui.wrapped_outputs:
|
||||
self.assertEqual(["foo-", "bar-", "baz-", "qux"] * 3,
|
||||
wrapped_output.lines[0 : 12])
|
||||
|
||||
# The scroll location should reflect the line wrapping.
|
||||
self.assertEqual([0, 0, 4, 8], ui.output_pad_rows)
|
||||
|
||||
def testRegexSearchNoMatchContinuation(self):
|
||||
"""Test continuing scrolling when there is no regex match."""
|
||||
|
||||
@ -811,8 +844,8 @@ class CursesTest(test_util.TensorFlowTestCase):
|
||||
"babble", self._babble, "babble some", prefix_aliases=["b"])
|
||||
ui.run_ui()
|
||||
|
||||
# The regex search and continuation search should not have produced any
|
||||
# output.
|
||||
# The regex search and continuation search in the 3rd command should not
|
||||
# have produced any output.
|
||||
self.assertEqual(1, len(ui.unwrapped_outputs))
|
||||
self.assertEqual([0], ui.output_pad_rows)
|
||||
|
||||
|
@ -223,12 +223,16 @@ def wrap_rich_text_lines(inp, cols):
|
||||
cols: Number of columns, as an int.
|
||||
|
||||
Returns:
|
||||
A new instance of RichTextLines, with line lengths limited to cols.
|
||||
|
||||
1) A new instance of RichTextLines, with line lengths limited to cols.
|
||||
2) A list of new (wrapped) line index. For example, if the original input
|
||||
consists of three lines and only the second line is wrapped, and it's
|
||||
wrapped into two lines, this return value will be: [0, 1, 3].
|
||||
Raises:
|
||||
ValueError: If inputs have invalid types.
|
||||
"""
|
||||
|
||||
new_line_indices = []
|
||||
|
||||
if not isinstance(inp, RichTextLines):
|
||||
raise ValueError("Invalid type of input screen_output")
|
||||
|
||||
@ -239,6 +243,8 @@ def wrap_rich_text_lines(inp, cols):
|
||||
|
||||
row_counter = 0 # Counter for new row index
|
||||
for i in xrange(len(inp.lines)):
|
||||
new_line_indices.append(out.num_lines())
|
||||
|
||||
line = inp.lines[i]
|
||||
|
||||
if i in inp.annotations:
|
||||
@ -298,7 +304,7 @@ def wrap_rich_text_lines(inp, cols):
|
||||
if not isinstance(key, int):
|
||||
out.annotations[key] = inp.annotations[key]
|
||||
|
||||
return out
|
||||
return out, new_line_indices
|
||||
|
||||
|
||||
class CommandHandlerRegistry(object):
|
||||
|
@ -453,16 +453,18 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
|
||||
|
||||
def testNoActualWrapping(self):
|
||||
# Large column limit should lead to no actual wrapping.
|
||||
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output,
|
||||
100)
|
||||
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
|
||||
self._orig_screen_output, 100)
|
||||
|
||||
self.assertEqual(self._orig_screen_output.lines, out.lines)
|
||||
self.assertEqual(self._orig_screen_output.font_attr_segs,
|
||||
out.font_attr_segs)
|
||||
self.assertEqual(self._orig_screen_output.annotations, out.annotations)
|
||||
self.assertEqual(new_line_indices, [0, 1, 2])
|
||||
|
||||
def testWrappingWithAttrCutoff(self):
|
||||
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output, 11)
|
||||
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
|
||||
self._orig_screen_output, 11)
|
||||
|
||||
# Add non-row-index field to out.
|
||||
out.annotations["metadata"] = "foo"
|
||||
@ -493,6 +495,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
|
||||
# Chec that the non-row-index field is present in output.
|
||||
self.assertEqual("foo", out.annotations["metadata"])
|
||||
|
||||
self.assertEqual(new_line_indices, [0, 1, 3])
|
||||
|
||||
def testWrappingWithMultipleAttrCutoff(self):
|
||||
self._orig_screen_output = debugger_cli_common.RichTextLines(
|
||||
["Folk song:", "Roses are red", "Violets are blue"],
|
||||
@ -501,7 +505,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
|
||||
annotations={1: "longer wavelength",
|
||||
2: "shorter wavelength"})
|
||||
|
||||
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output, 5)
|
||||
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
|
||||
self._orig_screen_output, 5)
|
||||
|
||||
# Check wrapped text.
|
||||
self.assertEqual(9, len(out.lines))
|
||||
@ -537,6 +542,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
|
||||
self.assertFalse(7 in out.annotations)
|
||||
self.assertFalse(8 in out.annotations)
|
||||
|
||||
self.assertEqual(new_line_indices, [0, 2, 5])
|
||||
|
||||
def testWrappingInvalidArguments(self):
|
||||
with self.assertRaisesRegexp(ValueError,
|
||||
"Invalid type of input screen_output"):
|
||||
@ -546,6 +553,13 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
|
||||
debugger_cli_common.wrap_rich_text_lines(
|
||||
debugger_cli_common.RichTextLines(["foo", "bar"]), "12")
|
||||
|
||||
def testWrappingEmptyInput(self):
|
||||
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
|
||||
debugger_cli_common.RichTextLines([]), 10)
|
||||
|
||||
self.assertEqual([], out.lines)
|
||||
self.assertEqual([], new_line_indices)
|
||||
|
||||
|
||||
class SliceRichTextLinesText(test_util.TensorFlowTestCase):
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user