diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3e8f487 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: python + +matrix: + include: + - python: 3.6 + - python: 3.5 + - python: 2.7 + - python: pypy + - python: pypy3 + +install: + - pip install . + - pip install pytest + - pip list + +script: + - echo "$TRAVIS_PYTHON_VERSION" + - cd tests + - py.test diff --git a/CHANGELOG b/CHANGELOG index 9846f35..7d46893 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,183 @@ CHANGELOG ========= +3.0.3: 2022-05-16 +----------------- + +- Implemented basic :s[ubstitute] command and various related fixes. +- Fixed license text in setup.py. + + +3.0.2: 2019-11-28 +------------------ + +- Added missing dependency: 'six'. + + +3.0.1: 2019-11-28 +------------------ + +- Upgrade to prompt_toolkit 3.0 + + +2.0.24: 2019-01-27 +------------------ + +- Improved the file explorer. + +2.0.23: 2018-09-30 +------------------ + +- Implemented "breakindent" option. +- Implemented "temporary navigation mode". + +2.0.22: 2018-06-03 +----------------- + +- Small fix: don't include default input processors from prompt_toolkit. + + +2.0.1: 2018-06-02 +----------------- + +Upgrade to prompt_toolkit 2.0 + +Edit: By accident, this was uploaded as 2.0.21. + + +0.0.21: 2017-08-08 +------------------ + +- Use load_key_bindings instead of KeyBindingManager (fixes compatibility with + latest prompt_toolkit 1.0) + + +0.0.20: 2016-10-16 +------------------- + +- Added support for inserting before/after visual block. +- Better Jedi integration for completion of Python files. +- Don't depend on ptpython code anymore. + +Upgrade to prompt_toolkit==1.0.8 + + +0.0.19: 2016-08-04 +------------------ + +- Take output encoding ($LANG) into account in several places. + +Upgrade to prompt_toolkit==1.0.4 + + +0.0.18: 2016-05-09 +------------------ + +Upgrade to ptpython==0.34. + + +0.0.17: 2016-05-05 +----------------- + +Upgrade to prompt_toolkit==1.0.0 and ptpython==0.32. + +- Added colorcolumn, cursorcolumn and cursorline commands. +- Added cul,nocul,cuc,nocuc commands. +- Added tildeop command. +- Fixes bug in ~ expension in :e command. + + +0.0.16: 2016-03-14 +----------------- + +Upgrade to prompt_toolkit==0.60 and ptpython==0.31. + + +0.0.15: 2016-02-27 +----------------- + +Upgrade to prompt_toolkit==0.59 and ptpython==0.30. + + +0.0.14: 2016-02-24 +----------------- + +Upgrade to prompt_toolkit==0.58 and ptpython==0.29. + + +0.0.13: 2016-01-04 +----------------- + +Upgrade to prompt_toolkit==0.57 and ptpython==0.28. + + +0.0.12: 2016-01-03 +----------------- + +Upgrade to prompt_toolkit==0.56 and ptpython==0.27. + +New features: +- Visual block selection type. +- Handle mouse events on tabs. +- Focus window on click. +- Show the current document in the titlebar. + +Fixes: +- Make sure that 'pyvim -u ...' alsa works on Python 2. + + +0.0.11: 2015-10-29 +----------------- + +Upgrade to prompt_toolkit==0.54 and ptpython==0.25. + + +0.0.10: 2015-09-24 +----------------- + +Upgrade to prompt_toolkit==0.52 and ptpython==0.24. + + +0.0.9: 2015-09-24 +----------------- + +Upgrade to prompt_toolkit==0.51 and ptpython==0.23. + + +0.0.8: 2015-08-08 +----------------- + +Upgrade to prompt_toolkit==0.46 and ptpython==0.21. + + +0.0.7: 2015-06-30 +----------------- + +Upgrade to prompt_toolkit==0.45 and ptpython==0.20. + + +0.0.6: 2015-06-30 +----------------- + +Upgrade to prompt_toolkit==0.44 and ptpython==0.19. + +New features: +- Added __main__: allow "python -m pyvim". + + +0.0.5: 2015-06-22 +----------------- + +Upgrade to prompt_toolkit==0.41 and ptpython==0.15. + + +0.0.4: 2015-05-31 +----------------- + +New features: +- Upgrade to prompt_toolkit==0.38: + Several new key bindings bug fixes and faster pasting. + 0.0.3: 2015-05-07 ----------------- diff --git a/README.rst b/README.rst index f1269e6..2bf09db 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,8 @@ pyvim Issues, questions, wishes, comments, feedback, remarks? Please create a GitHub issue, I appreciate it. +|Build Status| + Installation ------------ @@ -48,7 +50,7 @@ We have already many nice things, for instance: - All of the functionality of `prompt_toolkit `_. This includes a lot of Vi key bindings, it's platform independent and runs on every Python - version from python 2.6 up to 3.4. It also runs on Pypy with a noticable + version from python 2.6 up to 3.4. It also runs on Pypy with a noticeable performance boost. - Several ``:set ...`` commands have been implemented, like ``incsearch``, @@ -119,7 +121,7 @@ Compared to Vi Improved, Pyvim is still less powerful in many aspects. well for development and quickly prototyping of new features, but it comes with a performance penalty. Depending on the system, when a file has above a thousand lines and syntax highlighting is enabled, editing will become - noticable slower. (The bottleneck is probably the ``BufferControl`` code, + noticeable slower. (The bottleneck is probably the ``BufferControl`` code, which on every key press tries to reflow the text and calls pygments for highlighting. And this is Python code looping through single characters.) - A lot of nice Vim features, like line folding, macros, etcetera are not yet @@ -145,6 +147,29 @@ Maybe we will also have line folding and probably block editing. Maybe some day we will have a built-in Python debugger or mouse support. We'll see. :) +Testing +------- + +To run all tests, install pytest: + + pip install pytest + +And then run from root pyvim directory: + + py.test + +To test pyvim against all supported python versions, install tox: + + pip install tox + +And then run from root pyvim directory: + + tox + +You need to have installed all the supported versions of python in order to run +tox command successfully. + + Why did I create Pyvim? ----------------------- @@ -176,6 +201,7 @@ Certainly have a look at the alternatives: - Kaa: https://github.com/kaaedit/kaa by @atsuoishimoto - Vai: https://github.com/stefanoborini/vai by @stefanoborini +- Vis: https://github.com/martanne/vis by @martanne Q & A: @@ -186,11 +212,6 @@ Q A No, it uses only ``prompt-toolkit``. -Q - Why Python? -A - The only alternative would be Haskell, but I still have to learn that. - Thanks ------ @@ -199,3 +220,7 @@ Thanks - To Jedi, pyflakes and the docopt Python libraries. - To the Python wcwidth port of Jeff Quast for support of double width characters. - To Guido van Rossum, for creating Python. + + +.. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/pyvim.svg?branch=master + :target: https://travis-ci.org/jonathanslenders/pyvim# diff --git a/examples/config/pyvimrc b/examples/config/pyvimrc index 6de61de..94a1703 100644 --- a/examples/config/pyvimrc +++ b/examples/config/pyvimrc @@ -2,6 +2,9 @@ """ Pyvim configuration. Save to file to: ~/.pyvimrc """ +from prompt_toolkit.application import run_in_terminal +from prompt_toolkit.filters import ViInsertMode +from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys from subprocess import call import six @@ -44,6 +47,15 @@ def configure(editor): # Add custom key bindings: + @editor.add_key_binding('j', 'j', filter=ViInsertMode()) + def _(event): + """ + Typing 'jj' in Insert mode, should go back to navigation mode. + + (imap jj ) + """ + event.cli.key_processor.feed(KeyPress(Keys.Escape)) + @editor.add_key_binding(Keys.F9) def save_and_execute_python_file(event): """ @@ -53,17 +65,17 @@ def configure(editor): editor_buffer = editor.current_editor_buffer if editor_buffer is not None: - if editor_buffer.filename is None: + if editor_buffer.location is None: editor.show_message("File doesn't have a filename. Please save first.") return else: editor_buffer.write() - + # Now run the Python interpreter. But use # `CommandLineInterface.run_in_terminal` to go to the background and # not destroy the window layout. def execute(): - call(['python', editor_buffer.filename]) + call(['python3', editor_buffer.location]) six.moves.input('Press enter to continue...') - editor.cli.run_in_terminal(execute) + run_in_terminal(execute) diff --git a/pyvim/__init__.py b/pyvim/__init__.py old mode 100755 new mode 100644 index c4c10f8..673f6eb --- a/pyvim/__init__.py +++ b/pyvim/__init__.py @@ -1,3 +1,3 @@ from __future__ import unicode_literals -__version__ = '0.0.3' +__version__ = '3.0.3' diff --git a/pyvim/__main__.py b/pyvim/__main__.py new file mode 100644 index 0000000..03a57a1 --- /dev/null +++ b/pyvim/__main__.py @@ -0,0 +1,7 @@ +""" +Make `python -m pyvim` an alias for running `pyvim`. +""" +from __future__ import unicode_literals +from .entry_points.run_pyvim import run + +run() diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 0736065..9cf174f 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals, print_function +from prompt_toolkit.application import run_in_terminal +from prompt_toolkit.document import Document + import os +import re import six -import sys __all__ = ( 'has_command_handler', @@ -220,8 +223,8 @@ def handler(): eb = info.editor_buffer print(' %3i %-2s %-20s line %i' % ( info.index, char, eb.location, (eb.buffer.document.cursor_position_row + 1))) - (input() if six.PY3 else raw_input)('\nPress ENTER to continue...') - editor.cli.run_in_terminal(handler) + six.moves.input('\nPress ENTER to continue...') + run_in_terminal(handler) @_cmd('b') @@ -273,6 +276,7 @@ def buffer_edit(editor, location, force=False): else: eb.reload() else: + editor.file_explorer = '' editor.window_arrangement.open_buffer(location, show_in_current_window=True) @@ -293,7 +297,7 @@ def quit(editor, all_=False, force=False): editor.show_message('%i more files to edit' % (len(ebs) - 1)) else: - editor.cli.set_return_value('') + editor.application.exit() @cmd('qa', accepts_force=True) @@ -327,7 +331,7 @@ def write_and_quit(editor, location, force=False): Write file and quit. """ write(editor, location, force=force) - editor.cli.set_return_value('') + editor.application.exit() @cmd('cq') @@ -337,8 +341,8 @@ def quit_nonzero(editor): """ # Note: the try/finally in `prompt_toolkit.Interface.read_input` # will ensure that the render output is reset, leaving the alternate - # screen before quiting. - sys.exit(1) + # screen before quitting. + editor.application.exit() @cmd('wqa') @@ -363,6 +367,8 @@ def help(editor): editor.show_help() +@location_cmd('tabe') +@location_cmd('tabedit') @location_cmd('tabnew') def tab_new(editor, location): """ @@ -372,6 +378,7 @@ def tab_new(editor, location): @cmd('tabclose') +@cmd('tabc') def tab_close(editor): """ Close tab page. @@ -380,6 +387,7 @@ def tab_close(editor): @cmd('tabnext') +@cmd('tabn') def tab_next(editor): """ Go to next tab. @@ -388,6 +396,7 @@ def tab_next(editor): @cmd('tabprevious') +@cmd('tabp') def tab_previous(editor): """ Go to previous tab. @@ -395,7 +404,24 @@ def tab_previous(editor): editor.window_arrangement.go_to_previous_tab() +@cmd('pwd') +def pwd(editor): + " Print working directory. " + directory = os.getcwd() + editor.show_message('{}'.format(directory)) + + +@location_cmd('cd', accepts_force=False) +def pwd(editor, location): + " Change working directory. " + try: + os.chdir(location) + except OSError as e: + editor.show_message('{}'.format(e)) + + @_cmd('colorscheme') +@_cmd('colo') def color_scheme(editor, variables): """ Go to one of the open buffers. @@ -420,12 +446,14 @@ def line_numbers_hide(editor): @set_cmd('hlsearch') +@set_cmd('hls') def search_highlight(editor): """ Highlight search matches. """ editor.highlight_search = True @set_cmd('nohlsearch') +@set_cmd('nohls') def search_no_highlight(editor): """ Don't highlight search matches. """ editor.highlight_search = False @@ -444,12 +472,14 @@ def paste_mode_leave(editor): @set_cmd('ruler') +@set_cmd('ru') def ruler_show(editor): """ Show ruler. """ editor.show_ruler = True @set_cmd('noruler') +@set_cmd('noru') def ruler_hide(editor): """ Hide ruler. """ editor.show_ruler = False @@ -522,24 +552,28 @@ def set_scroll_offset(editor, value): @set_cmd('incsearch') +@set_cmd('is') def incsearch_enable(editor): """ Enable incsearch. """ editor.incsearch = True @set_cmd('noincsearch') +@set_cmd('nois') def incsearch_disable(editor): """ Disable incsearch. """ editor.incsearch = False @set_cmd('ignorecase') +@set_cmd('ic') def search_ignorecase(editor): """ Enable case insensitive searching. """ editor.ignore_case = True @set_cmd('noignorecase') +@set_cmd('noic') def searc_no_ignorecase(editor): """ Disable case insensitive searching. """ editor.ignore_case = False @@ -567,3 +601,156 @@ def jedi_enable(editor): def jedi_disable(editor): """ Disable Jedi autocompletion. """ editor.enable_jedi = False + + +@set_cmd('relativenumber') +@set_cmd('rnu') +def relative_number(editor): + " Enable relative number " + editor.relative_number = True + + +@set_cmd('norelativenumber') +@set_cmd('nornu') +def no_relative_number(editor): + " Disable relative number " + editor.relative_number = False + + +@set_cmd('wrap') +def enable_wrap(editor): + " Enable line wrapping. " + editor.wrap_lines = True + + +@set_cmd('nowrap') +def disable_wrap(editor): + " disable line wrapping. " + editor.wrap_lines = False + + +@set_cmd('breakindent') +@set_cmd('bri') +def enable_breakindent(editor): + " Enable the breakindent option. " + editor.break_indent = True + + +@set_cmd('nobreakindent') +@set_cmd('nobri') +def disable_breakindent(editor): + " Enable the breakindent option. " + editor.break_indent = False + + +@set_cmd('mouse') +def enable_mouse(editor): + " Enable mouse . " + editor.enable_mouse_support = True + + +@set_cmd('nomouse') +def disable_mouse(editor): + " Disable mouse. " + editor.enable_mouse_support = False + + +@set_cmd('tildeop') +@set_cmd('top') +def enable_tildeop(editor): + " Enable tilde operator. " + editor.application.vi_state.tilde_operator = True + + +@set_cmd('notildeop') +@set_cmd('notop') +def disable_tildeop(editor): + " Disable tilde operator. " + editor.application.vi_state.tilde_operator = False + + +@set_cmd('cursorline') +@set_cmd('cul') +def enable_cursorline(editor): + " Highlight the line that contains the cursor. " + editor.cursorline = True + +@set_cmd('nocursorline') +@set_cmd('nocul') +def disable_cursorline(editor): + " No cursorline. " + editor.cursorline = False + +@set_cmd('cursorcolumn') +@set_cmd('cuc') +def enable_cursorcolumn(editor): + " Highlight the column that contains the cursor. " + editor.cursorcolumn = True + +@set_cmd('nocursorcolumn') +@set_cmd('nocuc') +def disable_cursorcolumn(editor): + " No cursorcolumn. " + editor.cursorcolumn = False + + +@set_cmd('colorcolumn', accepts_value=True) +@set_cmd('cc', accepts_value=True) +def set_scroll_offset(editor, value): + try: + if value: + numbers = [int(val) for val in value.split(',')] + else: + numbers = [] + except ValueError: + editor.show_message( + 'Invalid value. Expecting comma separated list of integers') + else: + editor.colorcolumn = numbers + + +def substitute(editor, range_start, range_end, search, replace, flags): + """ Substitute /search/ with /replace/ over a range of text """ + def get_line_index_iterator(cursor_position_row, range_start, range_end): + if not range_start: + assert not range_end + range_start = range_end = cursor_position_row + else: + range_start = int(range_start) - 1 + range_end = int(range_end) - 1 if range_end else range_start + return range(range_start, range_end + 1) + + def get_transform_callback(search, replace, flags): + SUBSTITUTE_ALL, SUBSTITUTE_ONE = 0, 1 + sub_count = SUBSTITUTE_ALL if 'g' in flags else SUBSTITUTE_ONE + return lambda s: re.sub(search, replace, s, count=sub_count) + + search_state = editor.application.current_search_state + buffer = editor.current_editor_buffer.buffer + cursor_position_row = buffer.document.cursor_position_row + + # read editor state + if not search: + search = search_state.text + + if replace is None: + replace = editor.last_substitute_text + + line_index_iterator = get_line_index_iterator(cursor_position_row, range_start, range_end) + transform_callback = get_transform_callback(search, replace, flags) + new_text = buffer.transform_lines(line_index_iterator, transform_callback) + + assert len(line_index_iterator) >= 1 + new_cursor_position_row = line_index_iterator[-1] + + # update text buffer + buffer.document = Document( + new_text, + Document(new_text).translate_row_col_to_index(new_cursor_position_row, 0), + ) + buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) + buffer._search(search_state, include_current_position=True) + + # update editor state + editor.last_substitute_text = replace + search_state.text = search diff --git a/pyvim/commands/completer.py b/pyvim/commands/completer.py index c57760a..cb2bc34 100644 --- a/pyvim/commands/completer.py +++ b/pyvim/commands/completer.py @@ -1,8 +1,7 @@ from __future__ import unicode_literals from prompt_toolkit.completion import Completer, Completion -from prompt_toolkit.contrib.completers.base import WordCompleter -from prompt_toolkit.contrib.completers.filesystem import PathCompleter +from prompt_toolkit.completion import WordCompleter, PathCompleter from prompt_toolkit.contrib.completers.system import SystemCompleter from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter @@ -20,7 +19,7 @@ def create_command_completer(editor): return GrammarCompleter(COMMAND_GRAMMAR, { 'command': WordCompleter(commands), 'location': PathCompleter(expanduser=True), - 'set_option': WordCompleter(SET_COMMANDS), + 'set_option': WordCompleter(sorted(SET_COMMANDS)), 'buffer_name': BufferNameCompleter(editor), 'colorscheme': ColorSchemeCompleter(editor), 'shell_command': SystemCompleter(), diff --git a/pyvim/commands/grammar.py b/pyvim/commands/grammar.py index bc3630e..1975395 100644 --- a/pyvim/commands/grammar.py +++ b/pyvim/commands/grammar.py @@ -11,6 +11,9 @@ :* \s* ( + # Substitute command + ((?P\d+)(,(?P\d+))?)? (?Ps|substitute) \s* / (?P[^/]*) ( / (?P[^/]*) (?P /(g)? )? )? | + # Commands accepting a location. (?P%(commands_taking_locations)s)(?P!?) \s+ (?P[^\s]+) | diff --git a/pyvim/commands/handler.py b/pyvim/commands/handler.py index 454663e..606f971 100644 --- a/pyvim/commands/handler.py +++ b/pyvim/commands/handler.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from .grammar import COMMAND_GRAMMAR -from .commands import call_command_handler, has_command_handler +from .commands import call_command_handler, has_command_handler, substitute __all__ = ( 'handle_command', @@ -21,6 +21,11 @@ def handle_command(editor, input_string): command = variables.get('command') go_to_line = variables.get('go_to_line') shell_command = variables.get('shell_command') + range_start = variables.get('range_start') + range_end = variables.get('range_end') + search = variables.get('search') + replace = variables.get('replace') + flags = variables.get('flags', '') # Call command handler. @@ -30,11 +35,16 @@ def handle_command(editor, input_string): elif shell_command is not None: # Handle shell commands. - editor.cli.run_system_command(shell_command) + editor.application.run_system_command(shell_command) elif has_command_handler(command): # Handle other 'normal' commands. call_command_handler(command, editor, variables) + + elif command in ('s', 'substitute'): + flags = flags.lstrip('/') + substitute(editor, range_start, range_end, search, replace, flags) + else: # For unknown commands, show error message. editor.show_message('Not an editor command: %s' % input_string) @@ -49,5 +59,5 @@ def _go_to_line(editor, line): """ Move cursor to this line in the current buffer. """ - b = editor.cli.current_buffer + b = editor.application.current_buffer b.cursor_position = b.document.translate_row_col_to_index(max(0, int(line) - 1), 0) diff --git a/pyvim/commands/lexer.py b/pyvim/commands/lexer.py index a2cd6e4..2dd7807 100644 --- a/pyvim/commands/lexer.py +++ b/pyvim/commands/lexer.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer +from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer -from pygments.token import Token from pygments.lexers import BashLexer from .grammar import COMMAND_GRAMMAR @@ -15,9 +15,8 @@ def create_command_lexer(): """ Lexer for highlighting of the command line. """ - return GrammarLexer(COMMAND_GRAMMAR, tokens={ - 'command': Token.CommandLine.Command, - 'location': Token.CommandLine.Location, - }, lexers={ - 'shell_command': BashLexer, + return GrammarLexer(COMMAND_GRAMMAR, lexers={ + 'command': SimpleLexer('class:commandline.command'), + 'location': SimpleLexer('class:commandline.location'), + 'shell_command': PygmentsLexer(BashLexer), }) diff --git a/pyvim/commands/preview.py b/pyvim/commands/preview.py index 5fcd421..a9fc4d1 100644 --- a/pyvim/commands/preview.py +++ b/pyvim/commands/preview.py @@ -19,10 +19,14 @@ def save(self): """ e = self.editor - self._style = e.cli.style + self._style = e.current_style self._show_line_numbers = e.show_line_numbers self._highlight_search = e.highlight_search self._show_ruler = e.show_ruler + self._relative_number = e.relative_number + self._cursorcolumn = e.cursorcolumn + self._cursorline = e.cursorline + self._colorcolumn = e.colorcolumn def restore(self): """ @@ -30,10 +34,14 @@ def restore(self): """ e = self.editor - e.cli.style = self._style + e.current_style = self._style e.show_line_numbers = self._show_line_numbers e.highlight_search = self._highlight_search e.show_ruler = self._show_ruler + e.relative_number = self._relative_number + e.cursorcolumn = self._cursorcolumn + e.cursorline = self._cursorline + e.colorcolumn = self._colorcolumn def preview(self, input_string): """ @@ -65,15 +73,32 @@ def _apply(self, input_string): # Preview some set commands. if command == 'set': - if set_option == 'hlsearch': + if set_option in ('hlsearch', 'hls'): e.highlight_search = True - elif set_option == 'nohlsearch': + elif set_option in ('nohlsearch', 'nohls'): e.highlight_search = False elif set_option in ('nu', 'number'): e.show_line_numbers = True elif set_option in ('nonu', 'nonumber'): e.show_line_numbers = False - elif set_option == 'ruler': + elif set_option in ('ruler', 'ru'): e.show_ruler = True - elif set_option == 'noruler': + elif set_option in ('noruler', 'noru'): e.show_ruler = False + elif set_option in ('relativenumber', 'rnu'): + e.relative_number = True + elif set_option in ('norelativenumber', 'nornu'): + e.relative_number = False + elif set_option in ('cursorline', 'cul'): + e.cursorline = True + elif set_option in ('cursorcolumn', 'cuc'): + e.cursorcolumn = True + elif set_option in ('nocursorline', 'nocul'): + e.cursorline = False + elif set_option in ('nocursorcolumn', 'nocuc'): + e.cursorcolumn = False + elif set_option in ('colorcolumn', 'cc'): + value = variables.get('set_value', '') + if value: + e.colorcolumn = [ + int(v) for v in value.split(',') if v.isdigit()] diff --git a/pyvim/completion.py b/pyvim/completion.py index 6d21e45..50a18ad 100644 --- a/pyvim/completion.py +++ b/pyvim/completion.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from prompt_toolkit.completion import Completer, Completion -from ptpython.completer import PythonCompleter import re import weakref @@ -47,9 +46,75 @@ def get_completions(self, document, complete_event): # Select completer. if location.endswith('.py') and editor.enable_jedi: - completer = PythonCompleter(lambda: globals(), lambda: {}) + completer = _PythonCompleter(location) else: completer = DocumentWordsCompleter() # Call completer. return completer.get_completions(document, complete_event) + + +class _PythonCompleter(Completer): + """ + Wrapper around the Jedi completion engine. + """ + def __init__(self, location): + self.location = location + + def get_completions(self, document, complete_event): + script = self._get_jedi_script_from_document(document) + if script: + try: + completions = script.completions() + except TypeError: + # Issue #9: bad syntax causes completions() to fail in jedi. + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 + pass + except UnicodeDecodeError: + # Issue #43: UnicodeDecodeError on OpenBSD + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43 + pass + except AttributeError: + # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513 + pass + except ValueError: + # Jedi issue: "ValueError: invalid \x escape" + pass + except KeyError: + # Jedi issue: "KeyError: u'a_lambda'." + # https://github.com/jonathanslenders/ptpython/issues/89 + pass + except IOError: + # Jedi issue: "IOError: No such file or directory." + # https://github.com/jonathanslenders/ptpython/issues/71 + pass + else: + for c in completions: + yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), + display=c.name_with_symbols) + + def _get_jedi_script_from_document(self, document): + import jedi # We keep this import in-line, to improve start-up time. + # Importing Jedi is 'slow'. + + try: + return jedi.Script( + document.text, + column=document.cursor_position_col, + line=document.cursor_position_row + 1, + path=self.location) + except ValueError: + # Invalid cursor position. + # ValueError('`column` parameter is not in a valid range.') + return None + except AttributeError: + # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65 + # See also: https://github.com/davidhalter/jedi/issues/508 + return None + except IndexError: + # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 + return None + except KeyError: + # Workaround for a crash when the input is "u'", the start of a unicode string. + return None + diff --git a/pyvim/editor.py b/pyvim/editor.py index c798323..842df37 100644 --- a/pyvim/editor.py +++ b/pyvim/editor.py @@ -9,23 +9,20 @@ """ from __future__ import unicode_literals -from prompt_toolkit.buffer import Buffer, AcceptAction -from prompt_toolkit.shortcuts import create_eventloop -from prompt_toolkit.enums import SEARCH_BUFFER -from prompt_toolkit.filters import Always, Condition +from prompt_toolkit.application import Application +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.filters import Condition from prompt_toolkit.history import FileHistory -from prompt_toolkit.interface import CommandLineInterface, AbortAction from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.styles import DynamicStyle from .commands.completer import create_command_completer from .commands.handler import handle_command from .commands.preview import CommandPreviewer -from .editor_buffer import EditorBuffer -from .enums import COMMAND_BUFFER from .help import HELP_TEXT from .key_bindings import create_key_bindings -from .layout import EditorLayout -from .reporting import report +from .layout import EditorLayout, get_terminal_title from .style import generate_built_in_styles, get_editor_style_by_name from .window_arrangement import WindowArrangement from .io import FileIO, DirectoryIO, HttpIO, GZipFileIO @@ -41,8 +38,15 @@ class Editor(object): """ The main class. Containing the whole editor. + + :param config_directory: Place where configuration is stored. + :param input: (Optionally) `prompt_toolkit.input.Input` object. + :param output: (Optionally) `prompt_toolkit.output.Output` object. """ - def __init__(self, config_directory='~/.pyvim'): + def __init__(self, config_directory='~/.pyvim', input=None, output=None): + self.input = input + self.output = output + # Vi options. self.show_line_numbers = True self.highlight_search = True @@ -53,21 +57,28 @@ def __init__(self, config_directory='~/.pyvim'): self.tabstop = 4 # Number of spaces that a tab character represents. self.incsearch = True # Show matches while typing search string. self.ignore_case = False # Ignore case while searching. + self.enable_mouse_support = True self.display_unprintable_characters = True # ':set list' self.enable_jedi = True # ':set jedi', for Python Jedi completion. self.scroll_offset = 0 # ':set scrolloff' + self.relative_number = False # ':set relativenumber' + self.wrap_lines = True # ':set wrap' + self.break_indent = False # ':set breakindent' + self.cursorline = False # ':set cursorline' + self.cursorcolumn = False # ':set cursorcolumn' + self.colorcolumn = [] # ':set colorcolumn'. List of integers. # Ensure config directory exists. self.config_directory = os.path.abspath(os.path.expanduser(config_directory)) if not os.path.exists(self.config_directory): os.mkdir(self.config_directory) - self._reporters_running_for_buffer_names = set() self.window_arrangement = WindowArrangement(self) self.message = None # Load styles. (Mapping from name to Style class.) self.styles = generate_built_in_styles() + self.current_style = get_editor_style_by_name('vim') # I/O backends. self.io_backends = [ @@ -77,28 +88,49 @@ def __init__(self, config_directory='~/.pyvim'): FileIO(), ] - # Create eventloop. - self.eventloop = create_eventloop() + # Create history and search buffers. + def handle_action(buff): + ' When enter is pressed in the Vi command line. ' + text = buff.text # Remember: leave_command_mode resets the buffer. + + # First leave command mode. We want to make sure that the working + # pane is focussed again before executing the command handlers. + self.leave_command_mode(append_to_history=True) + + # Execute command. + handle_command(self, text) + + commands_history = FileHistory(os.path.join(self.config_directory, 'commands_history')) + self.command_buffer = Buffer( + accept_handler=handle_action, + enable_history_search=True, + completer=create_command_completer(self), + history=commands_history, + multiline=False) + + search_buffer_history = FileHistory(os.path.join(self.config_directory, 'search_history')) + self.search_buffer = Buffer( + history=search_buffer_history, + enable_history_search=True, + multiline=False) - # Create key bindings manager - self.key_bindings_manager = create_key_bindings(self) + # Create key bindings registry. + self.key_bindings = create_key_bindings(self) # Create layout and CommandLineInterface instance. - self.editor_layout = EditorLayout( - self, self.key_bindings_manager, self.window_arrangement) - self.cli = self._create_cli() + self.editor_layout = EditorLayout(self, self.window_arrangement) + self.application = self._create_application() # Hide message when a key is pressed. - def key_pressed(): + def key_pressed(_): self.message = None - self.cli.input_processor.beforeKeyPress += key_pressed - - # Call reporter when input changes. - self.cli.onBufferChanged += self._current_buffer_changed + self.application.key_processor.before_key_press += key_pressed # Command line previewer. self.previewer = CommandPreviewer(self) + self.last_substitute_text = '' + def load_initial_files(self, locations, in_tab_pages=False, hsplit=False, vsplit=False): """ Load a list of files. @@ -126,67 +158,46 @@ def load_initial_files(self, locations, in_tab_pages=False, hsplit=False, vsplit if locations and len(locations) > 1: self.show_message('%i files loaded.' % len(locations)) - def _create_cli(self): + def _create_application(self): """ Create CommandLineInterface instance. """ - # Create Vi command buffer. - def handle_action(cli, buffer): - ' When enter is pressed in the Vi command line. ' - text = buffer.text # Remember: leave_command_mode resets the buffer. - - # First leave command mode. We want to make sure that the working - # pane is focussed again before executing the command handlers. - self.leave_command_mode(append_to_history=True) - - # Execute command. - handle_command(self, text) - - # Create history and search buffers. - commands_history = FileHistory(os.path.join(self.config_directory, 'commands_history')) - command_buffer = Buffer(accept_action=AcceptAction(handler=handle_action), - enable_history_search=Always(), - completer=create_command_completer(self), - history=commands_history) - - search_buffer_history = FileHistory(os.path.join(self.config_directory, 'search_history')) - search_buffer = Buffer(history=search_buffer_history, - enable_history_search=Always(), - accept_action=AcceptAction.IGNORE) - - # Create CLI. - cli = CommandLineInterface( - eventloop=self.eventloop, + # Create Application. + application = Application( + input=self.input, + output=self.output, + editing_mode=EditingMode.VI, layout=self.editor_layout.layout, - key_bindings_registry=self.key_bindings_manager.registry, - buffers={ - COMMAND_BUFFER: command_buffer, - SEARCH_BUFFER: search_buffer, - }, - style=get_editor_style_by_name('default'), - paste_mode=Condition(lambda cli: self.paste_mode), - ignore_case=Condition(lambda cli: self.ignore_case), - use_alternate_screen=True, - on_abort=AbortAction.IGNORE, - on_exit=AbortAction.IGNORE) + key_bindings=self.key_bindings, +# get_title=lambda: get_terminal_title(self), + style=DynamicStyle(lambda: self.current_style), + paste_mode=Condition(lambda: self.paste_mode), +# ignore_case=Condition(lambda: self.ignore_case), # TODO + include_default_pygments_style=False, + mouse_support=Condition(lambda: self.enable_mouse_support), + full_screen=True, + enable_page_navigation_bindings=True) # Handle command line previews. # (e.g. when typing ':colorscheme blue', it should already show the # preview before pressing enter.) - def preview(): - if cli.current_buffer == command_buffer: - self.previewer.preview(command_buffer.text) - command_buffer.onTextChanged += preview + def preview(_): + if self.application.layout.has_focus(self.command_buffer): + self.previewer.preview(self.command_buffer.text) + self.command_buffer.on_text_changed += preview - return cli + return application @property def current_editor_buffer(self): """ Return the `EditorBuffer` that is currently active. """ + current_buffer = self.application.current_buffer + + # Find/return the EditorBuffer with this name. for b in self.window_arrangement.editor_buffers: - if b.buffer_name == self.cli.current_buffer_name: + if b.buffer == current_buffer: return b @property @@ -196,7 +207,7 @@ def add_key_binding(self): (Mostly useful for a pyvimrc file, that receives this Editor instance as input.) """ - return self.key_bindings_manager.registry.add_binding + return self.key_bindings.add def show_message(self, message): """ @@ -210,11 +221,9 @@ def use_colorscheme(self, name='default'): Apply new colorscheme. (By name.) """ try: - style = get_editor_style_by_name(name) + self.current_style = get_editor_style_by_name(name) except pygments.util.ClassNotFound: pass - else: - self.cli.style = style def sync_with_prompt_toolkit(self): """ @@ -226,58 +235,9 @@ def sync_with_prompt_toolkit(self): # Make sure that the focus stack of prompt-toolkit has the current # page. - self.cli.focus_stack._stack = [ - self.window_arrangement.active_editor_buffer.buffer_name] - - def _current_buffer_changed(self): - """ - Current buffer changed. - """ - name = self.cli.current_buffer_name - eb = self.window_arrangement.get_editor_buffer_for_buffer_name(name) - - if eb is not None: - # Run reporter. - self.run_reporter_for_editor_buffer(eb) - - def run_reporter_for_editor_buffer(self, editor_buffer): - """ - Run reporter on input. (Asynchronously.) - """ - assert isinstance(editor_buffer, EditorBuffer) - eb = editor_buffer - name = eb.buffer_name - - if name not in self._reporters_running_for_buffer_names: - text = eb.buffer.text - self._reporters_running_for_buffer_names.add(name) - - # Don't run reporter when we don't have a location. (We need to - # know the filetype, actually.) - if eb.location is None: - return - - # Better not to access the document in an executor. - document = eb.buffer.document - - def in_executor(): - # Call reporter - report_errors = report(eb.location, document) - - def ready(): - self._reporters_running_for_buffer_names.remove(name) - - # If the text has not been changed yet in the meantime, set - # reporter errors. (We were running in another thread.) - if text == eb.buffer.text: - eb.report_errors = report_errors - self.cli._redraw() - else: - # Restart reporter when the text was changed. - self._current_buffer_changed() - - self.cli.eventloop.call_from_executor(ready) - self.cli.eventloop.run_in_executor(in_executor) + window = self.window_arrangement.active_pt_window + if window: + self.application.layout.focus(window) def show_help(self): """ @@ -294,15 +254,19 @@ def run(self): # Make sure everything is in sync, before starting. self.sync_with_prompt_toolkit() + def pre_run(): + # Start in navigation mode. + self.application.vi_state.input_mode = InputMode.NAVIGATION + # Run eventloop of prompt_toolkit. - self.cli.read_input(reset_current_buffer=False) + self.application.run(pre_run=pre_run) def enter_command_mode(self): """ Go into command mode. """ - self.cli.focus_stack.push(COMMAND_BUFFER) - self.key_bindings_manager.vi_state.input_mode = InputMode.INSERT + self.application.layout.focus(self.command_buffer) + self.application.vi_state.input_mode = InputMode.INSERT self.previewer.save() @@ -312,7 +276,7 @@ def leave_command_mode(self, append_to_history=False): """ self.previewer.restore() - self.cli.focus_stack.pop() - self.key_bindings_manager.vi_state.input_mode = InputMode.NAVIGATION + self.application.layout.focus_last() + self.application.vi_state.input_mode = InputMode.NAVIGATION - self.cli.buffers[COMMAND_BUFFER].reset(append_to_history=append_to_history) + self.command_buffer.reset(append_to_history=append_to_history) diff --git a/pyvim/editor_buffer.py b/pyvim/editor_buffer.py index 994fc4e..76f7d1e 100644 --- a/pyvim/editor_buffer.py +++ b/pyvim/editor_buffer.py @@ -1,15 +1,24 @@ from __future__ import unicode_literals +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document +from prompt_toolkit import __version__ as ptk_version -from prompt_toolkit.buffer import Buffer, AcceptAction -from prompt_toolkit.filters import Always from pyvim.completion import DocumentCompleter +from pyvim.reporting import report from six import string_types import os import weakref +PTK3 = ptk_version.startswith('3.') + +if PTK3: + from asyncio import get_event_loop +else: + from prompt_toolkit.eventloop import call_from_executor, run_in_executor + __all__ = ( 'EditorBuffer', ) @@ -22,20 +31,21 @@ class EditorBuffer(object): A 'prompt-toolkit' `Buffer` doesn't know anything about files, changes, etc... This wrapper contains the necessary data for the editor. """ - def __init__(self, editor, buffer_name, location=None, text=None): - assert isinstance(buffer_name, string_types) + def __init__(self, editor, location=None, text=None): assert location is None or isinstance(location, string_types) assert text is None or isinstance(text, string_types) assert not (location and text) self._editor_ref = weakref.ref(editor) - self.buffer_name = buffer_name self.location = location self.encoding = 'utf-8' #: is_new: True when this file does not yet exist in the storage. self.is_new = True + # Empty if not in file explorer mode, directory path otherwise. + self.isdir = False + # Read text. if location: text = self._read(location) @@ -46,13 +56,14 @@ def __init__(self, editor, buffer_name, location=None, text=None): # Create Buffer. self.buffer = Buffer( - is_multiline=Always(), + multiline=True, completer=DocumentCompleter(editor, self), - initial_document=Document(text, 0), - accept_action=AcceptAction.IGNORE) + document=Document(text, 0), + on_text_changed=lambda _: self.run_reporter()) # List of reporting errors. self.report_errors = [] + self._reporter_is_running = False @property def editor(self): @@ -66,6 +77,13 @@ def has_unsaved_changes(self): """ return self._file_content != self.buffer.text + @property + def in_file_explorer_mode(self): + """ + True when we are in file explorer mode (when this is a directory). + """ + return self.isdir + def _read(self, location): """ Read file I/O backend. @@ -74,12 +92,17 @@ def _read(self, location): if io.can_open_location(location): # Found an I/O backend. exists = io.exists(location) + self.isdir = io.isdir(location) + if exists in (True, NotImplemented): # File could exist. Read it. self.is_new = False try: text, self.encoding = io.read(location) + # Replace \r\n by \n. + text = text.replace('\r\n', '\n') + # Drop trailing newline while editing. # (prompt-toolkit doesn't enforce the trailing newline.) if text.endswith('\n'): @@ -146,6 +169,49 @@ def get_display_name(self, short=False): return self.location def __repr__(self): - return '%s(buffer_name=%r, buffer=%r)' % ( - self.__class__.__name__, - self.buffer_name, self.buffer) + return '%s(buffer=%r)' % (self.__class__.__name__, self.buffer) + + def run_reporter(self): + " Buffer text changed. " + if not self._reporter_is_running: + self._reporter_is_running = True + + text = self.buffer.text + self.report_errors = [] + + # Don't run reporter when we don't have a location. (We need to + # know the filetype, actually.) + if self.location is None: + return + + # Better not to access the document in an executor. + document = self.buffer.document + + if PTK3: + loop = get_event_loop() + + def in_executor(): + # Call reporter + report_errors = report(self.location, document) + + def ready(): + self._reporter_is_running = False + + # If the text has not been changed yet in the meantime, set + # reporter errors. (We were running in another thread.) + if text == self.buffer.text: + self.report_errors = report_errors + get_app().invalidate() + else: + # Restart reporter when the text was changed. + self.run_reporter() + + if PTK3: + loop.call_soon_threadsafe(ready) + else: + call_from_executor(ready) + + if PTK3: + loop.run_in_executor(None, in_executor) + else: + run_in_executor(in_executor) diff --git a/pyvim/enums.py b/pyvim/enums.py index 8f65f35..baffc48 100644 --- a/pyvim/enums.py +++ b/pyvim/enums.py @@ -1,4 +1 @@ from __future__ import unicode_literals - -# Vim command line buffer. -COMMAND_BUFFER = 'command-buffer' diff --git a/pyvim/io/backends.py b/pyvim/io/backends.py index f0ffc78..b1a8f0a 100644 --- a/pyvim/io/backends.py +++ b/pyvim/io/backends.py @@ -28,7 +28,7 @@ def can_open_location(cls, location): return '://' not in location and not os.path.isdir(location) def exists(self, location): - return os.path.exists(location) + return os.path.exists(os.path.expanduser(location)) def read(self, location): """ @@ -115,7 +115,10 @@ def read(self, directory): result.append('" ==================================\n') result.append('" Directory Listing\n') result.append('" %s\n' % os.path.abspath(directory)) + result.append('" Quick help: -: go up dir\n') result.append('" ==================================\n') + result.append('../\n') + result.append('./\n') for d in directories: result.append('%s/\n' % d) @@ -128,6 +131,9 @@ def read(self, directory): def write(self, location, text, encoding): raise NotImplementedError('Cannot write to directory.') + def isdir(self, location): + return True + class HttpIO(EditorIO): """ diff --git a/pyvim/io/base.py b/pyvim/io/base.py index 830a099..f1ca302 100644 --- a/pyvim/io/base.py +++ b/pyvim/io/base.py @@ -44,3 +44,9 @@ def write(self, location, data, encoding='utf-8'): Write file to storage. Can raise IOError. """ + + def isdir(self, location): + """ + Return whether this location is a directory. + """ + return False diff --git a/pyvim/key_bindings.py b/pyvim/key_bindings.py index 9b62a9d..e3209ee 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -1,14 +1,10 @@ from __future__ import unicode_literals -from prompt_toolkit.filters import Condition, HasFocus, Filter -from prompt_toolkit.key_binding.bindings.utils import create_handle_decorator -from prompt_toolkit.key_binding.bindings.vi import ViStateFilter -from prompt_toolkit.key_binding.manager import KeyBindingManager -from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.utils import find_window_for_buffer_name +from prompt_toolkit.application import get_app +from prompt_toolkit.filters import Condition, has_focus, vi_insert_mode, vi_navigation_mode +from prompt_toolkit.key_binding import KeyBindings -from .enums import COMMAND_BUFFER +import os __all__ = ( 'create_key_bindings', @@ -19,7 +15,7 @@ def _current_window_for_event(event): """ Return the `Window` for the currently focussed Buffer. """ - return find_window_for_buffer_name(event.cli.layout, event.cli.current_buffer_name) + return event.app.layout.current_window def create_key_bindings(editor): @@ -29,155 +25,20 @@ def create_key_bindings(editor): This starts with the key bindings, defined by `prompt-toolkit`, but adds the ones which are specific for the editor. """ - # Create new Key binding manager. - manager = KeyBindingManager(enable_vi_mode=True, enable_system_prompt=True) - manager.vi_state.input_mode = InputMode.NAVIGATION + kb = KeyBindings() # Filters. - vi_buffer_focussed = Condition(lambda cli: cli.current_buffer_name.startswith('buffer-')) + @Condition + def vi_buffer_focussed(): + app = get_app() + if app.layout.has_focus(editor.search_buffer) or app.layout.has_focus(editor.command_buffer): + return False + return True - in_insert_mode = (ViStateFilter(manager.vi_state, InputMode.INSERT) & vi_buffer_focussed) - in_navigation_mode = (ViStateFilter(manager.vi_state, InputMode.NAVIGATION) & - vi_buffer_focussed) + in_insert_mode = vi_insert_mode & vi_buffer_focussed + in_navigation_mode = vi_navigation_mode & vi_buffer_focussed - # Decorator. - handle = create_handle_decorator(manager.registry) - - @handle(Keys.ControlF) - def scroll_forward(event, half=False): - """ - Scroll window down. - """ - w = _current_window_for_event(event) - b = event.cli.current_buffer - - if w and w.render_info: - # Determine height to move. - shift = w.render_info.rendered_height - if half: - shift = int(shift / 2) - - # Scroll. - new_document_line = min( - b.document.line_count - 1, - b.document.cursor_position_row + int(shift)) - b.cursor_position = b.document.translate_row_col_to_index(new_document_line, 0) - w.vertical_scroll = w.render_info.input_line_to_screen_line(new_document_line) - - @handle(Keys.ControlB) - def scroll_backward(event, half=False): - """ - Scroll window up. - """ - w = _current_window_for_event(event) - b = event.cli.current_buffer - - if w and w.render_info: - # Determine height to move. - shift = w.render_info.rendered_height - if half: - shift = int(shift / 2) - - # Scroll. - new_document_line = max(0, b.document.cursor_position_row - int(shift)) - b.cursor_position = b.document.translate_row_col_to_index(new_document_line, 0) - w.vertical_scroll = w.render_info.input_line_to_screen_line(new_document_line) - - @handle(Keys.ControlD) - def scroll_half_page_down(event): - """ - Same as ControlF, but only scroll half a page. - """ - scroll_forward(event, half=True) - - @handle(Keys.ControlU) - def scroll_half_page_up(event): - """ - Same as ControlB, but only scroll half a page. - """ - scroll_backward(event, half=True) - - @handle(Keys.ControlE) - def scroll_one_line_down(event): - """ - scroll_offset += 1 - """ - w = find_window_for_buffer_name(event.cli.layout, event.cli.current_buffer_name) - b = event.cli.current_buffer - - if w: - # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - if info.cursor_position.y <= info.configured_scroll_offset: - b.cursor_position += b.document.get_cursor_down_position() - - w.vertical_scroll += 1 - - @handle(Keys.ControlY) - def scroll_one_line_up(event): - """ - scroll_offset -= 1 - """ - w = find_window_for_buffer_name(event.cli.layout, event.cli.current_buffer_name) - b = event.cli.current_buffer - - if w: - # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - - if info.cursor_position.y >= info.rendered_height - 1 - info.configured_scroll_offset: - b.cursor_position += b.document.get_cursor_up_position() - - # Scroll window - w.vertical_scroll -= 1 - - @handle(Keys.PageDown) - def scroll_page_down(event): - """ - Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) - """ - w = _current_window_for_event(event) - b = event.cli.current_buffer - - if w and w.render_info: - # Scroll down one page. - w.vertical_scroll += w.render_info.rendered_height - - # Put cursor at the top of the visible region. - try: - new_document_line = w.render_info.screen_line_to_input_line[w.vertical_scroll] - except KeyError: - new_document_line = b.document.line_count - 1 - - b.cursor_position = b.document.translate_row_col_to_index(new_document_line, 0) - b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True) - - @handle(Keys.PageUp) - def scroll_page_up(event): - """ - Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) - """ - w = _current_window_for_event(event) - b = event.cli.current_buffer - - if w and w.render_info: - # Scroll down one page. - w.vertical_scroll = max(0, w.vertical_scroll - w.render_info.rendered_height) - - # Put cursor at the bottom of the visible region. - try: - new_document_line = w.render_info.screen_line_to_input_line[ - w.vertical_scroll + w.render_info.rendered_height - 1] - except KeyError: - new_document_line = 0 - - b.cursor_position = min(b.cursor_position, - b.document.translate_row_col_to_index(new_document_line, 0)) - b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True) - - @handle(Keys.ControlT) + @kb.add('c-t') def _(event): """ Override default behaviour of prompt-toolkit. @@ -186,12 +47,12 @@ def _(event): """ pass - @handle(Keys.ControlT, filter=in_insert_mode) + @kb.add('c-t', filter=in_insert_mode) def indent_line(event): """ Indent current line. """ - b = event.cli.current_buffer + b = event.application.current_buffer # Move to start of line. pos = b.document.get_start_of_line_position(after_whitespace=True) @@ -206,50 +67,49 @@ def indent_line(event): # Restore cursor. b.cursor_position -= pos - @handle(Keys.ControlR, filter=in_navigation_mode, save_before=False) + @kb.add('c-r', filter=in_navigation_mode, save_before=(lambda e: False)) def redo(event): """ Redo. """ - event.cli.current_buffer.redo() + event.app.current_buffer.redo() - @handle(':', filter=in_navigation_mode) + @kb.add(':', filter=in_navigation_mode) def enter_command_mode(event): """ Entering command mode. """ editor.enter_command_mode() - @handle(Keys.Tab, filter=ViStateFilter(manager.vi_state, InputMode.INSERT) & - ~HasFocus(COMMAND_BUFFER) & WhitespaceBeforeCursorOnLine()) + @kb.add('tab', filter=vi_insert_mode & + ~has_focus(editor.command_buffer) & whitespace_before_cursor_on_line) def autocomplete_or_indent(event): """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. """ - b = event.cli.current_buffer + b = event.app.current_buffer if editor.expand_tab: b.insert_text(' ') else: b.insert_text('\t') - @handle(Keys.Escape, filter=HasFocus(COMMAND_BUFFER)) - @handle(Keys.ControlC, filter=HasFocus(COMMAND_BUFFER)) - @handle( - Keys.Backspace, - filter=HasFocus(COMMAND_BUFFER) & Condition(lambda cli: cli.buffers[COMMAND_BUFFER].text == '')) + @kb.add('escape', filter=has_focus(editor.command_buffer)) + @kb.add('c-c', filter=has_focus(editor.command_buffer)) + @kb.add('backspace', + filter=has_focus(editor.command_buffer) & Condition(lambda: editor.command_buffer.text == '')) def leave_command_mode(event): """ Leaving command mode. """ editor.leave_command_mode() - @handle(Keys.ControlW, Keys.ControlW, filter=in_navigation_mode) + @kb.add('c-w', 'c-w', filter=in_navigation_mode) def focus_next_window(event): editor.window_arrangement.cycle_focus() editor.sync_with_prompt_toolkit() - @handle(Keys.ControlW, 'n', filter=in_navigation_mode) + @kb.add('c-w', 'n', filter=in_navigation_mode) def horizontal_split(event): """ Split horizontally. @@ -257,7 +117,7 @@ def horizontal_split(event): editor.window_arrangement.hsplit(None) editor.sync_with_prompt_toolkit() - @handle(Keys.ControlW, 'v', filter=in_navigation_mode) + @kb.add('c-w', 'v', filter=in_navigation_mode) def vertical_split(event): """ Split vertically. @@ -265,37 +125,57 @@ def vertical_split(event): editor.window_arrangement.vsplit(None) editor.sync_with_prompt_toolkit() - @handle('g', 't', filter=in_navigation_mode) + @kb.add('g', 't', filter=in_navigation_mode) def focus_next_tab(event): editor.window_arrangement.go_to_next_tab() editor.sync_with_prompt_toolkit() - @handle('g', 'T', filter=in_navigation_mode) + @kb.add('g', 'T', filter=in_navigation_mode) def focus_previous_tab(event): editor.window_arrangement.go_to_previous_tab() editor.sync_with_prompt_toolkit() - @handle(Keys.ControlJ, filter=in_navigation_mode) - def goto_line_beginning(event): - """ Enter in navigation mode should move to the start of the next line. """ - b = event.current_buffer - b.cursor_down(count=event.arg) - b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True) - - @handle(Keys.F1) + @kb.add('f1') def show_help(event): editor.show_help() - return manager + @Condition + def in_file_explorer_mode(): + return bool(editor.current_editor_buffer and + editor.current_editor_buffer.in_file_explorer_mode) + + @kb.add('enter', filter=in_file_explorer_mode) + def open_path(event): + """ + Open file/directory in file explorer mode. + """ + name_under_cursor = event.current_buffer.document.current_line + new_path = os.path.normpath(os.path.join( + editor.current_editor_buffer.location, name_under_cursor)) + + editor.window_arrangement.open_buffer( + new_path, show_in_current_window=True) + editor.sync_with_prompt_toolkit() + + @kb.add('-', filter=in_file_explorer_mode) + def to_parent_directory(event): + new_path = os.path.normpath(os.path.join( + editor.current_editor_buffer.location, '..')) + + editor.window_arrangement.open_buffer( + new_path, show_in_current_window=True) + editor.sync_with_prompt_toolkit() + + return kb -class WhitespaceBeforeCursorOnLine(Filter): +@Condition +def whitespace_before_cursor_on_line(): """ Filter which evaluates to True when the characters before the cursor are whitespace, or we are at the start of te line. """ - def __call__(self, cli): - b = cli.current_buffer - before_cursor = b.document.current_line_before_cursor + b = get_app().current_buffer + before_cursor = b.document.current_line_before_cursor - return bool(not before_cursor or before_cursor[-1].isspace()) + return bool(not before_cursor or before_cursor[-1].isspace()) diff --git a/pyvim/layout.py b/pyvim/layout.py index 6b09dd0..f7bc2be 100644 --- a/pyvim/layout.py +++ b/pyvim/layout.py @@ -2,37 +2,52 @@ The actual layout for the renderer. """ from __future__ import unicode_literals -from prompt_toolkit.filters import HasFocus, HasSearch, Condition, HasArg, Always +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import has_focus, is_searching, Condition, has_arg from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.layout import HSplit, VSplit, FloatContainer, Float -from prompt_toolkit.layout.containers import Window -from prompt_toolkit.layout.controls import BufferControl, FillControl -from prompt_toolkit.layout.controls import TokenListControl -from prompt_toolkit.layout.dimension import LayoutDimension +from prompt_toolkit.layout import HSplit, VSplit, FloatContainer, Float, Layout +from prompt_toolkit.layout.containers import Window, ConditionalContainer, ColorColumn, WindowAlign, ScrollOffsets +from prompt_toolkit.layout.controls import BufferControl +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.layout.dimension import Dimension +from prompt_toolkit.layout.margins import ConditionalMargin, NumberedMargin from prompt_toolkit.layout.menus import CompletionsMenu -from prompt_toolkit.layout.processors import Processor, HighlightSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, ConditionalProcessor, BeforeInput -from prompt_toolkit.layout.screen import Char -from prompt_toolkit.layout.toolbars import TokenListToolbar, SystemToolbar, SearchToolbar, ValidationToolbar, CompletionsToolbar +from prompt_toolkit.layout.processors import Processor, ConditionalProcessor, BeforeInput, ShowTrailingWhiteSpaceProcessor, Transformation, HighlightSelectionProcessor, HighlightSearchProcessor, HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, TabsProcessor, DisplayMultipleCursors +from prompt_toolkit.layout.utils import explode_text_fragments +from prompt_toolkit.mouse_events import MouseEventType from prompt_toolkit.selection import SelectionType -from prompt_toolkit.reactive import Integer - -from pygments.token import Token +from prompt_toolkit.widgets.toolbars import FormattedTextToolbar, SystemToolbar, SearchToolbar, ValidationToolbar, CompletionsToolbar from .commands.lexer import create_command_lexer -from .enums import COMMAND_BUFFER from .lexer import DocumentLexer from .welcome_message import WELCOME_MESSAGE_TOKENS, WELCOME_MESSAGE_HEIGHT, WELCOME_MESSAGE_WIDTH import pyvim.window_arrangement as window_arrangement +from functools import partial import re +import sys __all__ = ( 'EditorLayout', + 'get_terminal_title', ) +def _try_char(character, backup, encoding=sys.stdout.encoding): + """ + Return `character` if it can be encoded using sys.stdout, else return the + backup character. + """ + if character.encode(encoding, 'replace') == b'?': + return backup + else: + return character + + +TABSTOP_DOT = _try_char('\u2508', '.') -class TabsControl(TokenListControl): + +class TabsControl(FormattedTextControl): """ Displays the tabs at the top of the screen, when there is more than one open tab. @@ -41,7 +56,17 @@ def __init__(self, editor): def location_for_tab(tab): return tab.active_window.editor_buffer.get_display_name(short=True) - def get_tokens(cli): + def create_tab_handler(index): + " Return a mouse handler for this tab. Select the tab on click. " + def handler(app, mouse_event): + if mouse_event.event_type == MouseEventType.MOUSE_DOWN: + editor.window_arrangement.active_tab_index = index + editor.sync_with_prompt_toolkit() + else: + return NotImplemented + return handler + + def get_tokens(): selected_tab_index = editor.window_arrangement.active_tab_index result = [] @@ -52,40 +77,42 @@ def get_tokens(cli): if tab.has_unsaved_changes: caption = ' + ' + caption + handler = create_tab_handler(i) + if i == selected_tab_index: - append((Token.TabBar.Tab.Active, ' %s ' % caption)) + append(('class:tabbar.tab.active', ' %s ' % caption, handler)) else: - append((Token.TabBar.Tab, ' %s ' % caption)) - append((Token.TabBar, ' ')) + append(('class:tabbar.tab', ' %s ' % caption, handler)) + append(('class:tabbar', ' ')) return result - super(TabsControl, self).__init__(get_tokens, Char(token=Token.TabBar)) + super(TabsControl, self).__init__(get_tokens, style='class:tabbar') -class TabsToolbar(Window): +class TabsToolbar(ConditionalContainer): def __init__(self, editor): super(TabsToolbar, self).__init__( - TabsControl(editor), - height=LayoutDimension.exact(1), - filter=Condition(lambda cli: len(editor.window_arrangement.tab_pages) > 1)) + Window(TabsControl(editor), height=1), + filter=Condition(lambda: len(editor.window_arrangement.tab_pages) > 1)) -class CommandLine(Window): +class CommandLine(ConditionalContainer): """ The editor command line. (For at the bottom of the screen.) """ - def __init__(self): + def __init__(self, editor): super(CommandLine, self).__init__( - BufferControl( - buffer_name=COMMAND_BUFFER, - input_processors=[BeforeInput.static(':')], - lexer=create_command_lexer()), - height=LayoutDimension.exact(1), - filter=HasFocus(COMMAND_BUFFER)) + Window( + BufferControl( + buffer=editor.command_buffer, + input_processors=[BeforeInput(':')], + lexer=create_command_lexer()), + height=1), + filter=has_focus(editor.command_buffer)) -class WelcomeMessageWindow(Window): +class WelcomeMessageWindow(ConditionalContainer): """ Welcome message pop-up, which is shown during start-up when no other files were opened. @@ -93,7 +120,7 @@ class WelcomeMessageWindow(Window): def __init__(self, editor): once_hidden = [False] # Nonlocal - def condition(cli): + def condition(): # Get editor buffers buffers = editor.window_arrangement.editor_buffers @@ -106,30 +133,34 @@ def condition(cli): return result super(WelcomeMessageWindow, self).__init__( - TokenListControl(lambda cli: WELCOME_MESSAGE_TOKENS), + Window( + FormattedTextControl(lambda: WELCOME_MESSAGE_TOKENS), + align=WindowAlign.CENTER, + style="class:welcome"), filter=Condition(condition)) -def _bufferlist_overlay_visible_condition(cli): +def _bufferlist_overlay_visible(editor): """ True when the buffer list overlay should be displayed. (This is when someone starts typing ':b' or ':buffer' in the command line.) """ - text = cli.buffers[COMMAND_BUFFER].text.lstrip() - return cli.current_buffer_name == COMMAND_BUFFER and ( - any(text.startswith(p) for p in ['b ', 'b! ', 'buffer', 'buffer!'])) + @Condition + def overlay_is_visible(): + app = get_app() -bufferlist_overlay_visible_filter = Condition(_bufferlist_overlay_visible_condition) + text = editor.command_buffer.text.lstrip() + return app.layout.has_focus(editor.command_buffer) and ( + any(text.startswith(p) for p in ['b ', 'b! ', 'buffer', 'buffer!'])) + return overlay_is_visible -class BufferListOverlay(Window): +class BufferListOverlay(ConditionalContainer): """ Floating window that shows the list of buffers when we are typing ':b' inside the vim command line. """ def __init__(self, editor): - token = Token.BufferList - def highlight_location(location, search_string, default_token): """ Return a tokenlist with the `search_string` highlighted. @@ -139,15 +170,19 @@ def highlight_location(location, search_string, default_token): # Replace token of matching positions. for m in re.finditer(re.escape(search_string), location): for i in range(m.start(), m.end()): - result[i] = (token.SearchMatch, result[i][1]) + result[i] = ('class:searchmatch', result[i][1]) + + if location == search_string: + result[0] = (result[0][0] + ' [SetCursorPosition]', result[0][1]) + return result - def get_tokens(cli): + def get_tokens(): wa = editor.window_arrangement buffer_infos = wa.list_open_buffers() # Filter infos according to typed text. - input_params = cli.buffers[COMMAND_BUFFER].text.lstrip().split(None, 1) + input_params = editor.command_buffer.text.lstrip().split(None, 1) search_string = input_params[1] if len(input_params) > 1 else '' if search_string: @@ -164,10 +199,10 @@ def matches(info): return True # When this entry is part of the current completions list. - b = cli.buffers[COMMAND_BUFFER] + b = editor.command_buffer if b.complete_state and any(info.editor_buffer.location in c.display - for c in b.complete_state.current_completions + for c in b.complete_state.completions if info.editor_buffer.location is not None): return True @@ -177,13 +212,13 @@ def matches(info): # Render output. if len(buffer_infos) == 0: - return [(token, ' No match found. ')] + return [('', ' No match found. ')] else: result = [] # Create title. - result.append((token, ' ')) - result.append((token.Title, 'Open buffers\n')) + result.append(('', ' ')) + result.append(('class:title', 'Open buffers\n')) # Get length of longest location max_location_len = max(len(info.editor_buffer.get_display_name()) for info in buffer_infos) @@ -194,10 +229,10 @@ def matches(info): char = '%' if info.is_active else ' ' char2 = 'a' if info.is_visible else ' ' char3 = ' + ' if info.editor_buffer.has_unsaved_changes else ' ' - t = token.Active if info.is_active else token + t = 'class:active' if info.is_active else '' result.extend([ - (token, ' '), + ('', ' '), (t, '%3i ' % info.index), (t, '%s' % char), (t, '%s ' % char2), @@ -206,39 +241,41 @@ def matches(info): result.extend(highlight_location(eb.get_display_name(), search_string, t)) result.extend([ (t, ' ' * (max_location_len - len(eb.get_display_name()))), - (t.Lineno, ' line %i' % (eb.buffer.document.cursor_position_row + 1)), + (t + ' class:lineno', ' line %i' % (eb.buffer.document.cursor_position_row + 1)), (t, ' \n') ]) return result super(BufferListOverlay, self).__init__( - TokenListControl(get_tokens, default_char=Char(token=token)), - filter=bufferlist_overlay_visible_filter) + Window(FormattedTextControl(get_tokens), + style='class:bufferlist', + scroll_offsets=ScrollOffsets(top=1, bottom=1)), + filter=_bufferlist_overlay_visible(editor)) -class MessageToolbarBar(TokenListToolbar): +class MessageToolbarBar(ConditionalContainer): """ Pop-up (at the bottom) for showing error/status messages. """ def __init__(self, editor): - def get_tokens(cli): + def get_tokens(): if editor.message: - return [(Token.Message, editor.message)] + return [('class:message', editor.message)] else: return [] super(MessageToolbarBar, self).__init__( - get_tokens, - filter=Condition(lambda cli: editor.message is not None)) + FormattedTextToolbar(get_tokens), + filter=Condition(lambda: editor.message is not None)) -class ReportMessageToolbar(TokenListToolbar): +class ReportMessageToolbar(ConditionalContainer): """ Toolbar that shows the messages, given by the reporter. (It shows the error message, related to the current line.) """ def __init__(self, editor): - def get_tokens(cli): + def get_formatted_text(): eb = editor.window_arrangement.active_editor_buffer lineno = eb.buffer.document.cursor_position_row @@ -246,59 +283,79 @@ def get_tokens(cli): for e in errors: if e.lineno == lineno: - return e.message_token_list + return e.formatted_text return [] super(ReportMessageToolbar, self).__init__( - get_tokens, - filter=~HasFocus(COMMAND_BUFFER) & ~HasSearch() & ~HasFocus('system')) + FormattedTextToolbar(get_formatted_text), + filter=~has_focus(editor.command_buffer) & ~is_searching & ~has_focus('system')) -class WindowStatusBar(TokenListToolbar): +class WindowStatusBar(FormattedTextToolbar): """ The status bar, which is shown below each window in a tab page. """ - def __init__(self, editor, editor_buffer, manager): - def get_tokens(cli): - insert_mode = manager.vi_state.input_mode == InputMode.INSERT - replace_mode = manager.vi_state.input_mode == InputMode.REPLACE - sel = cli.buffers[editor_buffer.buffer_name].selection_state + def __init__(self, editor, editor_buffer): + def get_text(): + app = get_app() + + insert_mode = app.vi_state.input_mode in (InputMode.INSERT, InputMode.INSERT_MULTIPLE) + replace_mode = app.vi_state.input_mode == InputMode.REPLACE + sel = editor_buffer.buffer.selection_state + temp_navigation = app.vi_state.temporary_navigation_mode visual_line = sel is not None and sel.type == SelectionType.LINES + visual_block = sel is not None and sel.type == SelectionType.BLOCK visual_char = sel is not None and sel.type == SelectionType.CHARACTERS def mode(): - if cli.current_buffer_name == editor_buffer.buffer_name: + if get_app().layout.has_focus(editor_buffer.buffer): if insert_mode: - if editor.paste_mode: + if temp_navigation: + return ' -- (insert) --' + elif editor.paste_mode: return ' -- INSERT (paste)--' else: return ' -- INSERT --' elif replace_mode: - return ' -- REPLACE --' + if temp_navigation: + return ' -- (replace) --' + else: + return ' -- REPLACE --' + elif visual_block: + return ' -- VISUAL BLOCK --' elif visual_line: return ' -- VISUAL LINE --' elif visual_char: return ' -- VISUAL --' return ' ' - return [ - (Token.Toolbar, ' '), - (Token.Toolbar, editor_buffer.location or ''), - (Token.Toolbar, ' [New File]' if editor_buffer.is_new else ''), - (Token.Toolbar, '*' if editor_buffer.has_unsaved_changes else ''), - (Token.Toolbar, ' '), - (Token.Toolbar, mode()), - ] - super(WindowStatusBar, self).__init__(get_tokens, default_char=Char(' ', Token.Toolbar)) - - -class WindowStatusBarRuler(Window): + def recording(): + if app.vi_state.recording_register: + return 'recording ' + else: + return '' + + return ''.join([ + ' ', + recording(), + (editor_buffer.location or ''), + (' [New File]' if editor_buffer.is_new else ''), + ('*' if editor_buffer.has_unsaved_changes else ''), + (' '), + mode(), + ]) + super(WindowStatusBar, self).__init__( + get_text, + style='class:toolbar.status') + + +class WindowStatusBarRuler(ConditionalContainer): """ The right side of the Vim toolbar, showing the location of the cursor in the file, and the vectical scroll percentage. """ - def __init__(self, editor, buffer_window, buffer_name): + def __init__(self, editor, buffer_window, buffer): def get_scroll_text(): info = buffer_window.render_info @@ -315,46 +372,66 @@ def get_scroll_text(): return '' - def get_tokens(cli): - main_document = cli.buffers[buffer_name].document + def get_tokens(): + main_document = buffer.document return [ - (Token.Toolbar.CursorPosition, '(%i,%i)' % (main_document.cursor_position_row + 1, - main_document.cursor_position_col + 1)), - (Token.Toolbar, ' - '), - (Token.Toolbar.Percentage, get_scroll_text()), - (Token.Toolbar, ' '), + ('class:cursorposition', '(%i,%i)' % (main_document.cursor_position_row + 1, + main_document.cursor_position_col + 1)), + ('', ' - '), + ('class:percentage', get_scroll_text()), + ('', ' '), ] super(WindowStatusBarRuler, self).__init__( - TokenListControl(get_tokens, default_char=Char(' ', Token.Toolbar), align_right=True), - height=LayoutDimension.exact(1), - width=LayoutDimension.exact(15), - filter=Condition(lambda cli: editor.show_ruler)) + Window( + FormattedTextControl(get_tokens), + char=' ', + align=WindowAlign.RIGHT, + style='class:toolbar.status', + height=1, + ), + filter=Condition(lambda: editor.show_ruler)) -class SimpleArgToolbar(Window): +class SimpleArgToolbar(ConditionalContainer): """ Simple control showing the Vi repeat arg. """ def __init__(self): - def get_tokens(cli): - if cli.input_processor.arg is not None: - return [(Token.Arg, ' %i ' % cli.input_processor.arg)] + def get_tokens(): + arg = get_app().key_processor.arg + if arg is not None: + return [('class:arg', ' %s ' % arg)] else: return [] super(SimpleArgToolbar, self).__init__( - TokenListControl(get_tokens, align_right=True), filter=HasArg()), + Window(FormattedTextControl(get_tokens), align=WindowAlign.RIGHT), + filter=has_arg), + + +class PyvimScrollOffsets(ScrollOffsets): + def __init__(self, editor): + self.editor = editor + self.left = 0 + self.right = 0 + + @property + def top(self): + return self.editor.scroll_offset + + @property + def bottom(self): + return self.editor.scroll_offset class EditorLayout(object): """ The main layout class. """ - def __init__(self, editor, manager, window_arrangement): + def __init__(self, editor, window_arrangement): self.editor = editor # Back reference to editor. - self.manager = manager self.window_arrangement = window_arrangement # Mapping from (`window_arrangement.Window`, `EditorBuffer`) to a frame @@ -372,13 +449,15 @@ def __init__(self, editor, manager, window_arrangement): floats=[ Float(xcursor=True, ycursor=True, content=CompletionsMenu(max_height=12, - extra_filter=~HasFocus(COMMAND_BUFFER))), + scroll_offset=2, + extra_filter=~has_focus(editor.command_buffer))), Float(content=BufferListOverlay(editor), bottom=1, left=0), Float(bottom=1, left=0, right=0, height=1, - content=CompletionsToolbar( - extra_filter=HasFocus(COMMAND_BUFFER) & - ~bufferlist_overlay_visible_filter & - Condition(lambda cli: editor.show_wildmenu))), + content=ConditionalContainer( + CompletionsToolbar(), + filter=has_focus(editor.command_buffer) & + ~_bufferlist_overlay_visible(editor) & + Condition(lambda: editor.show_wildmenu))), Float(bottom=1, left=0, right=0, height=1, content=ValidationToolbar()), Float(bottom=1, left=0, right=0, height=1, @@ -389,20 +468,27 @@ def __init__(self, editor, manager, window_arrangement): ] ) - self.layout = FloatContainer( + search_toolbar = SearchToolbar(vi_mode=True, search_buffer=editor.search_buffer) + self.search_control = search_toolbar.control + + self.layout = Layout(FloatContainer( content=HSplit([ TabsToolbar(editor), self._fc, - CommandLine(), + CommandLine(editor), ReportMessageToolbar(editor), SystemToolbar(), - SearchToolbar(vi_mode=True), + search_toolbar, ]), floats=[ Float(right=0, height=1, bottom=0, width=5, content=SimpleArgToolbar()), ] - ) + )) + + def get_vertical_border_char(self): + " Return the character to be used for the vertical border. " + return _try_char('\u2502', '|', get_app().output.encoding()) def update(self): """ @@ -419,18 +505,20 @@ def create_layout_from_node(node): key = (node, node.editor_buffer) frame = existing_frames.get(key) if frame is None: - frame = self._create_window_frame(node.editor_buffer) + frame, pt_window = self._create_window_frame(node.editor_buffer) + + # Link layout Window to arrangement. + node.pt_window = pt_window + self._frames[key] = frame return frame elif isinstance(node, window_arrangement.VSplit): - children = [] - for n in node: - children.append(create_layout_from_node(n)) - children.append(Window(width=LayoutDimension.exact(1), - content=FillControl('\u2502', token=Token.FrameBorder))) - children.pop() - return VSplit(children) + return VSplit( + [create_layout_from_node(n) for n in node], + padding=1, + padding_char=self.get_vertical_border_char(), + padding_style='class:frameborder') if isinstance(node, window_arrangement.HSplit): return HSplit([create_layout_from_node(n) for n in node]) @@ -440,64 +528,101 @@ def create_layout_from_node(node): def _create_window_frame(self, editor_buffer): """ - Create a Window for the buffer, with underneat a status bar. + Create a Window for the buffer, with underneath a status bar. """ - # Pass `Editor.scroll_offset` by reference. - scroll_offset = Integer.from_callable(lambda: self.editor.scroll_offset) - - window = Window(self._create_buffer_control(editor_buffer), - allow_scroll_beyond_bottom=Always(), - scroll_offset=scroll_offset) + @Condition + def wrap_lines(): + return self.editor.wrap_lines + + window = Window( + self._create_buffer_control(editor_buffer), + allow_scroll_beyond_bottom=True, + scroll_offsets=ScrollOffsets( + left=0, right=0, + top=(lambda: self.editor.scroll_offset), + bottom=(lambda: self.editor.scroll_offset)), + wrap_lines=wrap_lines, + left_margins=[ConditionalMargin( + margin=NumberedMargin( + display_tildes=True, + relative=Condition(lambda: self.editor.relative_number)), + filter=Condition(lambda: self.editor.show_line_numbers))], + cursorline=Condition(lambda: self.editor.cursorline), + cursorcolumn=Condition(lambda: self.editor.cursorcolumn), + colorcolumns=( + lambda: [ColorColumn(pos) for pos in self.editor.colorcolumn]), + ignore_content_width=True, + ignore_content_height=True, + get_line_prefix=partial(self._get_line_prefix, editor_buffer.buffer)) return HSplit([ window, VSplit([ - WindowStatusBar(self.editor, editor_buffer, self.manager), - WindowStatusBarRuler(self.editor, window, editor_buffer.buffer_name), - ]), - ]) + WindowStatusBar(self.editor, editor_buffer), + WindowStatusBarRuler(self.editor, window, editor_buffer.buffer), + ], width=Dimension()), # Ignore actual status bar width. + ]), window def _create_buffer_control(self, editor_buffer): """ Create a new BufferControl for a given location. """ - buffer_name = editor_buffer.buffer_name - @Condition - def preview_search(cli): + def preview_search(): return self.editor.incsearch input_processors = [ - # Highlighting of the search. - ConditionalProcessor( - HighlightSearchProcessor(preview_search=preview_search), - Condition(lambda cli: self.editor.highlight_search)), - # Processor for visualising spaces. (should come before the # selection processor, otherwise, we won't see these spaces # selected.) ConditionalProcessor( ShowTrailingWhiteSpaceProcessor(), - Condition(lambda cli: self.editor.display_unprintable_characters)), + Condition(lambda: self.editor.display_unprintable_characters)), - # Highlight selection. - HighlightSelectionProcessor(), - - # Highlight matching parentheses. - HighlightMatchingBracketProcessor(), + # Replace tabs by spaces. + TabsProcessor( + tabstop=(lambda: self.editor.tabstop), + char1=(lambda: '|' if self.editor.display_unprintable_characters else ' '), + char2=(lambda: _try_char('\u2508', '.', get_app().output.encoding()) + if self.editor.display_unprintable_characters else ' '), + ), # Reporting of errors, for Pyflakes. ReportingProcessor(editor_buffer), + HighlightSelectionProcessor(), + ConditionalProcessor( + HighlightSearchProcessor(), + Condition(lambda: self.editor.highlight_search)), + ConditionalProcessor( + HighlightIncrementalSearchProcessor(), + Condition(lambda: self.editor.highlight_search) & preview_search), + HighlightMatchingBracketProcessor(), + DisplayMultipleCursors(), + ] + + return BufferControl( + lexer=DocumentLexer(editor_buffer), + include_default_input_processors=False, + input_processors=input_processors, + buffer=editor_buffer.buffer, + preview_search=preview_search, + search_buffer_control=self.search_control, + focus_on_click=True) + + def _get_line_prefix(self, buffer, line_number, wrap_count): + if wrap_count > 0: + result = [] - # Replace tabs by spaces. - TabsProcessor(self.editor)] - - return BufferControl(show_line_numbers=Condition(lambda cli: self.editor.show_line_numbers), - lexer=DocumentLexer(editor_buffer), - input_processors=input_processors, - buffer_name=buffer_name, - preview_search=preview_search) + # Add 'breakindent' prefix. + if self.editor.break_indent: + line = buffer.document.lines[line_number] + prefix = line[:len(line) - len(line.lstrip())] + result.append(('', prefix)) + # Add softwrap mark. + result.append(('class:soft-wrap', '...')) + return result + return '' class ReportingProcessor(Processor): """ @@ -506,78 +631,28 @@ class ReportingProcessor(Processor): def __init__(self, editor_buffer): self.editor_buffer = editor_buffer - def run(self, cli, document, tokens): + def apply_transformation(self, transformation_input): + fragments = transformation_input.fragments + if self.editor_buffer.report_errors: for error in self.editor_buffer.report_errors: - for i in range(error.start_index, error.end_index): - if i < len(tokens): - tokens[i] = (Token.FlakesError, tokens[i][1]) - - return tokens, lambda i: i - - def invalidation_hash(self, cli, document): - return (self.editor_buffer.report_errors, ) - + if error.lineno == transformation_input.lineno: + fragments = explode_text_fragments(fragments) + for i in range(error.start_column, error.end_column): + if i < len(fragments): + fragments[i] = ('class:flakeserror', fragments[i][1]) -class ShowTrailingWhiteSpaceProcessor(Processor): - """ - Make trailing whitespace visible. - """ - def __init__(self, token=Token.TrailingWhiteSpace, char='\xb7'): - self.token = token - self.char = char - - def run(self, cli, document, tokens): - # Walk backwards through all te tokens. - t = (self.token, self.char) - is_end_of_line = True - - for i in range(len(tokens) - 1, -1, -1): - char = tokens[i][1] - if is_end_of_line and char == ' ': - tokens[i] = t - elif char == '\n': - is_end_of_line = True - else: - is_end_of_line = False + return Transformation(fragments) - return tokens, lambda i: i -class TabsProcessor(Processor): +def get_terminal_title(editor): """ - Render tabs as spaces or make them visible. + Return the terminal title, + e.g.: "filename.py (/directory) - Pyvim" """ - def __init__(self, editor): - self.editor = editor - - def run(self, cli, document, tokens): - tabstop = self.editor.tabstop - - # Create separator for tabs. - if self.editor.display_unprintable_characters: - dots = '\u2508' - separator = dots * tabstop - token = Token.Tab - else: - separator = ' ' * tabstop - token = None # Don't replace the token. - - # Remember the positions where we replace the tab. - positions = set() - - # Replace tab by separator. - for i, value in enumerate(tokens): - if value[1] == '\t': - positions.add(i-1) - tokens[i] = (token or tokens[i][0], separator) - - def map_cursor(from_position): - """ Maps original cursor position to the new one. """ - count = len(list(p for p in positions if p < from_position -1)) - return from_position + count * (tabstop - 1) - - return tokens, map_cursor - - def invalidation_hash(self, cli, document): - return (self.editor.tabstop, ) + eb = editor.current_editor_buffer + if eb is not None: + return '%s - Pyvim' % (eb.location or '[New file]', ) + else: + return 'Pyvim' diff --git a/pyvim/lexer.py b/pyvim/lexer.py index c950844..8d6a1c1 100644 --- a/pyvim/lexer.py +++ b/pyvim/lexer.py @@ -1,41 +1,55 @@ from __future__ import unicode_literals -from pygments.lexers import get_lexer_for_filename +from prompt_toolkit.lexers import Lexer, SimpleLexer, PygmentsLexer +from pygments.lexer import RegexLexer from pygments.token import Token -from pygments.util import ClassNotFound __all__ = ( 'DocumentLexer', ) -class DocumentLexer(object): +class DocumentLexer(Lexer): """ Lexer that depending on the filetype, uses another pygments lexer. """ def __init__(self, editor_buffer): self.editor_buffer = editor_buffer - def __call__(self, stripnl=False, stripall=False, ensurenl=False): + def lex_document(self, document): """ - For compatibility with a Pygments lexer class. (We use an instance of - this as if it were such a class.) - """ - return self - - def get_tokens(self, text): - """ - Call the lexer and return the tokens. + Call the lexer and return a get_tokens_for_line function. """ location = self.editor_buffer.location if location: - # Create an instance of the correct lexer class. - try: - lexer = get_lexer_for_filename(location, stripnl=False, stripall=False, ensurenl=False) - except ClassNotFound: - pass - else: - return lexer.get_tokens(text) - - return [(Token, text)] + if self.editor_buffer.in_file_explorer_mode: + return PygmentsLexer(DirectoryListingLexer, sync_from_start=False).lex_document(document) + + return PygmentsLexer.from_filename(location, sync_from_start=False).lex_document(document) + + return SimpleLexer().lex_document(document) + + +_DirectoryListing = Token.DirectoryListing + +class DirectoryListingLexer(RegexLexer): + """ + Highlighting of directory listings. + """ + name = 'directory-listing' + tokens = { + str('root'): [ # Conversion to `str` because of Pygments on Python 2. + (r'^".*', _DirectoryListing.Header), + + (r'^\.\./$', _DirectoryListing.ParentDirectory), + (r'^\./$', _DirectoryListing.CurrentDirectory), + + (r'^[^"].*/$', _DirectoryListing.Directory), + (r'^[^"].*\.(txt|rst|md)$', _DirectoryListing.Textfile), + (r'^[^"].*\.(py)$', _DirectoryListing.PythonFile), + + (r'^[^"].*\.(pyc|pyd)$', _DirectoryListing.Tempfile), + (r'^\..*$', _DirectoryListing.Dotfile), + ] + } diff --git a/pyvim/rc_file.py b/pyvim/rc_file.py index 0ce1da6..3b76737 100644 --- a/pyvim/rc_file.py +++ b/pyvim/rc_file.py @@ -27,7 +27,7 @@ def run_rc_file(editor, rc_file): Run rc file. """ assert isinstance(editor, Editor) - assert isinstance(rc_file, six.text_type) + assert isinstance(rc_file, six.string_types) # Expand tildes. rc_file = os.path.expanduser(rc_file) @@ -41,10 +41,10 @@ def run_rc_file(editor, rc_file): # Run the rc file in an empty namespace. try: namespace = {} - if os.path.exists(rc_file): - with open(rc_file, 'r') as f: - code = compile(f.read(), rc_file, 'exec') - six.exec_(code, namespace, namespace) + + with open(rc_file, 'r') as f: + code = compile(f.read(), rc_file, 'exec') + six.exec_(code, namespace, namespace) # Now we should have a 'configure' method in this namespace. We call this # method with editor as an argument. diff --git a/pyvim/reporting.py b/pyvim/reporting.py index 079a6fb..cb8dc5b 100644 --- a/pyvim/reporting.py +++ b/pyvim/reporting.py @@ -14,8 +14,6 @@ import string import six -from pygments.token import Token - __all__ = ( 'report', ) @@ -25,11 +23,11 @@ class ReporterError(object): """ Error found by a reporter. """ - def __init__(self, lineno, start_index, end_index, message_token_list): + def __init__(self, lineno, start_column, end_column, formatted_text): self.lineno = lineno # Zero based line number. - self.start_index = start_index - self.end_index = end_index - self.message_token_list = message_token_list + self.start_column = start_column + self.end_column = end_column + self.formatted_text = formatted_text def report(location, document): @@ -60,9 +58,9 @@ def report_pyflakes(document): def format_flake_message(message): return [ - (Token.FlakeMessage.Prefix, 'pyflakes:'), - (Token, ' '), - (Token.FlakeMessage, message.message % message.message_args) + ('class:flakemessage.prefix', 'pyflakes:'), + ('', ' '), + ('class:flakemessage', message.message % message.message_args) ] def message_to_reporter_error(message): @@ -73,9 +71,9 @@ def message_to_reporter_error(message): end_index += 1 return ReporterError(lineno=message.lineno - 1, - start_index=start_index, - end_index=end_index, - message_token_list=format_flake_message(message)) + start_column=message.col, + end_column=message.col + end_index - start_index, + formatted_text=format_flake_message(message)) # Construct list of ReporterError instances. return [message_to_reporter_error(m) for m in reporter.messages] diff --git a/pyvim/style.py b/pyvim/style.py index 3d01a1d..8013d75 100644 --- a/pyvim/style.py +++ b/pyvim/style.py @@ -2,12 +2,13 @@ The styles, for the colorschemes. """ from __future__ import unicode_literals -from prompt_toolkit.styles import default_style_extensions +from prompt_toolkit.styles import Style, merge_styles +from prompt_toolkit.styles.pygments import style_from_pygments_cls from pygments.styles import get_all_styles, get_style_by_name -from pygments.token import Token __all__ = ( + 'generate_built_in_styles', 'get_editor_style_by_name', ) @@ -18,15 +19,15 @@ def get_editor_style_by_name(name): This raises `pygments.util.ClassNotFound` when there is no style with this name. """ - style_cls = get_style_by_name(name) + if name == 'vim': + vim_style = Style.from_dict(default_vim_style) + else: + vim_style = style_from_pygments_cls(get_style_by_name(name)) - class EditorStyle(style_cls): - styles = {} - styles.update(style_cls.styles) - styles.update(default_style_extensions) - styles.update(style_extensions) - EditorStyle.__name__ = str('PyvimStyle_%s' % style_cls.__name__) - return EditorStyle + return merge_styles([ + vim_style, + Style.from_dict(style_extensions), + ]) def generate_built_in_styles(): @@ -38,60 +39,110 @@ def generate_built_in_styles(): style_extensions = { # Toolbar colors. - Token.Toolbar: '#ffffff bg:#444444', - Token.Toolbar.CursorPosition: '#bbffbb bg:#444444', - Token.Toolbar.Percentage: '#ffbbbb bg:#444444', - Token.Header: '#ffffff bg:#662222', + 'toolbar.status': '#ffffff bg:#444444', + 'toolbar.status.cursorposition': '#bbffbb bg:#444444', + 'toolbar.status.percentage': '#ffbbbb bg:#444444', # Flakes color. - Token.FlakesError: 'bg:#ff4444 #ffffff', + 'flakeserror': 'bg:#ff4444 #ffffff', # Flake messages - Token.FlakeMessage.Prefix: 'bg:#ff8800 #ffffff', - Token.FlakeMessage: '#886600', + 'flakemessage.prefix': 'bg:#ff8800 #ffffff', + 'flakemessage': '#886600', # Highlighting for the text in the command bar. - Token.CommandLine.Command: 'bold', - Token.CommandLine.Location: 'bg:#bbbbff #000000', + 'commandline.command': 'bold', + 'commandline.location': 'bg:#bbbbff #000000', # Frame borders (for between vertical splits.) - Token.FrameBorder: 'bold', #bg:#88aa88 #ffffff', + 'frameborder': 'bold', #bg:#88aa88 #ffffff', # Messages - Token.Message: 'bg:#bbee88 #222222', + 'message': 'bg:#bbee88 #222222', # Welcome message - Token.Welcome.Title: 'underline', - Token.Welcome.Body: '', - Token.Welcome.Body.Key: '#0000ff', - Token.Welcome.PythonVersion: 'bg:#888888 #ffffff', - - # Matching brace color - Token.MatchingBracket: 'bg:#aaaaff #000000', + 'welcome title': 'underline', + 'welcome version': '#8800ff', + 'welcome key': '#0000ff', + 'welcome pythonversion': 'bg:#888888 #ffffff', # Tabs - Token.TabBar: 'bg:#000000', - Token.TabBar.Tab: 'bg:#888888 underline', - Token.TabBar.Tab.Active: 'bold noinherit', + 'tabbar': 'noinherit reverse', + 'tabbar.tab': 'underline', + 'tabbar.tab.active': 'bold noinherit', # Arg count. - Token.Arg: 'bg:#cccc44 #000000', + 'arg': 'bg:#cccc44 #000000', # Buffer list - Token.BufferList: 'bg:#aaddaa #000000', - Token.BufferList.Title: 'underline', - Token.BufferList.Lineno: '#666666', - Token.BufferList.Active: 'bg:#ccffcc', - Token.BufferList.Active.Lineno: '#666666', - Token.BufferList.SearchMatch: 'bg:#eeeeaa', + 'bufferlist': 'bg:#aaddaa #000000', + 'bufferlist title': 'underline', + 'bufferlist lineno': '#666666', + 'bufferlist active': 'bg:#ccffcc', + 'bufferlist active.lineno': '#666666', + 'bufferlist searchmatch': 'bg:#eeeeaa', # Completions toolbar. - Token.Toolbar.Completions: 'bg:#aaddaa #000000', - Token.Toolbar.Completions.Arrow: 'bg:#aaddaa #000000 bold', - Token.Toolbar.Completions.Completion: 'bg:#aaddaa #000000', - Token.Toolbar.Completions.Completion.Current: 'bg:#444444 #ffffff', + 'completions-toolbar': 'bg:#aaddaa #000000', + 'completions-toolbar.arrow': 'bg:#aaddaa #000000 bold', + 'completions-toolbar completion': 'bg:#aaddaa #000000', + 'completions-toolbar current-completion': 'bg:#444444 #ffffff', + + # Soft wrap. + 'soft-wrap': '#888888', + + # Directory listing style. + 'pygments.directorylisting.header': '#4444ff', + 'pygments.directorylisting.directory': '#ff4444 bold', + 'pygments.directorylisting.currentdirectory': '#888888', + 'pygments.directorylisting.parentdirectory': '#888888', + 'pygments.directorylisting.tempfile': '#888888', + 'pygments.directorylisting.dotfile': '#888888', + 'pygments.directorylisting.pythonfile': '#8800ff', + 'pygments.directorylisting.textfile': '#aaaa00', +} - # Tabs - Token.TrailingWhiteSpace: '#999999', - Token.Tab: '#999999', + +# Default 'vim' color scheme. Taken from the Pygments Vim colorscheme, but +# modified to use mainly ANSI colors. +default_vim_style = { + 'pygments': '', + 'pygments.whitespace': '', + 'pygments.comment': 'ansiblue', + 'pygments.comment.preproc': 'ansiyellow', + 'pygments.comment.special': 'bold', + + 'pygments.keyword': '#999900', + 'pygments.keyword.declaration': 'ansigreen', + 'pygments.keyword.namespace': 'ansimagenta', + 'pygments.keyword.pseudo': '', + 'pygments.keyword.type': 'ansigreen', + + 'pygments.operator': '', + 'pygments.operator.word': '', + + 'pygments.name': '', + 'pygments.name.class': 'ansicyan', + 'pygments.name.builtin': 'ansicyan', + 'pygments.name.exception': '', + 'pygments.name.variable': 'ansicyan', + 'pygments.name.function': 'ansicyan', + + 'pygments.literal': 'ansired', + 'pygments.string': 'ansired', + 'pygments.string.doc': '', + 'pygments.number': 'ansimagenta', + + 'pygments.generic.heading': 'bold ansiblue', + 'pygments.generic.subheading': 'bold ansimagenta', + 'pygments.generic.deleted': 'ansired', + 'pygments.generic.inserted': 'ansigreen', + 'pygments.generic.error': 'ansibrightred', + 'pygments.generic.emph': 'italic', + 'pygments.generic.strong': 'bold', + 'pygments.generic.prompt': 'bold ansiblue', + 'pygments.generic.output': 'ansigray', + 'pygments.generic.traceback': '#04d', + + 'pygments.error': 'border:ansired' } diff --git a/pyvim/welcome_message.py b/pyvim/welcome_message.py index ea90ff9..bd97315 100644 --- a/pyvim/welcome_message.py +++ b/pyvim/welcome_message.py @@ -2,9 +2,9 @@ The welcome message. This is displayed when the editor opens without any files. """ from __future__ import unicode_literals -from pygments.token import Token -from prompt_toolkit.layout.utils import token_list_len +from prompt_toolkit.formatted_text.utils import fragment_list_len +import prompt_toolkit import pyvim import platform import sys @@ -17,42 +17,28 @@ 'WELCOME_MESSAGE_HEIGHT', ) -WELCOME_MESSAGE_WIDTH = 34 - - -def _t(token_list): - """ - Center tokens on this line. - """ - length = token_list_len(token_list) - - return [(Token.Welcome, ' ' * int((WELCOME_MESSAGE_WIDTH - length) / 2))] \ - + token_list + [(Token.Welcome, '\n')] - - -WELCOME_MESSAGE_TOKENS = ( - _t([(Token.Welcome.Title, 'PyVim - Pure Python Vi clone')]) + - _t([(Token.Welcome.Body, 'Still experimental')]) + - _t([(Token.Welcome.Body, '')]) + - _t([(Token.Welcome.Body, 'version %s' % pyvim_version)]) + - _t([(Token.Welcome.Body, 'by Jonathan Slenders')]) + - _t([(Token.Welcome.Body, '')]) + - _t([(Token.Welcome.Body, 'type :q'), - (Token.Welcome.Body.Key, ''), - (Token.Welcome.Body, ' to exit')]) + - _t([(Token.Welcome.Body, 'type :help'), - (Token.Welcome.Body.Key, ''), - (Token.Welcome.Body, ' or '), - (Token.Welcome.Body.Key, ''), - (Token.Welcome.Body, ' for help')]) + - _t([(Token.Welcome.Body, '')]) + - _t([(Token.Welcome.Body, 'All feedback is appreciated.')]) + - _t([(Token.Welcome.Body, '')]) + - _t([(Token.Welcome.Body, '')]) + - - _t([(Token.Welcome.PythonVersion, ' %s %i.%i.%i ' % ( +WELCOME_MESSAGE_WIDTH = 36 + + +WELCOME_MESSAGE_TOKENS = [ + ('class:title', 'PyVim - Pure Python Vi clone\n'), + ('', 'Still experimental\n\n'), + ('', 'version '), ('class:version', pyvim_version), + ('', ', prompt_toolkit '), ('class:version', prompt_toolkit.__version__), + ('', '\n'), + ('', 'by Jonathan Slenders\n\n'), + ('', 'type :q'), + ('class:key', ''), + ('', ' to exit\n'), + ('', 'type :help'), + ('class:key', ''), + ('', ' or '), + ('class:key', ''), + ('', ' for help\n\n'), + ('', 'All feedback is appreciated.\n\n'), + ('class:pythonversion', ' %s %i.%i.%i ' % ( platform.python_implementation(), - version[0], version[1], version[2]))]) -) + version[0], version[1], version[2])), +] -WELCOME_MESSAGE_HEIGHT = ''.join(t[1] for t in WELCOME_MESSAGE_TOKENS).count('\n') +WELCOME_MESSAGE_HEIGHT = ''.join(t[1] for t in WELCOME_MESSAGE_TOKENS).count('\n') + 1 diff --git a/pyvim/window_arrangement.py b/pyvim/window_arrangement.py index 4c62331..5f5044b 100644 --- a/pyvim/window_arrangement.py +++ b/pyvim/window_arrangement.py @@ -33,6 +33,9 @@ def __init__(self, editor_buffer): assert isinstance(editor_buffer, EditorBuffer) self.editor_buffer = editor_buffer + # The prompt_toolkit layout Window. + self.pt_window = None + def __repr__(self): return '%s(editor_buffer=%r)' % (self.__class__.__name__, self.editor_buffer) @@ -180,7 +183,7 @@ def close_active_window(self): self.active_window = None # No windows left. # When there is exactly on item left, move this back into the parent - # split. (We don't want to keep a split with one item around -- exept + # split. (We don't want to keep a split with one item around -- except # for the root.) if len(active_split) == 1 and active_split != self.root: parent = self._get_split_parent(active_split) @@ -214,8 +217,6 @@ def __init__(self, editor): self.active_tab_index = None self.editor_buffers = [] # List of EditorBuffer - self._buffer_index = 0 # Index for generating buffer names. - @property def editor(self): """ The Editor instance. """ @@ -233,6 +234,14 @@ def active_editor_buffer(self): if self.active_tab and self.active_tab.active_window: return self.active_tab.active_window.editor_buffer + @property + def active_pt_window(self): + " The active prompt_toolkit layout Window. " + if self.active_tab: + w = self.active_tab.active_window + if w: + return w.pt_window + def get_editor_buffer_for_location(self, location): """ Return the `EditorBuffer` for this location. @@ -390,11 +399,8 @@ def _add_editor_buffer(self, editor_buffer, show_in_current_window=False): if show_in_current_window and self.active_tab: self.active_tab.show_editor_buffer(editor_buffer) - # Add buffer to CLI. - self.editor.cli.add_buffer(editor_buffer.buffer_name, editor_buffer.buffer) - # Start reporter. - self.editor.run_reporter_for_editor_buffer(editor_buffer) + editor_buffer.run_reporter() def _get_or_create_editor_buffer(self, location=None, text=None): """ @@ -406,14 +412,9 @@ def _get_or_create_editor_buffer(self, location=None, text=None): assert location is None or text is None # Don't pass two of them. assert location is None or isinstance(location, string_types) - def new_name(): - """ Generate name for new buffer. """ - self._buffer_index += 1 - return 'buffer-%i' % self._buffer_index - if location is None: # Create and add an empty EditorBuffer - eb = EditorBuffer(self.editor, new_name(), text=text) + eb = EditorBuffer(self.editor, text=text) self._add_editor_buffer(eb) return eb @@ -425,7 +426,7 @@ def new_name(): # Not found? Create one. if eb is None: # Create and add EditorBuffer - eb = EditorBuffer(self.editor, new_name(), location) + eb = EditorBuffer(self.editor, location) self._add_editor_buffer(eb) return eb @@ -458,7 +459,7 @@ def _auto_close_new_empty_buffers(self): # Remove empty/new buffers that are hidden. for eb in self.editor_buffers[:]: - if eb.is_new and eb not in ebs and eb.buffer.text == '': + if eb.is_new and not eb.location and eb not in ebs and eb.buffer.text == '': self.editor_buffers.remove(eb) def close_buffer(self): diff --git a/setup.py b/setup.py index b443684..f6257e7 100644 --- a/setup.py +++ b/setup.py @@ -3,28 +3,25 @@ from setuptools import setup, find_packages import pyvim -long_description = open( - os.path.join( - os.path.dirname(__file__), - 'README.rst' - ) -).read() +with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: + long_description = f.read() setup( name='pyvim', author='Jonathan Slenders', version=pyvim.__version__, - license='LICENSE', + license='BSD License', url='https://github.com/jonathanslenders/pyvim', - description='Pure Pyton Vi Implementation', + description='Pure Python Vi Implementation', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt-toolkit==0.37', - 'ptpython==0.10', # For the Python completion (with Jedi.) - 'pyflakes', # For Python error reporting. - 'docopt', # For command line arguments. + 'prompt_toolkit>=2.0.0,<3.1.0', + 'six', + 'pyflakes', # For Python error reporting. + 'pygments', # For the syntax highlighting. + 'docopt', # For command line arguments. ], entry_points={ 'console_scripts': [ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4fda75b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +import pytest + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.output import DummyOutput +from prompt_toolkit.input import DummyInput +from pyvim.editor import Editor +from pyvim.window_arrangement import TabPage, EditorBuffer, Window + + +@pytest.fixture +def editor(): + return Editor(output=DummyOutput(), input=DummyInput()) + + +@pytest.fixture +def editor_buffer(editor): + return EditorBuffer(editor) + + +@pytest.fixture +def window(editor_buffer): + return Window(editor_buffer) + + +@pytest.fixture +def tab_page(window): + return TabPage(window) diff --git a/tests/test_substitute.py b/tests/test_substitute.py new file mode 100644 index 0000000..5d1711b --- /dev/null +++ b/tests/test_substitute.py @@ -0,0 +1,139 @@ +from pyvim.commands.handler import handle_command + +sample_text = """ +Roses are red, + Violets are blue, +Sugar is sweet, + And so are you. +""".lstrip() + +def given_sample_text(editor_buffer, text=None): + editor = editor_buffer.editor + editor.window_arrangement._add_editor_buffer(editor_buffer) + editor_buffer.buffer.text = text or sample_text + editor.sync_with_prompt_toolkit() + + +def given_cursor_position(editor_buffer, line_number, column=0): + editor_buffer.buffer.cursor_position = \ + editor_buffer.buffer.document.translate_row_col_to_index(line_number - 1, column) + + +def test_substitute_current_line(editor, editor_buffer): + given_sample_text(editor_buffer) + given_cursor_position(editor_buffer, 2) + + handle_command(editor, ':s/s are/ is') + + assert 'Roses are red,' in editor_buffer.buffer.text + assert 'Violet is blue,' in editor_buffer.buffer.text + assert 'And so are you.' in editor_buffer.buffer.text + assert editor_buffer.buffer.cursor_position \ + == editor_buffer.buffer.text.index('Violet') + + +def test_substitute_single_line(editor, editor_buffer): + given_sample_text(editor_buffer) + given_cursor_position(editor_buffer, 1) + + handle_command(editor, ':2s/s are/ is') + + assert 'Roses are red,' in editor_buffer.buffer.text + assert 'Violet is blue,' in editor_buffer.buffer.text + assert 'And so are you.' in editor_buffer.buffer.text + assert editor_buffer.buffer.cursor_position \ + == editor_buffer.buffer.text.index('Violet') + + +def test_substitute_range(editor, editor_buffer): + given_sample_text(editor_buffer) + given_cursor_position(editor_buffer, 1) + + handle_command(editor, ':1,3s/s are/ is') + + assert 'Rose is red,' in editor_buffer.buffer.text + assert 'Violet is blue,' in editor_buffer.buffer.text + assert 'And so are you.' in editor_buffer.buffer.text + # FIXME: vim would have set the cursor position on last substituted line + # but we set the cursor position on the end_range even when there + # is not substitution there + # assert editor_buffer.buffer.cursor_position \ + # == editor_buffer.buffer.text.index('Violet') + assert editor_buffer.buffer.cursor_position \ + == editor_buffer.buffer.text.index('Sugar') + + +def test_substitute_range_boundaries(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet\n' * 4) + + handle_command(editor, ':2,3s/Violet/Rose') + + assert 'Violet\nRose\nRose\nViolet\n' in editor_buffer.buffer.text + + +def test_substitute_from_search_history(editor, editor_buffer): + given_sample_text(editor_buffer) + editor.application.current_search_state.text = 'blue' + + handle_command(editor, ':1,3s//pretty') + assert 'Violets are pretty,' in editor_buffer.buffer.text + + +def test_substitute_from_substitute_search_history(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet is Violet\n') + + handle_command(editor, ':s/Violet/Rose') + assert 'Rose is Violet' in editor_buffer.buffer.text + + handle_command(editor, ':s//Lily') + assert 'Rose is Lily' in editor_buffer.buffer.text + + +def test_substitute_with_repeat_last_substitution(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet is Violet\n') + editor.application.current_search_state.text = 'Lily' + + handle_command(editor, ':s/Violet/Rose') + assert 'Rose is Violet' in editor_buffer.buffer.text + + handle_command(editor, ':s') + assert 'Rose is Rose' in editor_buffer.buffer.text + + +def test_substitute_without_replacement_text(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet Violet Violet \n') + editor.application.current_search_state.text = 'Lily' + + handle_command(editor, ':s/Violet/') + assert ' Violet Violet \n' in editor_buffer.buffer.text + + handle_command(editor, ':s/Violet') + assert ' Violet \n' in editor_buffer.buffer.text + + handle_command(editor, ':s/') + assert ' \n' in editor_buffer.buffer.text + + +def test_substitute_with_repeat_last_substitution_without_previous_substitution(editor, editor_buffer): + original_text = 'Violet is blue\n' + given_sample_text(editor_buffer, original_text) + + handle_command(editor, ':s') + assert original_text in editor_buffer.buffer.text + + editor.application.current_search_state.text = 'blue' + + handle_command(editor, ':s') + assert 'Violet is \n' in editor_buffer.buffer.text + + +def test_substitute_flags_empty_flags(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet is Violet\n') + handle_command(editor, ':s/Violet/Rose/') + assert 'Rose is Violet' in editor_buffer.buffer.text + + +def test_substitute_flags_g(editor, editor_buffer): + given_sample_text(editor_buffer, 'Violet is Violet\n') + handle_command(editor, ':s/Violet/Rose/g') + assert 'Rose is Rose' in editor_buffer.buffer.text diff --git a/tests/test_window_arrangements.py b/tests/test_window_arrangements.py index fe136a1..b3d02bd 100644 --- a/tests/test_window_arrangements.py +++ b/tests/test_window_arrangements.py @@ -1,33 +1,20 @@ from __future__ import unicode_literals from prompt_toolkit.buffer import Buffer -from pyvim.window_arrangement import TabPage, EditorBuffer, Window, HSplit, VSplit +from pyvim.window_arrangement import EditorBuffer, VSplit -import unittest +def test_initial(window, tab_page): + assert isinstance(tab_page.root, VSplit) + assert tab_page.root == [window] -class BufferTest(unittest.TestCase): - def setUp(self): - b = Buffer() - eb = EditorBuffer('b1', b) - self.window = Window(eb) - self.tabpage = TabPage(self.window) - def test_initial(self): - self.assertIsInstance(self.tabpage.root, VSplit) - self.assertEqual(self.tabpage.root, [self.window]) +def test_vsplit(editor, tab_page): + # Create new buffer. + eb = EditorBuffer(editor) - def test_vsplit(self): - # Create new buffer. - b = Buffer() - eb = EditorBuffer('b1', b) + # Insert in tab, by splitting. + tab_page.vsplit(eb) - # Insert in tab, by splitting. - self.tabpage.vsplit(eb) - - self.assertIsInstance(self.tabpage.root, VSplit) - self.assertEqual(len(self.tabpage.root), 2) - - -if __name__ == '__main__': - unittest.main() + assert isinstance(tab_page.root, VSplit) + assert len(tab_page.root) == 2