├── README.md └── pythonvi.py /README.md: -------------------------------------------------------------------------------- 1 | # PythonVI # 2 | 3 | ---------- 4 | 5 | **VI editor implemented in python, supporting only the very basic features:** 6 | 7 | 1. Editing mode and command mode switch 8 | 1. Unlimited Undo/Redo history 9 | 1. Single clipboard for words/lines 10 | 1. Showing/hiding line numbers 11 | 1. Forward/backward searching, with regular expression (python re) 12 | 13 | **Implemented commands (mostly commands in cheatsheet)** 14 | 15 | - ("gg", "goto_first_line") 16 | - ("dw", "delete_word") 17 | - ("dW", "delete_term") 18 | - ("yw", "yank_word") 19 | - ("yW", "yank_term") 20 | - ("dd", "delete_line") 21 | - ("yy", "yank_line") 22 | - ("r", "replace_char") 23 | - ("i", "insert_mode") 24 | - ("I", "insert_line_start") 25 | - ("o", "insert_line_after") 26 | - ("O", "insert_line_before") 27 | - ("a", "insert_after") 28 | - ("A", "insert_line_end") 29 | - ("s", "delete_char_insert") 30 | - ("S", "delete_line_insert") 31 | - ("D", "delete_line_end") 32 | - ("u", "undo") 33 | - (".", "repeat_edit") 34 | - ("^", "goto_line_start") 35 | - ("0", "goto_line_start") 36 | - ("$", "goto_line_end") 37 | - ("-", "goto_prev_line_start") 38 | - ("+", "goto_next_line_start") 39 | - ("H", "goto_first_screen_line") 40 | - ("L", "goto_last_screen_line") 41 | - ("M", "goto_middle_screen_line") 42 | - ("G", "goto_line") 43 | - ("w", "next_word_start") 44 | - ("W", "next_term_start") 45 | - ("e", "word_end") 46 | - ("E", "term_end") 47 | - ("b", "word_start") 48 | - ("B", "term_start") 49 | - (":", "command_edit_mode") 50 | - ("~", "switch_case") 51 | - ("x", "delete_char") 52 | - ("X", "delete_last_char") 53 | - ("%", "match_pair") 54 | - ("/", "search_mode") 55 | - ("?", "reverse_search_mode") 56 | - ("n", "search_next") 57 | - ("N", "search_previous") 58 | - ("Y", "yank_line") 59 | - ("p", "paste_after") 60 | - ("P", "paste_before") 61 | - (CTRL+R, "redo") 62 | - (CTRL+F, "Next page") 63 | - (CTRL+B, "Previous page") 64 | - (CTRL+E, "Scroll down") 65 | - (CTRL+Y, "Scroll up") 66 | 67 | **Ex mode commands** 68 | 69 | - :w 70 | - :wq 71 | - :q 72 | - :q! 73 | - nu - show line number 74 | - nonu - hide line number -------------------------------------------------------------------------------- /pythonvi.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is my implementaion of the classic vi editor 3 | The plan is to support only the very basic functionality and commands 4 | """ 5 | import curses 6 | import curses.ascii 7 | import sys 8 | import copy 9 | import signal 10 | import re 11 | import string 12 | 13 | debug = True 14 | if debug: 15 | logfile = open("log.txt", "a") 16 | 17 | def writelog(*argv): 18 | if debug: 19 | logline = " ".join(["%s"%arg for arg in argv]) 20 | logfile.write(logline+"\n") 21 | 22 | class LineBeyondScreenError(Exception): 23 | "Raised when the line or cursor is beyond the screen and needs scrolling" 24 | 25 | class EditOp(object): 26 | """Edit operation: 27 | * edit_type: insert/delete/replace 28 | * object_type: char/line 29 | * cnt: the number of objects operated on 30 | * pos: the position when the operation happened 31 | * value: could be chars/single line/lines 32 | * replacement: only used by replace ops 33 | * backwards: boolean, true for delete command with backspace 34 | """ 35 | def __init__(self, editor, etype, otype, pos): 36 | self.editor = editor 37 | self.edit_type = etype 38 | self.object_type = otype 39 | self.pos = pos 40 | self.value = "" 41 | self.backwards = False 42 | 43 | def reverse(self): 44 | reverse_op = copy.copy(self) 45 | if self.edit_type == "insert": 46 | reverse_op.edit_type = "delete" 47 | elif self.edit_type == "delete": 48 | reverse_op.edit_type = "insert" 49 | else: # replace, just swap value and replacement 50 | reverse_op.value = self.replacement 51 | reverse_op.replacement = self.value 52 | reverse_op.apply() 53 | 54 | def apply(self): 55 | _buffer = self.editor.buffer 56 | y, x = self.pos 57 | if self.object_type == "char": 58 | if self.edit_type == "delete": 59 | if "\n" in self.value: 60 | segments = self.value.split("\n") 61 | if y= len(s) or idx <0: return idx 336 | step = 1 if direction=="forward" else -1 337 | if s[idx] in wordchars: 338 | while idx>=0 and idx < len(s) and s[idx] in wordchars : idx += step 339 | elif s[idx] == " ": 340 | while idx>=0 and idx < len(s) and s[idx] == " ": idx += step 341 | else: 342 | while id>=0 and idx < len(s) and s[idx] not in wordchars and s[idx] != " ": idx+=step 343 | return idx 344 | 345 | def advance_term(self, s, idx, direction="forward"): 346 | step = 1 if direction=="forward" else -1 347 | while idx>=0 and idx < len(s) and s[idx] != " ": idx +=step 348 | return idx 349 | 350 | def advance_spaces(self, s, idx, direction="forward"): 351 | step = 1 if direction=="forward" else -1 352 | while idx>=0 and idx < len(s) and s[idx]==" ": idx+=step 353 | return idx 354 | 355 | def advance_one_char(self, pos, direction="forward"): 356 | y, x = pos 357 | if direction=="forward": 358 | x = x+1 359 | if x >= len(self.buffer[y]): 360 | if y==len(self.buffer)-1: 361 | return None 362 | else: 363 | y+=1 364 | while len(self.buffer[y])==0: y+=1 365 | if y>=len(self.buffer): return None 366 | else: return y, 0 367 | else: 368 | return y, x 369 | else: 370 | x = x - 1 371 | if x < 0: 372 | if y == 0: 373 | return None 374 | else: 375 | y -= 1 376 | while len(self.buffer[y])==0: y-=1 377 | if y<0: return None 378 | else: return y, len(self.buffer[y])-1 379 | else: 380 | return y, x 381 | 382 | def handle_command(self, ch): 383 | if self.is_direction_char(ch): 384 | self.handle_cursor_move(ch) 385 | return 386 | cmd = self.parse_command_after_char(ch) 387 | if not cmd: return 388 | # if is a tuple, set the parameter 389 | if isinstance(cmd, tuple): 390 | parameter = cmd[1] 391 | cmd = cmd[0] 392 | else: 393 | parameter = None 394 | ### commands for switching to editing mode ### 395 | if cmd == "insert_line_after": 396 | self.buffer.insert(self.pos[0]+1, "") 397 | self.pos = (self.pos[0]+1, 0) 398 | self.refresh() 399 | self.refresh_cursor() 400 | elif cmd == "insert_line_before": 401 | self.buffer.insert(self.pos[0], "") 402 | self.pos = (self.pos[0], 0) 403 | self.refresh() 404 | self.refresh_cursor() 405 | elif cmd == "insert_line_start": 406 | self.pos = (self.pos[0], 0) 407 | self.refresh_cursor() 408 | elif cmd == "insert_after": 409 | if self.buffer[self.pos[0]]: 410 | self.pos = (self.pos[0], self.pos[1]+1) 411 | self.refresh_cursor() 412 | elif cmd == "insert_line_end": 413 | self.pos = (self.pos[0], len(self.buffer[self.pos[0]])) 414 | self.refresh_cursor() 415 | elif cmd == "delete_char_insert": 416 | s = self.buffer[self.pos[0]] 417 | if s: 418 | char = s[self.pos[1]] 419 | s = s[:self.pos[1]]+s[self.pos[1]+1:] 420 | self.buffer[self.pos[0]] = s 421 | self.refresh() 422 | self.refresh_cursor() 423 | self.editop = EditOp(self, "delete", "char", self.pos) 424 | self.editop.value = char 425 | self.commit_current_edit() 426 | elif cmd == "delete_line_insert": 427 | s = self.buffer[self.pos[0]] 428 | if s: 429 | oldline = self.buffer[self.pos[0]] 430 | self.buffer[self.pos[0]] = "" 431 | self.pos = self.pos[0], 0 432 | self.refresh() 433 | self.refresh_cursor() 434 | self.editop = EditOp(self, "replace", "char", (self.pos[0], 0)) 435 | self.editop.value = oldline 436 | self.editop.replacement = "" 437 | self.commit_current_edit() 438 | ### commands for moving cursor position ### 439 | elif cmd == "goto_line_start": 440 | self.pos = (self.pos[0], 0) 441 | self.refresh_cursor() 442 | elif cmd == "goto_line_end": 443 | self.pos = (self.pos[0], max(len(self.buffer[self.pos[0]])-1, 0)) 444 | self.refresh_cursor() 445 | elif cmd == "goto_prev_line_start": 446 | if self.pos[0]>0: 447 | y, x = self.pos 448 | self.pos = (y-1, 0) 449 | self.refresh_cursor() 450 | elif cmd == "goto_next_line_start": 451 | if self.pos[0]=0: 478 | line_cnt += len(self.buffer[idx])/self.maxx+1 479 | if line_cnt > self.maxy-1: 480 | break 481 | idx -= 1 482 | idx = min(idx+1, self.topline) 483 | self.topline = idx 484 | self.refresh() 485 | self.pos = (self.topline + self.screen_lines-1, 0) 486 | self.refresh_cursor() 487 | elif cmd == "scroll_down": 488 | if self.topline == len(self.buffer)-1: 489 | return 490 | self.topline = self.topline + 1 491 | self.refresh() 492 | if self.pos[0] < self.topline: 493 | # if the cursor is beyond top of screen, move cursor to topline, keep the x pos 494 | xpos = max(0, min(self.pos[1], len(self.buffer[self.topline])-1)) 495 | self.pos = (self.topline, xpos) 496 | self.refresh_cursor() 497 | elif cmd == "scroll_up": 498 | if self.topline == 0: 499 | return 500 | self.topline = self.topline - 1 501 | self.refresh() 502 | # to make it simple, when cursor is beyond the bottom, move it to the start of last line 503 | if self.pos[0] >= self.topline+self.screen_lines-1: 504 | self.pos = (self.topline+self.screen_lines-1, 0) 505 | self.refresh_cursor() 506 | elif cmd=="goto_line" or cmd=="goto_first_line": 507 | if cmd=="goto_first_line": 508 | lineno = 0 509 | elif not parameter or parameter>len(self.buffer): 510 | # go to last line 511 | lineno = len(self.buffer)-1 512 | else: 513 | lineno = parameter-1 514 | self.topline = lineno 515 | self.refresh() 516 | self.pos = (lineno, 0) 517 | self.refresh_cursor() 518 | elif cmd == "next_word_start": 519 | s = self.buffer[self.pos[0]] 520 | idx = self.pos[1] 521 | idx = self.advance_word(s, idx) 522 | idx = self.advance_spaces(s, idx) 523 | if idx >= len(s): 524 | if self.pos[0] < len(self.buffer)-1: 525 | self.pos = self.pos[0]+1, 0 526 | else: 527 | self.pos = self.pos[0], idx 528 | self.refresh_cursor() 529 | elif cmd == "next_term_start": 530 | s = self.buffer[self.pos[0]] 531 | idx = self.pos[1] 532 | idx = self.advance_term(s, idx) 533 | idx = self.advance_spaces(s, idx) 534 | if idx >= len(s): 535 | if self.pos[0] < len(self.buffer)-1: 536 | self.pos = self.pos[0]+1, 0 537 | else: 538 | self.pos = self.pos[0], idx 539 | self.refresh_cursor() 540 | elif cmd == "word_end": 541 | s = self.buffer[self.pos[0]] 542 | idx = self.pos[1]+1 543 | if idx >= len(s): 544 | if self.pos[0] < len(self.buffer)-1: 545 | self.pos = self.pos[0]+1, 0 546 | s = self.buffer[self.pos[0]] 547 | idx = 0 548 | idx = self.advance_word(s, idx) 549 | self.pos = self.pos[0], max(idx-1, 0) 550 | self.refresh_cursor() 551 | elif cmd == "term_end": 552 | s = self.buffer[self.pos[0]] 553 | idx = self.pos[1]+1 554 | if idx >= len(s): 555 | if self.pos[0] < len(self.buffer)-1: 556 | self.pos = self.pos[0]+1, 0 557 | s = self.buffer[self.pos[0]] 558 | idx = 0 559 | idx = self.advance_spaces(s, idx) 560 | idx = self.advance_term(s, idx) 561 | self.pos = self.pos[0], max(idx-1, 0) 562 | self.refresh_cursor() 563 | elif cmd == "word_start": 564 | s = self.buffer[self.pos[0]] 565 | idx = self.pos[1]-1 566 | if idx < 0: 567 | if self.pos[0]>0: 568 | # back up to last line and recurse 569 | lineno = self.pos[0]-1 570 | self.pos = lineno, len(self.buffer[lineno]) 571 | self.handle_command(ch) 572 | return 573 | if s[idx] == " ": 574 | idx = self.advance_spaces(s, idx, "backwards") 575 | # this is duplicate code, need a way to dedup 576 | if idx < 0: 577 | if self.pos[0]>0: 578 | # back up to last line and recurse 579 | lineno = self.pos[0]-1 580 | self.pos = lineno, len(self.buffer[lineno]) 581 | self.handle_command(ch) 582 | return 583 | idx = self.advance_word(s, idx, "backwards") 584 | self.pos = self.pos[0], idx+1 585 | self.refresh_cursor() 586 | elif cmd == "term_start": 587 | s = self.buffer[self.pos[0]] 588 | idx = self.pos[1]-1 589 | if idx < 0: 590 | if self.pos[0]>0: 591 | # back up to last line and recurse 592 | lineno = self.pos[0]-1 593 | self.pos = lineno, len(self.buffer[lineno]) 594 | self.handle_command(ch) 595 | return 596 | if s[idx] == " ": 597 | idx = self.advance_spaces(s, idx, "backwards") 598 | # this is duplicate code, need a way to dedup 599 | if idx < 0: 600 | if self.pos[0]>0: 601 | # back up to last line and recurse 602 | lineno = self.pos[0]-1 603 | self.pos = lineno, len(self.buffer[lineno])-1 604 | self.handle_command(ch) 605 | return 606 | idx = self.advance_term(s, idx, "backwards") 607 | self.pos = self.pos[0], idx+1 608 | self.refresh_cursor() 609 | elif cmd == "backup_char": 610 | self.pos = self.pos[0], max(self.pos[1]-1,0) 611 | self.refresh_cursor() 612 | ### commands for managing edits history ### 613 | elif cmd == "undo": # for undo 614 | pos = self.editlist.get_pos() 615 | if not self.editlist.undo(): 616 | self.flash_status_line("-- Already at the first edit --") 617 | else: 618 | self.pos = pos 619 | self.refresh() 620 | self.refresh_cursor() 621 | elif cmd=="redo": # Ctrl+R for redo 622 | if not self.editlist.redo(): 623 | self.flash_status_line("-- Already at the last edit --") 624 | else: 625 | self.pos = self.editlist.get_pos() 626 | self.refresh() 627 | self.refresh_cursor() 628 | elif cmd=="repeat_edit": 629 | if self.editlist.repeat(): 630 | self.pos = self.editlist.get_pos() 631 | self.refresh() 632 | self.refresh_cursor() 633 | ### commands for entering command editing mode (Ex mode) ### 634 | elif cmd in ("command_edit_mode", "search_mode", "reverse_search_mode"): 635 | self.command_editing = True 636 | self.commandline = chr(ch) 637 | self.refresh_command_line() 638 | ### edit commands ### 639 | elif cmd == "switch_case": 640 | line = self.buffer[self.pos[0]] 641 | ch = line[self.pos[1]] 642 | if ch in string.letters: 643 | line = line[:self.pos[1]]+ch.swapcase()+line[self.pos[1]+1:] 644 | self.buffer[self.pos[0]] = line 645 | self.refresh() 646 | # add edit operation to list 647 | self.editop = EditOp(self, "replace", "char", self.pos) 648 | self.editop.value = ch 649 | self.editop.replacement = ch.swapcase() 650 | self.commit_current_edit() 651 | if self.pos[1]= len(s): 673 | newx = self.pos[1]-1 674 | else: 675 | newx = self.pos[1] 676 | self.pos = self.pos[0], max(newx, 0) 677 | self.refresh_cursor() 678 | elif cmd in ("delete_term", "yank_term"): 679 | s = self.buffer[self.pos[0]] 680 | idx = self.pos[1] 681 | idx = self.advance_term(s, idx) 682 | idx = self.advance_spaces(s, idx) 683 | if cmd =="delete_term": 684 | self.buffer[self.pos[0]] = s[:self.pos[1]]+s[idx:] 685 | self.refresh() 686 | if self.pos[1]= len(s): 695 | newx = self.pos[1]-1 696 | else: 697 | newx = self.pos[1] 698 | self.pos = self.pos[0], max(newx, 0) 699 | self.refresh_cursor() 700 | elif cmd == "delete_line": 701 | if not len(self.buffer) or (len(self.buffer)==1 and not self.buffer[0]): return 702 | oldline = self.buffer[self.pos[0]] 703 | self.clipboard.store([oldline]) # deleted lines are stored in clipboard 704 | del self.buffer[self.pos[0]] 705 | if not self.buffer: # should preserve at least one blank line 706 | self.buffer =[""] 707 | self.editop = EditOp(self, "replace", "line", self.pos) 708 | self.editop.replacement = [""] 709 | else: 710 | self.editop = EditOp(self, "delete", "line", self.pos) 711 | self.editop.value = [oldline] 712 | self.commit_current_edit() 713 | self.refresh() 714 | if self.pos[0] >= len(self.buffer): 715 | y = len(self.buffer)-1 716 | else: 717 | y = self.pos[0] 718 | x = min(self.pos[1], len(self.buffer[y])-1) 719 | self.pos = y, max(x, 0) 720 | self.refresh_cursor() 721 | elif cmd == "delete_line_end": 722 | s = self.buffer[self.pos[0]] 723 | if s: 724 | oldpos = self.pos 725 | deleted = s[self.pos[1]:] 726 | self.buffer[self.pos[0]] = s[:self.pos[1]] 727 | self.pos = self.pos[0], max(self.pos[1]-1, 0) 728 | self.refresh() 729 | self.refresh_cursor() 730 | self.clipboard.store(deleted) 731 | self.editop = EditOp(self, "delete", "char", oldpos) 732 | self.editop.value = deleted 733 | self.commit_current_edit() 734 | elif cmd == "replace_char": 735 | line = self.buffer[self.pos[0]] 736 | oldchar = line[self.pos[1]] 737 | line = line[:self.pos[1]]+parameter+line[self.pos[1]+1:] 738 | self.buffer[self.pos[0]] = line 739 | self.refresh() 740 | self.refresh_cursor() 741 | # add replace operation 742 | self.editop = EditOp(self, "replace", "char", self.pos) 743 | self.editop.value = oldchar 744 | self.editop.replacement = parameter 745 | self.commit_current_edit() 746 | ### search related commands ### 747 | elif cmd == "match_pair": 748 | chrs = "()[]{}" 749 | matcher = {"(":")", ")":"(", "[":"]", "]":"[","{":"}", "}":"{"} 750 | char = self.buffer[self.pos[0]][self.pos[1]] 751 | if char not in chrs: return 752 | if char in "([{": 753 | direction = "forward" 754 | else: 755 | direction = "backward" 756 | pos = self.pos 757 | depth = 1 758 | while True: 759 | pos = self.advance_one_char(pos, direction) 760 | # import pdb;pdb.set_trace() 761 | if not pos: 762 | self.flash_status_line("--No matching symbol found--") 763 | break 764 | current = self.buffer[pos[0]][pos[1]] 765 | if current == char: depth+=1 766 | elif current == matcher[char]: 767 | depth -=1 768 | if depth == 0: 769 | self.pos = pos 770 | self.refresh_cursor() 771 | break 772 | elif cmd in ("search_next", "search_previous"): 773 | if cmd=="search_next": 774 | direction = self.searchdir 775 | else: 776 | direction = "forward" if self.searchdir == "backward" else "backward" 777 | pos = self.search_for_keyword(direction) 778 | if not pos: 779 | self.flash_status_line("--Search pattern not found--") 780 | return 781 | self.pos = pos 782 | self.refresh_cursor() 783 | ### copy paste commands ### 784 | elif cmd == "yank_line": 785 | self.clipboard.store([self.buffer[self.pos[0]]]) 786 | elif cmd in ("paste_before", "paste_after"): 787 | t, v = self.clipboard.retrieve() 788 | if not v: return 789 | if t=="char": 790 | line = self.buffer[self.pos[0]] 791 | insertx = self.pos[1] if cmd=="paste_before" else self.pos[1]+1 792 | self.buffer[self.pos[0]] = line[:insertx] + v +line[insertx:] 793 | insert_pos = self.pos[0], insertx 794 | if not line: 795 | self.pos = self.pos[0], len(v) -1 796 | else: 797 | self.pos = self.pos[0], self.pos[1] + len(v) 798 | # commit the edit operation 799 | self.start_new_char_edit("insert", insert_pos) 800 | self.editop.value = v 801 | self.commit_current_edit() 802 | else: 803 | assert isinstance(v, list) 804 | inserty = self.pos[0] if cmd =="paste_before" else self.pos[0]+1 805 | for line in reversed(v): 806 | self.buffer.insert(inserty, line) 807 | self.pos = inserty, 0 808 | self.editop = EditOp(self, "insert", "line", self.pos) 809 | self.editop.value = v 810 | self.commit_current_edit() 811 | self.refresh() 812 | self.refresh_cursor() 813 | # Check if needs to switching to editing mode 814 | if "insert" in cmd: 815 | self.mode = "editing" 816 | self.refresh_command_line() 817 | 818 | def save_file(self): 819 | assert self.outfile is not None 820 | self.outfile.truncate(0) 821 | # Seek is absolutely necessary as truncate does NOT modify file position 822 | self.outfile.seek(0) 823 | for line in self.buffer: 824 | self.outfile.write(line) 825 | self.outfile.write("\n") 826 | self.outfile.flush() 827 | # save the editlist cursor 828 | self.checkpoint = self.editlist.cursor 829 | 830 | def handle_editing_command(self, ch): 831 | if curses.ascii.isprint(ch): 832 | self.commandline += chr(ch) 833 | self.refresh_command_line() 834 | elif ch==curses.KEY_BACKSPACE or ch==127: 835 | self.commandline = self.commandline[:-1] 836 | self.refresh_command_line() 837 | elif ch==27 or ch==curses.ascii.ETX: # ESC, back to command mode 838 | self.command_editing = False 839 | self.commandline = "" 840 | self.refresh_command_line() 841 | self.refresh_cursor() 842 | elif ch==10: # new line \n 843 | self.command_editing = False 844 | if self.commandline.startswith(":"): 845 | commandline = self.commandline[1:].strip() 846 | if commandline in ("q", "q!"): # handle quit commands 847 | if commandline == "q" and self.dirty: 848 | self.flash_status_line("--Unsaved Changes--") 849 | else: 850 | raise SystemExit() 851 | elif commandline in ("w", "wq"): # handle write and write/quit without filename 852 | if self.outfile: 853 | self.save_file() 854 | if commandline == "wq": 855 | raise SystemExit() 856 | else: 857 | self.flash_status_line("--File saved--") 858 | else: 859 | self.flash_status_line("--Target file not specified--") 860 | elif commandline.startswith("w ") or commandline.startswith("wq "): 861 | parts = commandline.split() 862 | if len(parts)>2: 863 | self.flash_status_line("--Only one file name allowed--") 864 | else: 865 | cmd, filename = parts 866 | try: 867 | self.outfile = open(filename, "w") 868 | except Exception as e: 869 | self.flash_status_line("--File open fails: %s--"%e) 870 | else: 871 | self.save_file() 872 | if cmd == "wq": 873 | raise SystemExit() 874 | else: 875 | self.flash_status_line("--File saved--") 876 | elif commandline in ("nu", "nonu"): 877 | self.showlineno = (commandline=="nu") 878 | if self.showlineno: 879 | self.maxx = self.scr.getmaxyx()[1] - self.get_lineno_width() 880 | else: 881 | self.maxx = self.scr.getmaxyx()[1] 882 | self.refresh() 883 | self.refresh_cursor() 884 | else: 885 | self.flash_status_line("-- Command not recognized --") 886 | elif self.commandline.startswith("/") or self.commandline.startswith("?"): 887 | keyword = self.commandline[1:] 888 | try: 889 | keyword_regex = re.compile(keyword) 890 | except: 891 | self.flash_status_line("--Invalid search pattern string--") 892 | return 893 | self.searchpos = self.pos 894 | self.searchkw = keyword_regex 895 | if self.commandline.startswith("/"): 896 | self.searchdir = "forward" 897 | else: 898 | self.searchdir = "backward" 899 | pos = self.search_for_keyword(self.searchdir) 900 | if not pos: 901 | self.flash_status_line("--Search pattern not found--") 902 | return 903 | self.pos = pos 904 | self.refresh_cursor() 905 | self.refresh_command_line() 906 | self.refresh_cursor() 907 | 908 | def search_for_keyword(self, direction="forward"): 909 | # return a position where the keyword is found, or return None if not found 910 | y, x = self.searchpos 911 | # First make sure the start_pos is valid position, if invalid, fall back to 0,0 912 | if y>=len(self.buffer) or (x>=len(self.buffer[y]) and x!=0): 913 | y, x = 0, 0 914 | step = 1 if direction=="forward" else -1 915 | visited = [] 916 | while True: # iterate over the lines 917 | visited.append((y, x)) 918 | line = self.buffer[y][x:] if direction=="forward" else self.buffer[y][:x] 919 | if line: 920 | if direction == "forward": 921 | mo = self.searchkw.search(line) # left to right 922 | if mo: 923 | self.searchpos = y, x+mo.end() 924 | if self.searchpos[1] == len(self.buffer[y]): 925 | self.searchpos = (y+step)%len(self.buffer), 0 926 | return (y, x+mo.start()) 927 | else: # backward 928 | # to search right to left for regex: http://stackoverflow.com/questions/33232729/how-to-search-for-the-last-occurrence-of-a-regular-expression-in-a-string-in-pyt 929 | starts = [mo.start() for mo in self.searchkw.finditer(line)] 930 | if starts: 931 | self.searchpos = y, starts[-1] 932 | return (y, starts[-1]) 933 | y = y+step 934 | if y >=len(self.buffer): 935 | y = 0 936 | self.flash_status_line("--Search hit BOTTOM, continuing at TOP--") 937 | elif y<0: 938 | y = len(self.buffer)-1 939 | self.flash_status_line("--Search hit TOP, continuing at BOTTOM--") 940 | x = 0 if direction=="forward" else len(self.buffer[y]) 941 | if (y, x) in visited: # revisit a visited position, meaning the doc has been scanned in full 942 | return None 943 | 944 | def handle_cursor_move(self, ch): 945 | # finish the last edit if exists 946 | self.commit_current_edit() 947 | y, x = self.pos 948 | # print y, x 949 | if ch in (curses.KEY_UP, ord('k')) and y > 0: 950 | y = y-1 951 | last_char = len(self.buffer[y]) 952 | if self.mode == "command" and last_char>0: 953 | last_char = last_char-1 954 | x = min(x, last_char) 955 | self.pos = (y, x) 956 | # if y0: 964 | last_char = last_char-1 965 | x = min(x, last_char) 966 | self.pos = (y, x) 967 | self.refresh_cursor() 968 | elif ch in (curses.KEY_LEFT, ord('h')) and x>0: 969 | self.pos = (y, x-1) 970 | self.refresh_cursor() 971 | elif ch in (curses.KEY_RIGHT, ord('l')): 972 | last_char = len(self.buffer[y]) 973 | if self.mode == "command" and last_char>0: 974 | last_char = last_char-1 975 | if x 0: 1008 | self.editop.append_edit("\n") 1009 | if ch==88: self.clipboard.store("\n") 1010 | lastlen = len(self.buffer[y-1]) 1011 | self.buffer[y-1] = self.buffer[y-1] + self.buffer[y] 1012 | del self.buffer[y] 1013 | self.pos = y-1, lastlen 1014 | self.commit_current_edit() 1015 | else: 1016 | char = self.buffer[y][x-1] 1017 | self.editop.append_edit(char) 1018 | if ch == 88: 1019 | self.clipboard.store(char) 1020 | self.commit_current_edit() 1021 | self.buffer[y] = self.buffer[y][:x-1]+self.buffer[y][x:] 1022 | self.pos = y, x-1 1023 | self.refresh() 1024 | self.refresh_cursor() 1025 | 1026 | def handle_editing(self, ch): 1027 | y, x = self.pos 1028 | if curses.ascii.isprint(ch) or ch==ord("\n") or ch==ord("\t"): 1029 | if not self.editop or not self.editop.edit_type == "insert": 1030 | self.start_new_char_edit("insert", self.pos) 1031 | if chr(ch)=="\t" and self.config["expandtab"]: # if expand tab into spaces 1032 | spaces = " "*self.config["tabspaces"] 1033 | self.editop.append_edit(spaces) 1034 | self.buffer[y] = self.buffer[y][:x] + spaces + self.buffer[y][x:] 1035 | self.pos = y, x+self.config["tabspaces"] 1036 | else: 1037 | self.editop.append_edit(chr(ch)) 1038 | if chr(ch)=="\n": 1039 | line = self.buffer[y] 1040 | self.buffer[y] = line[:x] 1041 | self.buffer.insert(y+1, line[x:]) 1042 | self.pos = y+1, 0 1043 | # now adjust the indentation if needed 1044 | self.reindent_line(y+1) 1045 | self.start_new_char_edit("insert", self.pos) 1046 | else: 1047 | self.buffer[y] = self.buffer[y][:x]+chr(ch)+self.buffer[y][x:] 1048 | self.pos = y, x+1 1049 | self.refresh() 1050 | self.refresh_cursor() 1051 | elif ch in (curses.KEY_DC, curses.KEY_BACKSPACE, 127): # DEL or BACKSPACE 1052 | self.handle_delete_char(ch) 1053 | elif self.is_direction_char(ch): 1054 | self.handle_cursor_move(ch) 1055 | elif ch==curses.KEY_HOME: 1056 | self.pos = self.pos[0], 0 1057 | self.refresh_cursor() 1058 | elif ch==curses.KEY_END: 1059 | self.pos = self.pos[0], len(self.buffer[self.pos[0]]) if self.buffer[self.pos[0]] else 0 1060 | self.refresh_cursor() 1061 | elif ch==27 or ch==curses.ascii.ETX: # ESC, to exit editing mode 1062 | self.mode = "command" 1063 | self.command_editing = False 1064 | self.partial = "" 1065 | self.status_line = "-- COMMAND --" 1066 | # need to commit edit before switching mode 1067 | self.commit_current_edit() 1068 | # If currently pos beyond end of line, move back 1 char before entering command mode 1069 | if x != 0 and x == len(self.buffer[y]): 1070 | self.pos = (y, x-1) 1071 | self.refresh_cursor() 1072 | self.refresh_command_line() 1073 | return True 1074 | 1075 | # View part of MVC: screen rendering 1076 | def clear_scr_line(self, y): 1077 | self.scr.move(y,0) 1078 | self.scr.clrtoeol() 1079 | 1080 | def get_lineno_width(self): 1081 | if self.showlineno: 1082 | return len(str(len(self.buffer))) 1083 | else: 1084 | return 0 1085 | 1086 | def refresh_cursor(self): 1087 | # move the cursor position based on self.pos 1088 | # when cursor moves beyond top of screen 1089 | if self.pos[0] < self.topline: 1090 | self.topline = self.pos[0] 1091 | self.refresh() 1092 | screen_y = sum(self.line_heights[:self.pos[0]-self.topline]) 1093 | screen_y += self.pos[1]/self.maxx 1094 | screen_x = self.pos[1]%self.maxx + self.get_lineno_width() 1095 | 1096 | if screen_y >= self.maxy-1 and self.topline= self.maxy-1: 1142 | raise LineBeyondScreenError() 1143 | while idx= self.maxy-1: 1151 | raise LineBeyondScreenError() 1152 | except LineBeyondScreenError: 1153 | self.line_heights.append(line_height) 1154 | break 1155 | self.line_heights.append(line_height) 1156 | 1157 | # fill the extra lines with ~ 1158 | while _y < self.maxy-1: 1159 | self.clear_scr_line(_y) 1160 | self.scr.addstr(_y,0,"~", curses.COLOR_RED) 1161 | _y+=1 1162 | # last line is reserved for commands 1163 | # self.refresh_command_line() 1164 | 1165 | def intercept_signals(): 1166 | signal.signal(signal.SIGINT, signal.SIG_IGN) 1167 | 1168 | def main(): 1169 | # parse the file argument if exists 1170 | if len(sys.argv)==1: 1171 | openfile = None 1172 | elif len(sys.argv)==2: 1173 | openfile = sys.argv[1] 1174 | else: 1175 | print "Only support opening one file for now" 1176 | raise SystemExit() 1177 | 1178 | if openfile: 1179 | f = open(openfile, "r+") 1180 | buf = [line[:-1] if line.endswith('\n') else line for line in f.readlines()] 1181 | else: 1182 | f = None 1183 | buf = [""] 1184 | intercept_signals() 1185 | editor = Editor(f, buf) 1186 | try: 1187 | curses.wrapper(editor.main_loop) 1188 | except Exception: 1189 | editor.outfile = open("before_crash_text", "w") 1190 | editor.save_file() 1191 | print "Sorry, pythonvi crashed, your text has been saved in before_crash_text in current directory." 1192 | finally: 1193 | if editor.outfile: editor.outfile.close() 1194 | 1195 | 1196 | if __name__ == "__main__": 1197 | main() 1198 | --------------------------------------------------------------------------------