From 9249576d27f76e1a740b2f907c16f50e4caa308c 2012-04-14 02:16:24 From: Puneeth Chaganti Date: 2012-04-14 02:16:24 Subject: [PATCH] BUG: qtconsole -- non-standard handling of \a and \b. [Fixes #1561] --- diff --git a/IPython/frontend/qt/console/ansi_code_processor.py b/IPython/frontend/qt/console/ansi_code_processor.py index 76019b3..cdcb206 100644 --- a/IPython/frontend/qt/console/ansi_code_processor.py +++ b/IPython/frontend/qt/console/ansi_code_processor.py @@ -29,16 +29,22 @@ ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count']) # An action for the carriage return character CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) +# An action for the \n character +NewLineAction = namedtuple('NewLineAction', ['action']) + # An action for the beep character BeepAction = namedtuple('BeepAction', ['action']) +# An action for backspace +BackSpaceAction = namedtuple('BackSpaceAction', ['action']) + # Regular expressions. CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS OSC_SUBPATTERN = '\](.*?)[\x07\x1b]' ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ (CSI_SUBPATTERN, OSC_SUBPATTERN)) -ANSI_OR_SPECIAL_PATTERN = re.compile('(\b|\r(?!\n))|(?:%s)' % ANSI_PATTERN) +ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN) SPECIAL_PATTERN = re.compile('([\f])') #----------------------------------------------------------------------------- @@ -83,24 +89,41 @@ class AnsiCodeProcessor(object): self.actions = [] start = 0 + # strings ending with \r are assumed to be ending in \r\n since + # \n is appended to output strings automatically. Accounting + # for that, here. + last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None + string = string[:-1] if last_char is not None else string + for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): raw = string[start:match.start()] substring = SPECIAL_PATTERN.sub(self._replace_special, raw) if substring or self.actions: yield substring + self.actions = [] start = match.end() - self.actions = [] groups = filter(lambda x: x is not None, match.groups()) - if groups[0] == '\r': - self.actions.append(CarriageReturnAction('carriage-return')) - yield '' - elif groups[0] == '\b': + g0 = groups[0] + if g0 == '\a': self.actions.append(BeepAction('beep')) - yield '' + yield None + self.actions = [] + elif g0 == '\r': + self.actions.append(CarriageReturnAction('carriage-return')) + yield None + self.actions = [] + elif g0 == '\b': + self.actions.append(BackSpaceAction('backspace')) + yield None + self.actions = [] + elif g0 == '\n' or g0 == '\r\n': + self.actions.append(NewLineAction('newline')) + yield g0 + self.actions = [] else: params = [ param for param in groups[1].split(';') if param ] - if groups[0].startswith('['): + if g0.startswith('['): # Case 1: CSI code. try: params = map(int, params) @@ -110,7 +133,7 @@ class AnsiCodeProcessor(object): else: self.set_csi_code(groups[2], params) - elif groups[0].startswith(']'): + elif g0.startswith(']'): # Case 2: OSC code. self.set_osc_code(params) @@ -119,6 +142,10 @@ class AnsiCodeProcessor(object): if substring or self.actions: yield substring + if last_char is not None: + self.actions.append(NewLineAction('newline')) + yield last_char + def set_csi_code(self, command, params=[]): """ Set attributes based on CSI (Control Sequence Introducer) code. diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index f4e7d3a..646ae14 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -1553,8 +1553,31 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): elif act.action == 'beep': QtGui.qApp.beep() + elif act.action == 'backspace': + if not cursor.atBlockStart(): + cursor.movePosition( + cursor.PreviousCharacter, cursor.KeepAnchor) + + elif act.action == 'newline': + cursor.movePosition(cursor.EndOfLine) + format = self._ansi_processor.get_format() - cursor.insertText(substring, format) + + selection = cursor.selectedText() + if len(selection) == 0: + cursor.insertText(substring, format) + elif substring is not None: + # BS and CR are treated as a change in print + # position, rather than a backwards character + # deletion for output equivalence with (I)Python + # terminal. + if len(substring) >= len(selection): + cursor.insertText(substring, format) + else: + old_text = selection[len(substring):] + cursor.insertText(substring + old_text, format) + cursor.movePosition(cursor.PreviousCharacter, + cursor.KeepAnchor, len(old_text)) else: cursor.insertText(text) cursor.endEditBlock() diff --git a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py index 30698b1..569c369 100644 --- a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py +++ b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py @@ -106,28 +106,65 @@ class TestAnsiCodeProcessor(unittest.TestCase): """ Are carriage return characters processed correctly? """ string = 'foo\rbar' # carriage return - self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) - self.assertEquals(len(self.processor.actions), 1) - action = self.processor.actions[0] - self.assertEquals(action.action, 'carriage-return') + splits = [] + actions = [] + for split in self.processor.split_string(string): + splits.append(split) + actions.append([action.action for action in self.processor.actions]) + self.assertEquals(splits, ['foo', None, 'bar']) + self.assertEquals(actions, [[], ['carriage-return'], []]) def test_carriage_return_newline(self): """transform CRLF to LF""" - string = 'foo\rbar\r\ncat\r\n' # carriage return and newline + string = 'foo\rbar\r\ncat\r\n\n' # carriage return and newline # only one CR action should occur, and '\r\n' should transform to '\n' - self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar\r\ncat\r\n']) - self.assertEquals(len(self.processor.actions), 1) - action = self.processor.actions[0] - self.assertEquals(action.action, 'carriage-return') + splits = [] + actions = [] + for split in self.processor.split_string(string): + splits.append(split) + actions.append([action.action for action in self.processor.actions]) + self.assertEquals(splits, ['foo', None, 'bar', '\r\n', 'cat', '\r\n', '\n']) + self.assertEquals(actions, [[], ['carriage-return'], [], ['newline'], [], ['newline'], ['newline']]) def test_beep(self): """ Are beep characters processed correctly? """ - string = 'foo\bbar' # form feed - self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) - self.assertEquals(len(self.processor.actions), 1) - action = self.processor.actions[0] - self.assertEquals(action.action, 'beep') + string = 'foo\abar' # bell + splits = [] + actions = [] + for split in self.processor.split_string(string): + splits.append(split) + actions.append([action.action for action in self.processor.actions]) + self.assertEquals(splits, ['foo', None, 'bar']) + self.assertEquals(actions, [[], ['beep'], []]) + + def test_backspace(self): + """ Are backspace characters processed correctly? + """ + string = 'foo\bbar' # backspace + splits = [] + actions = [] + for split in self.processor.split_string(string): + splits.append(split) + actions.append([action.action for action in self.processor.actions]) + self.assertEquals(splits, ['foo', None, 'bar']) + self.assertEquals(actions, [[], ['backspace'], []]) + + def test_combined(self): + """ Are CR and BS characters processed correctly in combination? + + BS is treated as a change in print position, rather than a + backwards character deletion. Therefore a BS at EOL is + effectively ignored. + """ + string = 'abc\rdef\b' # CR and backspace + splits = [] + actions = [] + for split in self.processor.split_string(string): + splits.append(split) + actions.append([action.action for action in self.processor.actions]) + self.assertEquals(splits, ['abc', None, 'def', None]) + self.assertEquals(actions, [[], ['carriage-return'], [], ['backspace']]) if __name__ == '__main__':