1""" 2Processors are little transformation blocks that transform the token list from 3a buffer before the BufferControl will render it to the screen. 4 5They can insert tokens before or after, or highlight fragments by replacing the 6token types. 7""" 8from __future__ import unicode_literals 9from abc import ABCMeta, abstractmethod 10from six import with_metaclass 11from six.moves import range 12 13from prompt_toolkit.cache import SimpleCache 14from prompt_toolkit.document import Document 15from prompt_toolkit.enums import SEARCH_BUFFER 16from prompt_toolkit.filters import to_cli_filter, ViInsertMultipleMode 17from prompt_toolkit.layout.utils import token_list_to_text 18from prompt_toolkit.reactive import Integer 19from prompt_toolkit.token import Token 20 21from .utils import token_list_len, explode_tokens 22 23import re 24 25__all__ = ( 26 'Processor', 27 'Transformation', 28 29 'HighlightSearchProcessor', 30 'HighlightSelectionProcessor', 31 'PasswordProcessor', 32 'HighlightMatchingBracketProcessor', 33 'DisplayMultipleCursors', 34 'BeforeInput', 35 'AfterInput', 36 'AppendAutoSuggestion', 37 'ConditionalProcessor', 38 'ShowLeadingWhiteSpaceProcessor', 39 'ShowTrailingWhiteSpaceProcessor', 40 'TabsProcessor', 41) 42 43 44class Processor(with_metaclass(ABCMeta, object)): 45 """ 46 Manipulate the tokens for a given line in a 47 :class:`~prompt_toolkit.layout.controls.BufferControl`. 48 """ 49 @abstractmethod 50 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 51 """ 52 Apply transformation. Returns a :class:`.Transformation` instance. 53 54 :param cli: :class:`.CommandLineInterface` instance. 55 :param lineno: The number of the line to which we apply the processor. 56 :param source_to_display: A function that returns the position in the 57 `tokens` for any position in the source string. (This takes 58 previous processors into account.) 59 :param tokens: List of tokens that we can transform. (Received from the 60 previous processor.) 61 """ 62 return Transformation(tokens) 63 64 def has_focus(self, cli): 65 """ 66 Processors can override the focus. 67 (Used for the reverse-i-search prefix in DefaultPrompt.) 68 """ 69 return False 70 71 72class Transformation(object): 73 """ 74 Transformation result, as returned by :meth:`.Processor.apply_transformation`. 75 76 Important: Always make sure that the length of `document.text` is equal to 77 the length of all the text in `tokens`! 78 79 :param tokens: The transformed tokens. To be displayed, or to pass to the 80 next processor. 81 :param source_to_display: Cursor position transformation from original string to 82 transformed string. 83 :param display_to_source: Cursor position transformed from source string to 84 original string. 85 """ 86 def __init__(self, tokens, source_to_display=None, display_to_source=None): 87 self.tokens = tokens 88 self.source_to_display = source_to_display or (lambda i: i) 89 self.display_to_source = display_to_source or (lambda i: i) 90 91 92class HighlightSearchProcessor(Processor): 93 """ 94 Processor that highlights search matches in the document. 95 Note that this doesn't support multiline search matches yet. 96 97 :param preview_search: A Filter; when active it indicates that we take 98 the search text in real time while the user is typing, instead of the 99 last active search state. 100 """ 101 def __init__(self, preview_search=False, search_buffer_name=SEARCH_BUFFER, 102 get_search_state=None): 103 self.preview_search = to_cli_filter(preview_search) 104 self.search_buffer_name = search_buffer_name 105 self.get_search_state = get_search_state or (lambda cli: cli.search_state) 106 107 def _get_search_text(self, cli): 108 """ 109 The text we are searching for. 110 """ 111 # When the search buffer has focus, take that text. 112 if self.preview_search(cli) and cli.buffers[self.search_buffer_name].text: 113 return cli.buffers[self.search_buffer_name].text 114 # Otherwise, take the text of the last active search. 115 else: 116 return self.get_search_state(cli).text 117 118 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 119 search_text = self._get_search_text(cli) 120 searchmatch_current_token = (':', ) + Token.SearchMatch.Current 121 searchmatch_token = (':', ) + Token.SearchMatch 122 123 if search_text and not cli.is_returning: 124 # For each search match, replace the Token. 125 line_text = token_list_to_text(tokens) 126 tokens = explode_tokens(tokens) 127 128 flags = re.IGNORECASE if cli.is_ignoring_case else 0 129 130 # Get cursor column. 131 if document.cursor_position_row == lineno: 132 cursor_column = source_to_display(document.cursor_position_col) 133 else: 134 cursor_column = None 135 136 for match in re.finditer(re.escape(search_text), line_text, flags=flags): 137 if cursor_column is not None: 138 on_cursor = match.start() <= cursor_column < match.end() 139 else: 140 on_cursor = False 141 142 for i in range(match.start(), match.end()): 143 old_token, text = tokens[i] 144 if on_cursor: 145 tokens[i] = (old_token + searchmatch_current_token, tokens[i][1]) 146 else: 147 tokens[i] = (old_token + searchmatch_token, tokens[i][1]) 148 149 return Transformation(tokens) 150 151 152class HighlightSelectionProcessor(Processor): 153 """ 154 Processor that highlights the selection in the document. 155 """ 156 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 157 selected_token = (':', ) + Token.SelectedText 158 159 # In case of selection, highlight all matches. 160 selection_at_line = document.selection_range_at_line(lineno) 161 162 if selection_at_line: 163 from_, to = selection_at_line 164 from_ = source_to_display(from_) 165 to = source_to_display(to) 166 167 tokens = explode_tokens(tokens) 168 169 if from_ == 0 and to == 0 and len(tokens) == 0: 170 # When this is an empty line, insert a space in order to 171 # visualiase the selection. 172 return Transformation([(Token.SelectedText, ' ')]) 173 else: 174 for i in range(from_, to + 1): 175 if i < len(tokens): 176 old_token, old_text = tokens[i] 177 tokens[i] = (old_token + selected_token, old_text) 178 179 return Transformation(tokens) 180 181 182class PasswordProcessor(Processor): 183 """ 184 Processor that turns masks the input. (For passwords.) 185 186 :param char: (string) Character to be used. "*" by default. 187 """ 188 def __init__(self, char='*'): 189 self.char = char 190 191 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 192 tokens = [(token, self.char * len(text)) for token, text in tokens] 193 return Transformation(tokens) 194 195 196class HighlightMatchingBracketProcessor(Processor): 197 """ 198 When the cursor is on or right after a bracket, it highlights the matching 199 bracket. 200 201 :param max_cursor_distance: Only highlight matching brackets when the 202 cursor is within this distance. (From inside a `Processor`, we can't 203 know which lines will be visible on the screen. But we also don't want 204 to scan the whole document for matching brackets on each key press, so 205 we limit to this value.) 206 """ 207 _closing_braces = '])}>' 208 209 def __init__(self, chars='[](){}<>', max_cursor_distance=1000): 210 self.chars = chars 211 self.max_cursor_distance = max_cursor_distance 212 213 self._positions_cache = SimpleCache(maxsize=8) 214 215 def _get_positions_to_highlight(self, document): 216 """ 217 Return a list of (row, col) tuples that need to be highlighted. 218 """ 219 # Try for the character under the cursor. 220 if document.current_char and document.current_char in self.chars: 221 pos = document.find_matching_bracket_position( 222 start_pos=document.cursor_position - self.max_cursor_distance, 223 end_pos=document.cursor_position + self.max_cursor_distance) 224 225 # Try for the character before the cursor. 226 elif (document.char_before_cursor and document.char_before_cursor in 227 self._closing_braces and document.char_before_cursor in self.chars): 228 document = Document(document.text, document.cursor_position - 1) 229 230 pos = document.find_matching_bracket_position( 231 start_pos=document.cursor_position - self.max_cursor_distance, 232 end_pos=document.cursor_position + self.max_cursor_distance) 233 else: 234 pos = None 235 236 # Return a list of (row, col) tuples that need to be highlighted. 237 if pos: 238 pos += document.cursor_position # pos is relative. 239 row, col = document.translate_index_to_position(pos) 240 return [(row, col), (document.cursor_position_row, document.cursor_position_col)] 241 else: 242 return [] 243 244 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 245 # Get the highlight positions. 246 key = (cli.render_counter, document.text, document.cursor_position) 247 positions = self._positions_cache.get( 248 key, lambda: self._get_positions_to_highlight(document)) 249 250 # Apply if positions were found at this line. 251 if positions: 252 for row, col in positions: 253 if row == lineno: 254 col = source_to_display(col) 255 tokens = explode_tokens(tokens) 256 token, text = tokens[col] 257 258 if col == document.cursor_position_col: 259 token += (':', ) + Token.MatchingBracket.Cursor 260 else: 261 token += (':', ) + Token.MatchingBracket.Other 262 263 tokens[col] = (token, text) 264 265 return Transformation(tokens) 266 267 268class DisplayMultipleCursors(Processor): 269 """ 270 When we're in Vi block insert mode, display all the cursors. 271 """ 272 _insert_multiple = ViInsertMultipleMode() 273 274 def __init__(self, buffer_name): 275 self.buffer_name = buffer_name 276 277 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 278 buff = cli.buffers[self.buffer_name] 279 280 if self._insert_multiple(cli): 281 positions = buff.multiple_cursor_positions 282 tokens = explode_tokens(tokens) 283 284 # If any cursor appears on the current line, highlight that. 285 start_pos = document.translate_row_col_to_index(lineno, 0) 286 end_pos = start_pos + len(document.lines[lineno]) 287 288 token_suffix = (':', ) + Token.MultipleCursors.Cursor 289 290 for p in positions: 291 if start_pos <= p < end_pos: 292 column = source_to_display(p - start_pos) 293 294 # Replace token. 295 token, text = tokens[column] 296 token += token_suffix 297 tokens[column] = (token, text) 298 elif p == end_pos: 299 tokens.append((token_suffix, ' ')) 300 301 return Transformation(tokens) 302 else: 303 return Transformation(tokens) 304 305 306class BeforeInput(Processor): 307 """ 308 Insert tokens before the input. 309 310 :param get_tokens: Callable that takes a 311 :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the 312 list of tokens to be inserted. 313 """ 314 def __init__(self, get_tokens): 315 assert callable(get_tokens) 316 self.get_tokens = get_tokens 317 318 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 319 if lineno == 0: 320 tokens_before = self.get_tokens(cli) 321 tokens = tokens_before + tokens 322 323 shift_position = token_list_len(tokens_before) 324 source_to_display = lambda i: i + shift_position 325 display_to_source = lambda i: i - shift_position 326 else: 327 source_to_display = None 328 display_to_source = None 329 330 return Transformation(tokens, source_to_display=source_to_display, 331 display_to_source=display_to_source) 332 333 @classmethod 334 def static(cls, text, token=Token): 335 """ 336 Create a :class:`.BeforeInput` instance that always inserts the same 337 text. 338 """ 339 def get_static_tokens(cli): 340 return [(token, text)] 341 return cls(get_static_tokens) 342 343 def __repr__(self): 344 return '%s(get_tokens=%r)' % ( 345 self.__class__.__name__, self.get_tokens) 346 347 348class AfterInput(Processor): 349 """ 350 Insert tokens after the input. 351 352 :param get_tokens: Callable that takes a 353 :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the 354 list of tokens to be appended. 355 """ 356 def __init__(self, get_tokens): 357 assert callable(get_tokens) 358 self.get_tokens = get_tokens 359 360 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 361 # Insert tokens after the last line. 362 if lineno == document.line_count - 1: 363 return Transformation(tokens=tokens + self.get_tokens(cli)) 364 else: 365 return Transformation(tokens=tokens) 366 367 @classmethod 368 def static(cls, text, token=Token): 369 """ 370 Create a :class:`.AfterInput` instance that always inserts the same 371 text. 372 """ 373 def get_static_tokens(cli): 374 return [(token, text)] 375 return cls(get_static_tokens) 376 377 def __repr__(self): 378 return '%s(get_tokens=%r)' % ( 379 self.__class__.__name__, self.get_tokens) 380 381 382class AppendAutoSuggestion(Processor): 383 """ 384 Append the auto suggestion to the input. 385 (The user can then press the right arrow the insert the suggestion.) 386 387 :param buffer_name: The name of the buffer from where we should take the 388 auto suggestion. If not given, we take the current buffer. 389 """ 390 def __init__(self, buffer_name=None, token=Token.AutoSuggestion): 391 self.buffer_name = buffer_name 392 self.token = token 393 394 def _get_buffer(self, cli): 395 if self.buffer_name: 396 return cli.buffers[self.buffer_name] 397 else: 398 return cli.current_buffer 399 400 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 401 # Insert tokens after the last line. 402 if lineno == document.line_count - 1: 403 buffer = self._get_buffer(cli) 404 405 if buffer.suggestion and buffer.document.is_cursor_at_the_end: 406 suggestion = buffer.suggestion.text 407 else: 408 suggestion = '' 409 410 return Transformation(tokens=tokens + [(self.token, suggestion)]) 411 else: 412 return Transformation(tokens=tokens) 413 414 415class ShowLeadingWhiteSpaceProcessor(Processor): 416 """ 417 Make leading whitespace visible. 418 419 :param get_char: Callable that takes a :class:`CommandLineInterface` 420 instance and returns one character. 421 :param token: Token to be used. 422 """ 423 def __init__(self, get_char=None, token=Token.LeadingWhiteSpace): 424 assert get_char is None or callable(get_char) 425 426 if get_char is None: 427 def get_char(cli): 428 if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?': 429 return '.' 430 else: 431 return '\xb7' 432 433 self.token = token 434 self.get_char = get_char 435 436 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 437 # Walk through all te tokens. 438 if tokens and token_list_to_text(tokens).startswith(' '): 439 t = (self.token, self.get_char(cli)) 440 tokens = explode_tokens(tokens) 441 442 for i in range(len(tokens)): 443 if tokens[i][1] == ' ': 444 tokens[i] = t 445 else: 446 break 447 448 return Transformation(tokens) 449 450 451class ShowTrailingWhiteSpaceProcessor(Processor): 452 """ 453 Make trailing whitespace visible. 454 455 :param get_char: Callable that takes a :class:`CommandLineInterface` 456 instance and returns one character. 457 :param token: Token to be used. 458 """ 459 def __init__(self, get_char=None, token=Token.TrailingWhiteSpace): 460 assert get_char is None or callable(get_char) 461 462 if get_char is None: 463 def get_char(cli): 464 if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?': 465 return '.' 466 else: 467 return '\xb7' 468 469 self.token = token 470 self.get_char = get_char 471 472 473 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 474 if tokens and tokens[-1][1].endswith(' '): 475 t = (self.token, self.get_char(cli)) 476 tokens = explode_tokens(tokens) 477 478 # Walk backwards through all te tokens and replace whitespace. 479 for i in range(len(tokens) - 1, -1, -1): 480 char = tokens[i][1] 481 if char == ' ': 482 tokens[i] = t 483 else: 484 break 485 486 return Transformation(tokens) 487 488 489class TabsProcessor(Processor): 490 """ 491 Render tabs as spaces (instead of ^I) or make them visible (for instance, 492 by replacing them with dots.) 493 494 :param tabstop: (Integer) Horizontal space taken by a tab. 495 :param get_char1: Callable that takes a `CommandLineInterface` and return a 496 character (text of length one). This one is used for the first space 497 taken by the tab. 498 :param get_char2: Like `get_char1`, but for the rest of the space. 499 """ 500 def __init__(self, tabstop=4, get_char1=None, get_char2=None, token=Token.Tab): 501 assert isinstance(tabstop, Integer) 502 assert get_char1 is None or callable(get_char1) 503 assert get_char2 is None or callable(get_char2) 504 505 self.get_char1 = get_char1 or get_char2 or (lambda cli: '|') 506 self.get_char2 = get_char2 or get_char1 or (lambda cli: '\u2508') 507 self.tabstop = tabstop 508 self.token = token 509 510 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 511 tabstop = int(self.tabstop) 512 token = self.token 513 514 # Create separator for tabs. 515 separator1 = self.get_char1(cli) 516 separator2 = self.get_char2(cli) 517 518 # Transform tokens. 519 tokens = explode_tokens(tokens) 520 521 position_mappings = {} 522 result_tokens = [] 523 pos = 0 524 525 for i, token_and_text in enumerate(tokens): 526 position_mappings[i] = pos 527 528 if token_and_text[1] == '\t': 529 # Calculate how many characters we have to insert. 530 count = tabstop - (pos % tabstop) 531 if count == 0: 532 count = tabstop 533 534 # Insert tab. 535 result_tokens.append((token, separator1)) 536 result_tokens.append((token, separator2 * (count - 1))) 537 pos += count 538 else: 539 result_tokens.append(token_and_text) 540 pos += 1 541 542 position_mappings[len(tokens)] = pos 543 544 def source_to_display(from_position): 545 " Maps original cursor position to the new one. " 546 return position_mappings[from_position] 547 548 def display_to_source(display_pos): 549 " Maps display cursor position to the original one. " 550 position_mappings_reversed = dict((v, k) for k, v in position_mappings.items()) 551 552 while display_pos >= 0: 553 try: 554 return position_mappings_reversed[display_pos] 555 except KeyError: 556 display_pos -= 1 557 return 0 558 559 return Transformation( 560 result_tokens, 561 source_to_display=source_to_display, 562 display_to_source=display_to_source) 563 564 565class ConditionalProcessor(Processor): 566 """ 567 Processor that applies another processor, according to a certain condition. 568 Example:: 569 570 # Create a function that returns whether or not the processor should 571 # currently be applied. 572 def highlight_enabled(cli): 573 return true_or_false 574 575 # Wrapt it in a `ConditionalProcessor` for usage in a `BufferControl`. 576 BufferControl(input_processors=[ 577 ConditionalProcessor(HighlightSearchProcessor(), 578 Condition(highlight_enabled))]) 579 580 :param processor: :class:`.Processor` instance. 581 :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance. 582 """ 583 def __init__(self, processor, filter): 584 assert isinstance(processor, Processor) 585 586 self.processor = processor 587 self.filter = to_cli_filter(filter) 588 589 def apply_transformation(self, cli, document, lineno, source_to_display, tokens): 590 # Run processor when enabled. 591 if self.filter(cli): 592 return self.processor.apply_transformation( 593 cli, document, lineno, source_to_display, tokens) 594 else: 595 return Transformation(tokens) 596 597 def has_focus(self, cli): 598 if self.filter(cli): 599 return self.processor.has_focus(cli) 600 else: 601 return False 602 603 def __repr__(self): 604 return '%s(processor=%r, filter=%r)' % ( 605 self.__class__.__name__, self.processor, self.filter) 606