1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7"""Mix-in classes 8 9These classes were created to be able to provide Spyder's regular text and 10console widget features to an independant widget based on QTextEdit for the 11IPython console plugin. 12""" 13 14# Standard library imports 15from xml.sax.saxutils import escape 16import os 17import os.path as osp 18import re 19import sre_constants 20import textwrap 21 22# Third party imports 23from qtpy.QtCore import QPoint, Qt 24from qtpy.QtGui import QCursor, QTextCursor, QTextDocument 25from qtpy.QtWidgets import QApplication, QToolTip 26from qtpy import QT_VERSION 27 28# Local imports 29from spyder.config.base import _ 30from spyder.py3compat import is_text_string, to_text_string 31from spyder.utils import encoding, sourcecode, programs 32from spyder.utils.dochelpers import (getargspecfromtext, getobj, 33 getsignaturefromtext) 34from spyder.utils.misc import get_error_match 35from spyder.widgets.arraybuilder import NumpyArrayDialog 36 37QT55_VERSION = programs.check_version(QT_VERSION, "5.5", ">=") 38 39if QT55_VERSION: 40 from qtpy.QtCore import QRegularExpression 41else: 42 from qtpy.QtCore import QRegExp 43 44 45class BaseEditMixin(object): 46 47 def __init__(self): 48 self.eol_chars = None 49 self.calltip_size = 600 50 51 #------Line number area 52 def get_linenumberarea_width(self): 53 """Return line number area width""" 54 # Implemented in CodeEditor, but needed for calltip/completion widgets 55 return 0 56 57 58 #------Calltips 59 def _format_signature(self, text): 60 formatted_lines = [] 61 name = text.split('(')[0] 62 rows = textwrap.wrap(text, width=50, 63 subsequent_indent=' '*(len(name)+1)) 64 for r in rows: 65 r = escape(r) # Escape most common html chars 66 r = r.replace(' ', ' ') 67 for char in ['=', ',', '(', ')', '*', '**']: 68 r = r.replace(char, 69 '<span style=\'color: red; font-weight: bold\'>' + \ 70 char + '</span>') 71 formatted_lines.append(r) 72 signature = '<br>'.join(formatted_lines) 73 return signature, rows 74 75 def show_calltip(self, title, text, signature=False, color='#2D62FF', 76 at_line=None, at_position=None): 77 """Show calltip""" 78 if text is None or len(text) == 0: 79 return 80 81 # Saving cursor position: 82 if at_position is None: 83 at_position = self.get_position('cursor') 84 self.calltip_position = at_position 85 86 # Preparing text: 87 if signature: 88 text, wrapped_textlines = self._format_signature(text) 89 else: 90 if isinstance(text, list): 91 text = "\n ".join(text) 92 text = text.replace('\n', '<br>') 93 if len(text) > self.calltip_size: 94 text = text[:self.calltip_size] + " ..." 95 96 # Formatting text 97 font = self.font() 98 size = font.pointSize() 99 family = font.family() 100 format1 = '<div style=\'font-family: "%s"; font-size: %spt; color: %s\'>'\ 101 % (family, size, color) 102 format2 = '<div style=\'font-family: "%s"; font-size: %spt\'>'\ 103 % (family, size-1 if size > 9 else size) 104 tiptext = format1 + ('<b>%s</b></div>' % title) + '<hr>' + \ 105 format2 + text + "</div>" 106 107 # Showing tooltip at cursor position: 108 cx, cy = self.get_coordinates('cursor') 109 if at_line is not None: 110 cx = 5 111 cursor = QTextCursor(self.document().findBlockByNumber(at_line-1)) 112 cy = self.cursorRect(cursor).top() 113 point = self.mapToGlobal(QPoint(cx, cy)) 114 point.setX(point.x()+self.get_linenumberarea_width()) 115 point.setY(point.y()+font.pointSize()+5) 116 if signature: 117 self.calltip_widget.show_tip(point, tiptext, wrapped_textlines) 118 else: 119 QToolTip.showText(point, tiptext) 120 121 122 #------EOL characters 123 def set_eol_chars(self, text): 124 """Set widget end-of-line (EOL) characters from text (analyzes text)""" 125 if not is_text_string(text): # testing for QString (PyQt API#1) 126 text = to_text_string(text) 127 eol_chars = sourcecode.get_eol_chars(text) 128 is_document_modified = eol_chars is not None and self.eol_chars is not None 129 self.eol_chars = eol_chars 130 if is_document_modified: 131 self.document().setModified(True) 132 if self.sig_eol_chars_changed is not None: 133 self.sig_eol_chars_changed.emit(eol_chars) 134 135 def get_line_separator(self): 136 """Return line separator based on current EOL mode""" 137 if self.eol_chars is not None: 138 return self.eol_chars 139 else: 140 return os.linesep 141 142 def get_text_with_eol(self): 143 """Same as 'toPlainText', replace '\n' 144 by correct end-of-line characters""" 145 utext = to_text_string(self.toPlainText()) 146 lines = utext.splitlines() 147 linesep = self.get_line_separator() 148 txt = linesep.join(lines) 149 if utext.endswith('\n'): 150 txt += linesep 151 return txt 152 153 154 #------Positions, coordinates (cursor, EOF, ...) 155 def get_position(self, subject): 156 """Get offset in character for the given subject from the start of 157 text edit area""" 158 cursor = self.textCursor() 159 if subject == 'cursor': 160 pass 161 elif subject == 'sol': 162 cursor.movePosition(QTextCursor.StartOfBlock) 163 elif subject == 'eol': 164 cursor.movePosition(QTextCursor.EndOfBlock) 165 elif subject == 'eof': 166 cursor.movePosition(QTextCursor.End) 167 elif subject == 'sof': 168 cursor.movePosition(QTextCursor.Start) 169 else: 170 # Assuming that input argument was already a position 171 return subject 172 return cursor.position() 173 174 def get_coordinates(self, position): 175 position = self.get_position(position) 176 cursor = self.textCursor() 177 cursor.setPosition(position) 178 point = self.cursorRect(cursor).center() 179 return point.x(), point.y() 180 181 def get_cursor_line_column(self): 182 """Return cursor (line, column) numbers""" 183 cursor = self.textCursor() 184 return cursor.blockNumber(), cursor.columnNumber() 185 186 def get_cursor_line_number(self): 187 """Return cursor line number""" 188 return self.textCursor().blockNumber()+1 189 190 def set_cursor_position(self, position): 191 """Set cursor position""" 192 position = self.get_position(position) 193 cursor = self.textCursor() 194 cursor.setPosition(position) 195 self.setTextCursor(cursor) 196 self.ensureCursorVisible() 197 198 def move_cursor(self, chars=0): 199 """Move cursor to left or right (unit: characters)""" 200 direction = QTextCursor.Right if chars > 0 else QTextCursor.Left 201 for _i in range(abs(chars)): 202 self.moveCursor(direction, QTextCursor.MoveAnchor) 203 204 def is_cursor_on_first_line(self): 205 """Return True if cursor is on the first line""" 206 cursor = self.textCursor() 207 cursor.movePosition(QTextCursor.StartOfBlock) 208 return cursor.atStart() 209 210 def is_cursor_on_last_line(self): 211 """Return True if cursor is on the last line""" 212 cursor = self.textCursor() 213 cursor.movePosition(QTextCursor.EndOfBlock) 214 return cursor.atEnd() 215 216 def is_cursor_at_end(self): 217 """Return True if cursor is at the end of the text""" 218 return self.textCursor().atEnd() 219 220 def is_cursor_before(self, position, char_offset=0): 221 """Return True if cursor is before *position*""" 222 position = self.get_position(position) + char_offset 223 cursor = self.textCursor() 224 cursor.movePosition(QTextCursor.End) 225 if position < cursor.position(): 226 cursor.setPosition(position) 227 return self.textCursor() < cursor 228 229 def __move_cursor_anchor(self, what, direction, move_mode): 230 assert what in ('character', 'word', 'line') 231 if what == 'character': 232 if direction == 'left': 233 self.moveCursor(QTextCursor.PreviousCharacter, move_mode) 234 elif direction == 'right': 235 self.moveCursor(QTextCursor.NextCharacter, move_mode) 236 elif what == 'word': 237 if direction == 'left': 238 self.moveCursor(QTextCursor.PreviousWord, move_mode) 239 elif direction == 'right': 240 self.moveCursor(QTextCursor.NextWord, move_mode) 241 elif what == 'line': 242 if direction == 'down': 243 self.moveCursor(QTextCursor.NextBlock, move_mode) 244 elif direction == 'up': 245 self.moveCursor(QTextCursor.PreviousBlock, move_mode) 246 247 def move_cursor_to_next(self, what='word', direction='left'): 248 """ 249 Move cursor to next *what* ('word' or 'character') 250 toward *direction* ('left' or 'right') 251 """ 252 self.__move_cursor_anchor(what, direction, QTextCursor.MoveAnchor) 253 254 255 #------Selection 256 def clear_selection(self): 257 """Clear current selection""" 258 cursor = self.textCursor() 259 cursor.clearSelection() 260 self.setTextCursor(cursor) 261 262 def extend_selection_to_next(self, what='word', direction='left'): 263 """ 264 Extend selection to next *what* ('word' or 'character') 265 toward *direction* ('left' or 'right') 266 """ 267 self.__move_cursor_anchor(what, direction, QTextCursor.KeepAnchor) 268 269 270 #------Text: get, set, ... 271 def __select_text(self, position_from, position_to): 272 position_from = self.get_position(position_from) 273 position_to = self.get_position(position_to) 274 cursor = self.textCursor() 275 cursor.setPosition(position_from) 276 cursor.setPosition(position_to, QTextCursor.KeepAnchor) 277 return cursor 278 279 def get_text_line(self, line_nb): 280 """Return text line at line number *line_nb*""" 281 # Taking into account the case when a file ends in an empty line, 282 # since splitlines doesn't return that line as the last element 283 # TODO: Make this function more efficient 284 try: 285 return to_text_string(self.toPlainText()).splitlines()[line_nb] 286 except IndexError: 287 return self.get_line_separator() 288 289 def get_text(self, position_from, position_to): 290 """ 291 Return text between *position_from* and *position_to* 292 Positions may be positions or 'sol', 'eol', 'sof', 'eof' or 'cursor' 293 """ 294 cursor = self.__select_text(position_from, position_to) 295 text = to_text_string(cursor.selectedText()) 296 all_text = position_from == 'sof' and position_to == 'eof' 297 if text and not all_text: 298 while text.endswith("\n"): 299 text = text[:-1] 300 while text.endswith(u"\u2029"): 301 text = text[:-1] 302 return text 303 304 def get_character(self, position, offset=0): 305 """Return character at *position* with the given offset.""" 306 position = self.get_position(position) + offset 307 cursor = self.textCursor() 308 cursor.movePosition(QTextCursor.End) 309 if position < cursor.position(): 310 cursor.setPosition(position) 311 cursor.movePosition(QTextCursor.Right, 312 QTextCursor.KeepAnchor) 313 return to_text_string(cursor.selectedText()) 314 else: 315 return '' 316 317 def insert_text(self, text): 318 """Insert text at cursor position""" 319 if not self.isReadOnly(): 320 self.textCursor().insertText(text) 321 322 def replace_text(self, position_from, position_to, text): 323 cursor = self.__select_text(position_from, position_to) 324 cursor.removeSelectedText() 325 cursor.insertText(text) 326 327 def remove_text(self, position_from, position_to): 328 cursor = self.__select_text(position_from, position_to) 329 cursor.removeSelectedText() 330 331 def get_current_word(self): 332 """Return current word, i.e. word at cursor position""" 333 cursor = self.textCursor() 334 335 if cursor.hasSelection(): 336 # Removes the selection and moves the cursor to the left side 337 # of the selection: this is required to be able to properly 338 # select the whole word under cursor (otherwise, the same word is 339 # not selected when the cursor is at the right side of it): 340 cursor.setPosition(min([cursor.selectionStart(), 341 cursor.selectionEnd()])) 342 else: 343 # Checks if the first character to the right is a white space 344 # and if not, moves the cursor one word to the left (otherwise, 345 # if the character to the left do not match the "word regexp" 346 # (see below), the word to the left of the cursor won't be 347 # selected), but only if the first character to the left is not a 348 # white space too. 349 def is_space(move): 350 curs = self.textCursor() 351 curs.movePosition(move, QTextCursor.KeepAnchor) 352 return not to_text_string(curs.selectedText()).strip() 353 if is_space(QTextCursor.NextCharacter): 354 if is_space(QTextCursor.PreviousCharacter): 355 return 356 cursor.movePosition(QTextCursor.WordLeft) 357 358 cursor.select(QTextCursor.WordUnderCursor) 359 text = to_text_string(cursor.selectedText()) 360 # find a valid python variable name 361 match = re.findall(r'([^\d\W]\w*)', text, re.UNICODE) 362 if match: 363 return match[0] 364 365 def get_current_line(self): 366 """Return current line's text""" 367 cursor = self.textCursor() 368 cursor.select(QTextCursor.BlockUnderCursor) 369 return to_text_string(cursor.selectedText()) 370 371 def get_current_line_to_cursor(self): 372 """Return text from prompt to cursor""" 373 return self.get_text(self.current_prompt_pos, 'cursor') 374 375 def get_line_number_at(self, coordinates): 376 """Return line number at *coordinates* (QPoint)""" 377 cursor = self.cursorForPosition(coordinates) 378 return cursor.blockNumber()-1 379 380 def get_line_at(self, coordinates): 381 """Return line at *coordinates* (QPoint)""" 382 cursor = self.cursorForPosition(coordinates) 383 cursor.select(QTextCursor.BlockUnderCursor) 384 return to_text_string(cursor.selectedText()).replace(u'\u2029', '') 385 386 def get_word_at(self, coordinates): 387 """Return word at *coordinates* (QPoint)""" 388 cursor = self.cursorForPosition(coordinates) 389 cursor.select(QTextCursor.WordUnderCursor) 390 return to_text_string(cursor.selectedText()) 391 392 def get_block_indentation(self, block_nb): 393 """Return line indentation (character number)""" 394 text = to_text_string(self.document().findBlockByNumber(block_nb).text()) 395 text = text.replace("\t", " "*self.tab_stop_width_spaces) 396 return len(text)-len(text.lstrip()) 397 398 def get_selection_bounds(self): 399 """Return selection bounds (block numbers)""" 400 cursor = self.textCursor() 401 start, end = cursor.selectionStart(), cursor.selectionEnd() 402 block_start = self.document().findBlock(start) 403 block_end = self.document().findBlock(end) 404 return sorted([block_start.blockNumber(), block_end.blockNumber()]) 405 406 407 #------Text selection 408 def has_selected_text(self): 409 """Returns True if some text is selected""" 410 return bool(to_text_string(self.textCursor().selectedText())) 411 412 def get_selected_text(self): 413 """ 414 Return text selected by current text cursor, converted in unicode 415 416 Replace the unicode line separator character \u2029 by 417 the line separator characters returned by get_line_separator 418 """ 419 return to_text_string(self.textCursor().selectedText()).replace(u"\u2029", 420 self.get_line_separator()) 421 422 def remove_selected_text(self): 423 """Delete selected text""" 424 self.textCursor().removeSelectedText() 425 426 def replace(self, text, pattern=None): 427 """Replace selected text by *text* 428 If *pattern* is not None, replacing selected text using regular 429 expression text substitution""" 430 cursor = self.textCursor() 431 cursor.beginEditBlock() 432 if pattern is not None: 433 seltxt = to_text_string(cursor.selectedText()) 434 cursor.removeSelectedText() 435 if pattern is not None: 436 text = re.sub(to_text_string(pattern), 437 to_text_string(text), to_text_string(seltxt)) 438 cursor.insertText(text) 439 cursor.endEditBlock() 440 441 442 #------Find/replace 443 def find_multiline_pattern(self, regexp, cursor, findflag): 444 """Reimplement QTextDocument's find method 445 446 Add support for *multiline* regular expressions""" 447 pattern = to_text_string(regexp.pattern()) 448 text = to_text_string(self.toPlainText()) 449 try: 450 regobj = re.compile(pattern) 451 except sre_constants.error: 452 return 453 if findflag & QTextDocument.FindBackward: 454 # Find backward 455 offset = min([cursor.selectionEnd(), cursor.selectionStart()]) 456 text = text[:offset] 457 matches = [_m for _m in regobj.finditer(text, 0, offset)] 458 if matches: 459 match = matches[-1] 460 else: 461 return 462 else: 463 # Find forward 464 offset = max([cursor.selectionEnd(), cursor.selectionStart()]) 465 match = regobj.search(text, offset) 466 if match: 467 pos1, pos2 = match.span() 468 fcursor = self.textCursor() 469 fcursor.setPosition(pos1) 470 fcursor.setPosition(pos2, QTextCursor.KeepAnchor) 471 return fcursor 472 473 def find_text(self, text, changed=True, forward=True, case=False, 474 words=False, regexp=False): 475 """Find text""" 476 cursor = self.textCursor() 477 findflag = QTextDocument.FindFlag() 478 if not forward: 479 findflag = findflag | QTextDocument.FindBackward 480 if case: 481 findflag = findflag | QTextDocument.FindCaseSensitively 482 moves = [QTextCursor.NoMove] 483 if forward: 484 moves += [QTextCursor.NextWord, QTextCursor.Start] 485 if changed: 486 if to_text_string(cursor.selectedText()): 487 new_position = min([cursor.selectionStart(), 488 cursor.selectionEnd()]) 489 cursor.setPosition(new_position) 490 else: 491 cursor.movePosition(QTextCursor.PreviousWord) 492 else: 493 moves += [QTextCursor.End] 494 if not regexp: 495 text = re.escape(to_text_string(text)) 496 if QT55_VERSION: 497 pattern = QRegularExpression(r"\b{}\b".format(text) if words else 498 text) 499 if case: 500 pattern.setPatternOptions( 501 QRegularExpression.CaseInsensitiveOption) 502 else: 503 pattern = QRegExp(r"\b{}\b".format(text) 504 if words else text, Qt.CaseSensitive if case else 505 Qt.CaseInsensitive, QRegExp.RegExp2) 506 507 for move in moves: 508 cursor.movePosition(move) 509 if regexp and '\\n' in text: 510 # Multiline regular expression 511 found_cursor = self.find_multiline_pattern(pattern, cursor, 512 findflag) 513 else: 514 # Single line find: using the QTextDocument's find function, 515 # probably much more efficient than ours 516 found_cursor = self.document().find(pattern, cursor, findflag) 517 if found_cursor is not None and not found_cursor.isNull(): 518 self.setTextCursor(found_cursor) 519 return True 520 return False 521 522 def is_editor(self): 523 """Needs to be overloaded in the codeeditor where it will be True""" 524 return False 525 526 def get_number_matches(self, pattern, source_text='', case=False): 527 """Get the number of matches for the searched text.""" 528 pattern = to_text_string(pattern) 529 if not pattern: 530 return 0 531 if not source_text: 532 source_text = to_text_string(self.toPlainText()) 533 try: 534 if case: 535 regobj = re.compile(pattern) 536 else: 537 regobj = re.compile(pattern, re.IGNORECASE) 538 except sre_constants.error: 539 return 540 541 number_matches = 0 542 for match in regobj.finditer(source_text): 543 number_matches += 1 544 545 return number_matches 546 547 def get_match_number(self, pattern, case=False): 548 """Get number of the match for the searched text.""" 549 position = self.textCursor().position() 550 source_text = self.get_text(position_from='sof', position_to=position) 551 match_number = self.get_number_matches(pattern, 552 source_text=source_text, 553 case=case) 554 return match_number 555 556 # --- Numpy matrix/array helper / See 'spyder/widgets/arraybuilder.py' 557 def enter_array_inline(self): 558 """ """ 559 self._enter_array(True) 560 561 def enter_array_table(self): 562 """ """ 563 self._enter_array(False) 564 565 def _enter_array(self, inline): 566 """ """ 567 offset = self.get_position('cursor') - self.get_position('sol') 568 rect = self.cursorRect() 569 dlg = NumpyArrayDialog(self, inline, offset) 570 571 # TODO: adapt to font size 572 x = rect.left() 573 x = x + self.get_linenumberarea_width() - 14 574 y = rect.top() + (rect.bottom() - rect.top())/2 575 y = y - dlg.height()/2 - 3 576 577 pos = QPoint(x, y) 578 dlg.move(self.mapToGlobal(pos)) 579 580 # called from editor 581 if self.is_editor(): 582 python_like_check = self.is_python_like() 583 suffix = '\n' 584 # called from a console 585 else: 586 python_like_check = True 587 suffix = '' 588 589 if python_like_check and dlg.exec_(): 590 text = dlg.text() + suffix 591 if text != '': 592 cursor = self.textCursor() 593 cursor.beginEditBlock() 594 cursor.insertText(text) 595 cursor.endEditBlock() 596 597 598class TracebackLinksMixin(object): 599 """ """ 600 QT_CLASS = None 601 go_to_error = None 602 603 def __init__(self): 604 self.__cursor_changed = False 605 self.setMouseTracking(True) 606 607 #------Mouse events 608 def mouseReleaseEvent(self, event): 609 """Go to error""" 610 self.QT_CLASS.mouseReleaseEvent(self, event) 611 text = self.get_line_at(event.pos()) 612 if get_error_match(text) and not self.has_selected_text(): 613 if self.go_to_error is not None: 614 self.go_to_error.emit(text) 615 616 def mouseMoveEvent(self, event): 617 """Show Pointing Hand Cursor on error messages""" 618 text = self.get_line_at(event.pos()) 619 if get_error_match(text): 620 if not self.__cursor_changed: 621 QApplication.setOverrideCursor(QCursor(Qt.PointingHandCursor)) 622 self.__cursor_changed = True 623 event.accept() 624 return 625 if self.__cursor_changed: 626 QApplication.restoreOverrideCursor() 627 self.__cursor_changed = False 628 self.QT_CLASS.mouseMoveEvent(self, event) 629 630 def leaveEvent(self, event): 631 """If cursor has not been restored yet, do it now""" 632 if self.__cursor_changed: 633 QApplication.restoreOverrideCursor() 634 self.__cursor_changed = False 635 self.QT_CLASS.leaveEvent(self, event) 636 637 638class GetHelpMixin(object): 639 def __init__(self): 640 self.help = None 641 self.help_enabled = False 642 643 def set_help(self, help_plugin): 644 """Set Help DockWidget reference""" 645 self.help = help_plugin 646 647 def set_help_enabled(self, state): 648 self.help_enabled = state 649 650 def inspect_current_object(self): 651 text = '' 652 text1 = self.get_text('sol', 'cursor') 653 tl1 = re.findall(r'([a-zA-Z_]+[0-9a-zA-Z_\.]*)', text1) 654 if tl1 and text1.endswith(tl1[-1]): 655 text += tl1[-1] 656 text2 = self.get_text('cursor', 'eol') 657 tl2 = re.findall(r'([0-9a-zA-Z_\.]+[0-9a-zA-Z_\.]*)', text2) 658 if tl2 and text2.startswith(tl2[0]): 659 text += tl2[0] 660 if text: 661 self.show_object_info(text, force=True) 662 663 def show_object_info(self, text, call=False, force=False): 664 """Show signature calltip and/or docstring in the Help plugin""" 665 text = to_text_string(text) 666 667 # Show docstring 668 help_enabled = self.help_enabled or force 669 if force and self.help is not None: 670 self.help.dockwidget.setVisible(True) 671 self.help.dockwidget.raise_() 672 if help_enabled and (self.help is not None) and \ 673 (self.help.dockwidget.isVisible()): 674 # Help widget exists and is visible 675 if hasattr(self, 'get_doc'): 676 self.help.set_shell(self) 677 else: 678 self.help.set_shell(self.parent()) 679 self.help.set_object_text(text, ignore_unknown=False) 680 self.setFocus() # if help was not at top level, raising it to 681 # top will automatically give it focus because of 682 # the visibility_changed signal, so we must give 683 # focus back to shell 684 685 # Show calltip 686 if call and self.calltips: 687 # Display argument list if this is a function call 688 iscallable = self.iscallable(text) 689 if iscallable is not None: 690 if iscallable: 691 arglist = self.get_arglist(text) 692 name = text.split('.')[-1] 693 argspec = signature = '' 694 if isinstance(arglist, bool): 695 arglist = [] 696 if arglist: 697 argspec = '(' + ''.join(arglist) + ')' 698 else: 699 doc = self.get__doc__(text) 700 if doc is not None: 701 # This covers cases like np.abs, whose docstring is 702 # the same as np.absolute and because of that a 703 # proper signature can't be obtained correctly 704 argspec = getargspecfromtext(doc) 705 if not argspec: 706 signature = getsignaturefromtext(doc, name) 707 if argspec or signature: 708 if argspec: 709 tiptext = name + argspec 710 else: 711 tiptext = signature 712 self.show_calltip(_("Arguments"), tiptext, 713 signature=True, color='#2D62FF') 714 715 def get_last_obj(self, last=False): 716 """ 717 Return the last valid object on the current line 718 """ 719 return getobj(self.get_current_line_to_cursor(), last=last) 720 721 722class SaveHistoryMixin(object): 723 724 INITHISTORY = None 725 SEPARATOR = None 726 HISTORY_FILENAMES = [] 727 728 append_to_history = None 729 730 def __init__(self, history_filename=''): 731 self.history_filename = history_filename 732 self.create_history_filename() 733 734 def create_history_filename(self): 735 """Create history_filename with INITHISTORY if it doesn't exist.""" 736 if self.history_filename and not osp.isfile(self.history_filename): 737 encoding.writelines(self.INITHISTORY, self.history_filename) 738 739 def add_to_history(self, command): 740 """Add command to history""" 741 command = to_text_string(command) 742 if command in ['', '\n'] or command.startswith('Traceback'): 743 return 744 if command.endswith('\n'): 745 command = command[:-1] 746 self.histidx = None 747 if len(self.history) > 0 and self.history[-1] == command: 748 return 749 self.history.append(command) 750 text = os.linesep + command 751 752 # When the first entry will be written in history file, 753 # the separator will be append first: 754 if self.history_filename not in self.HISTORY_FILENAMES: 755 self.HISTORY_FILENAMES.append(self.history_filename) 756 text = self.SEPARATOR + text 757 # Needed to prevent errors when writing history to disk 758 # See issue 6431 759 try: 760 encoding.write(text, self.history_filename, mode='ab') 761 except (IOError, OSError): 762 pass 763 if self.append_to_history is not None: 764 self.append_to_history.emit(self.history_filename, text) 765 766 767class BrowseHistoryMixin(object): 768 769 def __init__(self): 770 self.history = [] 771 self.histidx = None 772 self.hist_wholeline = False 773 774 def clear_line(self): 775 """Clear current line (without clearing console prompt)""" 776 self.remove_text(self.current_prompt_pos, 'eof') 777 778 def browse_history(self, backward): 779 """Browse history""" 780 if self.is_cursor_before('eol') and self.hist_wholeline: 781 self.hist_wholeline = False 782 tocursor = self.get_current_line_to_cursor() 783 text, self.histidx = self.find_in_history(tocursor, self.histidx, 784 backward) 785 if text is not None: 786 if self.hist_wholeline: 787 self.clear_line() 788 self.insert_text(text) 789 else: 790 cursor_position = self.get_position('cursor') 791 # Removing text from cursor to the end of the line 792 self.remove_text('cursor', 'eol') 793 # Inserting history text 794 self.insert_text(text) 795 self.set_cursor_position(cursor_position) 796 797 def find_in_history(self, tocursor, start_idx, backward): 798 """Find text 'tocursor' in history, from index 'start_idx'""" 799 if start_idx is None: 800 start_idx = len(self.history) 801 # Finding text in history 802 step = -1 if backward else 1 803 idx = start_idx 804 if len(tocursor) == 0 or self.hist_wholeline: 805 idx += step 806 if idx >= len(self.history) or len(self.history) == 0: 807 return "", len(self.history) 808 elif idx < 0: 809 idx = 0 810 self.hist_wholeline = True 811 return self.history[idx], idx 812 else: 813 for index in range(len(self.history)): 814 idx = (start_idx+step*(index+1)) % len(self.history) 815 entry = self.history[idx] 816 if entry.startswith(tocursor): 817 return entry[len(tocursor):], idx 818 else: 819 return None, start_idx 820 821 def reset_search_pos(self): 822 """Reset the position from which to search the history""" 823 self.histidx = None 824