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 c038368..7d46893 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,81 @@ 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 ----------------- diff --git a/README.rst b/README.rst index 92d0c37..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: @@ -194,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 9f8050e..673f6eb --- a/pyvim/__init__.py +++ b/pyvim/__init__.py @@ -1,3 +1,3 @@ from __future__ import unicode_literals -__version__ = '0.0.17' +__version__ = '3.0.3' diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 4e29802..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', @@ -221,7 +224,7 @@ def handler(): print(' %3i %-2s %-20s line %i' % ( info.index, char, eb.location, (eb.buffer.document.cursor_position_row + 1))) six.moves.input('\nPress ENTER to continue...') - editor.cli.run_in_terminal(handler) + 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') @@ -400,6 +404,22 @@ 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): @@ -608,6 +628,21 @@ 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 . " @@ -624,14 +659,14 @@ def disable_mouse(editor): @set_cmd('top') def enable_tildeop(editor): " Enable tilde operator. " - editor.cli.vi_state.tilde_operator = True + editor.application.vi_state.tilde_operator = True @set_cmd('notildeop') @set_cmd('notop') def disable_tildeop(editor): " Disable tilde operator. " - editor.cli.vi_state.tilde_operator = False + editor.application.vi_state.tilde_operator = False @set_cmd('cursorline') @@ -663,9 +698,59 @@ def disable_cursorcolumn(editor): @set_cmd('cc', accepts_value=True) def set_scroll_offset(editor, value): try: - value = [int(val) for val in value.split(',')] + 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 = value + 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 6e0c046..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 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 3716ace..2dd7807 100644 --- a/pyvim/commands/lexer.py +++ b/pyvim/commands/lexer.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer -from prompt_toolkit.layout.lexers import PygmentsLexer, SimpleLexer +from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer -from pygments.token import Token from pygments.lexers import BashLexer from .grammar import COMMAND_GRAMMAR @@ -17,7 +16,7 @@ def create_command_lexer(): Lexer for highlighting of the command line. """ return GrammarLexer(COMMAND_GRAMMAR, lexers={ - 'command': SimpleLexer(Token.CommandLine.Command), - 'location': SimpleLexer(Token.CommandLine.Location), + 'command': SimpleLexer('class:commandline.command'), + 'location': SimpleLexer('class:commandline.location'), 'shell_command': PygmentsLexer(BashLexer), }) 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 f26d2ae..842df37 100644 --- a/pyvim/editor.py +++ b/pyvim/editor.py @@ -10,24 +10,19 @@ from __future__ import unicode_literals from prompt_toolkit.application import Application -from prompt_toolkit.buffer import Buffer, AcceptAction -from prompt_toolkit.enums import SEARCH_BUFFER, EditingMode -from prompt_toolkit.filters import Always, Condition +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 from prompt_toolkit.key_binding.vi_state import InputMode -from prompt_toolkit.shortcuts import create_eventloop 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, get_terminal_title -from .reporting import report from .style import generate_built_in_styles, get_editor_style_by_name from .window_arrangement import WindowArrangement from .io import FileIO, DirectoryIO, HttpIO, GZipFileIO @@ -43,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 @@ -61,6 +63,7 @@ def __init__(self, config_directory='~/.pyvim'): 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. @@ -70,13 +73,12 @@ def __init__(self, config_directory='~/.pyvim'): 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('default') + self.current_style = get_editor_style_by_name('vim') # I/O backends. self.io_backends = [ @@ -86,29 +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.editor_layout = EditorLayout(self, self.window_arrangement) self.application = self._create_application() - self.cli = CommandLineInterface( - eventloop=self.eventloop, - application=self.application) - # Hide message when a key is pressed. def key_pressed(_): self.message = None - self.cli.input_processor.beforeKeyPress += key_pressed + 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. @@ -140,56 +162,29 @@ 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 app. - - # Create CLI. + # 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, - get_title=lambda: get_terminal_title(self), - buffers={ - COMMAND_BUFFER: command_buffer, - SEARCH_BUFFER: search_buffer, - }, + key_bindings=self.key_bindings, +# get_title=lambda: get_terminal_title(self), style=DynamicStyle(lambda: self.current_style), - paste_mode=Condition(lambda cli: self.paste_mode), - ignore_case=Condition(lambda cli: self.ignore_case), - mouse_support=Condition(lambda cli: self.enable_mouse_support), - use_alternate_screen=True, - on_buffer_changed=self._current_buffer_changed) + 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 self.cli.current_buffer == command_buffer: - self.previewer.preview(command_buffer.text) - command_buffer.on_text_changed += 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 application @@ -198,13 +193,12 @@ def current_editor_buffer(self): """ Return the `EditorBuffer` that is currently active. """ - # For each buffer name on the focus stack. - for current_buffer_name in self.cli.buffers.focus_stack: - if current_buffer_name is not None: - # Find/return the EditorBuffer with this name. - for b in self.window_arrangement.editor_buffers: - if b.buffer_name == current_buffer_name: - return b + current_buffer = self.application.current_buffer + + # Find/return the EditorBuffer with this name. + for b in self.window_arrangement.editor_buffers: + if b.buffer == current_buffer: + return b @property def add_key_binding(self): @@ -213,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): """ @@ -241,59 +235,9 @@ def sync_with_prompt_toolkit(self): # Make sure that the focus stack of prompt-toolkit has the current # page. - self.cli.focus( - self.window_arrangement.active_editor_buffer.buffer_name) - - def _current_buffer_changed(self, cli): - """ - 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) - eb.report_errors = [] - - # 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.invalidate() - else: - # Restart reporter when the text was changed. - self._current_buffer_changed(self.cli) - - 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): """ @@ -312,17 +256,17 @@ def run(self): def pre_run(): # Start in navigation mode. - self.cli.vi_state.input_mode = InputMode.NAVIGATION + self.application.vi_state.input_mode = InputMode.NAVIGATION # Run eventloop of prompt_toolkit. - self.cli.run(reset_current_buffer=False, pre_run=pre_run) + self.application.run(pre_run=pre_run) def enter_command_mode(self): """ Go into command mode. """ - self.cli.push_focus(COMMAND_BUFFER) - self.cli.vi_state.input_mode = InputMode.INSERT + self.application.layout.focus(self.command_buffer) + self.application.vi_state.input_mode = InputMode.INSERT self.previewer.save() @@ -332,7 +276,7 @@ def leave_command_mode(self, append_to_history=False): """ self.previewer.restore() - self.cli.pop_focus() - self.cli.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 47e1952..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,6 +92,8 @@ 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 @@ -149,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 c121877..b1a8f0a 100644 --- a/pyvim/io/backends.py +++ b/pyvim/io/backends.py @@ -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 cc45d58..e3209ee 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -1,12 +1,10 @@ from __future__ import unicode_literals -from prompt_toolkit.filters import Condition, HasFocus, Filter, ViInsertMode, ViNavigationMode -from prompt_toolkit.key_binding.bindings.utils import create_handle_decorator -from prompt_toolkit.key_binding.manager import KeyBindingManager -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', @@ -17,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): @@ -27,23 +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_search=True, - enable_extra_page_navigation=True, - enable_system_bindings=True) + 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 = ViInsertMode() & vi_buffer_focussed - in_navigation_mode = ViNavigationMode() & 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.ControlT) + @kb.add('c-t') def _(event): """ Override default behaviour of prompt-toolkit. @@ -52,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) @@ -72,50 +67,49 @@ def indent_line(event): # Restore cursor. b.cursor_position -= pos - @handle(Keys.ControlR, filter=in_navigation_mode, save_before=(lambda e: 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=ViInsertMode() & - ~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. @@ -123,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. @@ -131,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 96237bb..f7bc2be 100644 --- a/pyvim/layout.py +++ b/pyvim/layout.py @@ -2,41 +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, ConditionalContainer, ScrollOffsets, ColorColumn -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.margins import ConditionalMargin, NumberredMargin +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, ConditionalProcessor, BeforeInput, ShowTrailingWhiteSpaceProcessor, Transformation, HighlightSelectionProcessor, HighlightSearchProcessor, HighlightMatchingBracketProcessor, TabsProcessor -from prompt_toolkit.layout.screen import Char -from prompt_toolkit.layout.toolbars import TokenListToolbar, SystemToolbar, SearchToolbar, ValidationToolbar, CompletionsToolbar -from prompt_toolkit.layout.utils import explode_tokens -from prompt_toolkit.mouse_events import MouseEventTypes -from prompt_toolkit.reactive import Integer +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 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. @@ -47,15 +58,15 @@ def location_for_tab(tab): def create_tab_handler(index): " Return a mouse handler for this tab. Select the tab on click. " - def handler(cli, mouse_event): - if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN: + 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(cli): + def get_tokens(): selected_tab_index = editor.window_arrangement.active_tab_index result = [] @@ -69,36 +80,36 @@ def get_tokens(cli): handler = create_tab_handler(i) if i == selected_tab_index: - append((Token.TabBar.Tab.Active, ' %s ' % caption, handler)) + append(('class:tabbar.tab.active', ' %s ' % caption, handler)) else: - append((Token.TabBar.Tab, ' %s ' % caption, handler)) - 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(ConditionalContainer): def __init__(self, editor): super(TabsToolbar, self).__init__( - Window(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(ConditionalContainer): """ The editor command line. (For at the bottom of the screen.) """ - def __init__(self): + def __init__(self, editor): super(CommandLine, self).__init__( Window( BufferControl( - buffer_name=COMMAND_BUFFER, - input_processors=[BeforeInput.static(':')], + buffer=editor.command_buffer, + input_processors=[BeforeInput(':')], lexer=create_command_lexer()), - height=LayoutDimension.exact(1)), - filter=HasFocus(COMMAND_BUFFER)) + height=1), + filter=has_focus(editor.command_buffer)) class WelcomeMessageWindow(ConditionalContainer): @@ -109,7 +120,7 @@ class WelcomeMessageWindow(ConditionalContainer): def __init__(self, editor): once_hidden = [False] # Nonlocal - def condition(cli): + def condition(): # Get editor buffers buffers = editor.window_arrangement.editor_buffers @@ -122,20 +133,26 @@ def condition(cli): return result super(WelcomeMessageWindow, self).__init__( - Window(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(ConditionalContainer): @@ -144,8 +161,6 @@ class BufferListOverlay(ConditionalContainer): 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. @@ -155,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: @@ -180,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 @@ -193,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) @@ -210,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), @@ -222,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__( - Window(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 @@ -262,37 +283,45 @@ 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 = cli.vi_state.input_mode == InputMode.INSERT - replace_mode = cli.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: @@ -301,15 +330,24 @@ def mode(): return ' -- VISUAL --' return ' ' - return [ - (Token.Toolbar.Status, ' '), - (Token.Toolbar.Status, editor_buffer.location or ''), - (Token.Toolbar.Status, ' [New File]' if editor_buffer.is_new else ''), - (Token.Toolbar.Status, '*' if editor_buffer.has_unsaved_changes else ''), - (Token.Toolbar.Status, ' '), - (Token.Toolbar.Status, mode()), - ] - super(WindowStatusBar, self).__init__(get_tokens, default_char=Char(' ', Token.Toolbar.Status)) + 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): @@ -317,7 +355,7 @@ 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 @@ -334,23 +372,26 @@ 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.Status.CursorPosition, '(%i,%i)' % (main_document.cursor_position_row + 1, - main_document.cursor_position_col + 1)), - (Token.Toolbar.Status, ' - '), - (Token.Toolbar.Status.Percentage, get_scroll_text()), - (Token.Toolbar.Status, ' '), + ('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__( Window( - TokenListControl(get_tokens, default_char=Char(' ', Token.Toolbar.Status), align_right=True), - height=LayoutDimension.exact(1), - ), - filter=Condition(lambda cli: editor.show_ruler)) + FormattedTextControl(get_tokens), + char=' ', + align=WindowAlign.RIGHT, + style='class:toolbar.status', + height=1, + ), + filter=Condition(lambda: editor.show_ruler)) class SimpleArgToolbar(ConditionalContainer): @@ -358,15 +399,16 @@ 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__( - Window(TokenListControl(get_tokens, align_right=True)), - filter=HasArg()), + Window(FormattedTextControl(get_tokens), align=WindowAlign.RIGHT), + filter=has_arg), class PyvimScrollOffsets(ScrollOffsets): @@ -388,9 +430,8 @@ 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 @@ -409,13 +450,14 @@ def __init__(self, editor, manager, window_arrangement): Float(xcursor=True, ycursor=True, content=CompletionsMenu(max_height=12, scroll_offset=2, - extra_filter=~HasFocus(COMMAND_BUFFER))), + 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, @@ -426,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): """ @@ -456,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]) @@ -477,46 +528,47 @@ 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. """ @Condition - def wrap_lines(cli): + def wrap_lines(): return self.editor.wrap_lines window = Window( self._create_buffer_control(editor_buffer), - allow_scroll_beyond_bottom=Always(), + allow_scroll_beyond_bottom=True, scroll_offsets=ScrollOffsets( left=0, right=0, - top=Integer.from_callable(lambda: self.editor.scroll_offset), - bottom=Integer.from_callable(lambda: self.editor.scroll_offset)), + top=(lambda: self.editor.scroll_offset), + bottom=(lambda: self.editor.scroll_offset)), wrap_lines=wrap_lines, left_margins=[ConditionalMargin( - margin=NumberredMargin( + margin=NumberedMargin( display_tildes=True, - relative=Condition(lambda cli: self.editor.relative_number)), - filter=Condition(lambda cli: self.editor.show_line_numbers))], - cursorline=Condition(lambda cli: self.editor.cursorline), - cursorcolumn=Condition(lambda cli: self.editor.cursorcolumn), - get_colorcolumns=( - lambda cli: [ColorColumn(pos) for pos in self.editor.colorcolumn])) + 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 = [ @@ -525,30 +577,52 @@ def preview_search(cli): # selected.) ConditionalProcessor( ShowTrailingWhiteSpaceProcessor(), - Condition(lambda cli: self.editor.display_unprintable_characters)), + Condition(lambda: self.editor.display_unprintable_characters)), # Replace tabs by spaces. TabsProcessor( - tabstop=Integer.from_callable(lambda: self.editor.tabstop), - get_char1=(lambda cli: '|' if self.editor.display_unprintable_characters else ' '), - get_char2=(lambda cli: '\u2508' if self.editor.display_unprintable_characters else ' '), + 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(preview_search=preview_search), - Condition(lambda cli: self.editor.highlight_search)), + 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), - input_processors=input_processors, - buffer_name=buffer_name, - preview_search=preview_search, - focus_on_click=True) + 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 = [] + # 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): """ @@ -557,16 +631,18 @@ class ReportingProcessor(Processor): def __init__(self, editor_buffer): self.editor_buffer = editor_buffer - def apply_transformation(self, cli, document, lineno, source_to_display, 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: - if error.lineno == lineno: - tokens = explode_tokens(tokens) + if error.lineno == transformation_input.lineno: + fragments = explode_text_fragments(fragments) for i in range(error.start_column, error.end_column): - if i < len(tokens): - tokens[i] = (Token.FlakesError, tokens[i][1]) + if i < len(fragments): + fragments[i] = ('class:flakeserror', fragments[i][1]) - return Transformation(tokens) + return Transformation(fragments) diff --git a/pyvim/lexer.py b/pyvim/lexer.py index 3171d62..8d6a1c1 100644 --- a/pyvim/lexer.py +++ b/pyvim/lexer.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals -from prompt_toolkit.layout.lexers import Lexer, SimpleLexer, PygmentsLexer +from prompt_toolkit.lexers import Lexer, SimpleLexer, PygmentsLexer +from pygments.lexer import RegexLexer +from pygments.token import Token __all__ = ( 'DocumentLexer', @@ -14,13 +16,40 @@ class DocumentLexer(Lexer): def __init__(self, editor_buffer): self.editor_buffer = editor_buffer - def lex_document(self, cli, document): + def lex_document(self, document): """ Call the lexer and return a get_tokens_for_line function. """ location = self.editor_buffer.location if location: - return PygmentsLexer.from_filename(location, sync_from_start=False).lex_document(cli, document) + if self.editor_buffer.in_file_explorer_mode: + return PygmentsLexer(DirectoryListingLexer, sync_from_start=False).lex_document(document) - return SimpleLexer().lex_document(cli, 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/reporting.py b/pyvim/reporting.py index 910945c..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_column, end_column, message_token_list): + def __init__(self, lineno, start_column, end_column, formatted_text): self.lineno = lineno # Zero based line number. self.start_column = start_column self.end_column = end_column - self.message_token_list = message_token_list + 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): @@ -75,7 +73,7 @@ def message_to_reporter_error(message): return ReporterError(lineno=message.lineno - 1, start_column=message.col, end_column=message.col + end_index - start_index, - message_token_list=format_flake_message(message)) + 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 31bf781..8013d75 100644 --- a/pyvim/style.py +++ b/pyvim/style.py @@ -2,10 +2,10 @@ The styles, for the colorschemes. """ from __future__ import unicode_literals -from prompt_toolkit.styles import DEFAULT_STYLE_EXTENSIONS, style_from_dict +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', @@ -19,14 +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)) - styles = {} - styles.update(style_cls.styles) - styles.update(DEFAULT_STYLE_EXTENSIONS) - styles.update(style_extensions) - - return style_from_dict(styles) + return merge_styles([ + vim_style, + Style.from_dict(style_extensions), + ]) def generate_built_in_styles(): @@ -38,52 +39,110 @@ def generate_built_in_styles(): style_extensions = { # Toolbar colors. - Token.Toolbar.Status: '#ffffff bg:#444444', - Token.Toolbar.Status.CursorPosition: '#bbffbb bg:#444444', - Token.Toolbar.Status.Percentage: '#ffbbbb bg:#444444', + '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', + 'welcome title': 'underline', + 'welcome version': '#8800ff', + 'welcome key': '#0000ff', + 'welcome pythonversion': 'bg:#888888 #ffffff', # Tabs - Token.TabBar: 'noinherit reverse', - Token.TabBar.Tab: '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', +} + + +# 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 2315752..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 diff --git a/setup.py b/setup.py index 87bba8b..f6257e7 100644 --- a/setup.py +++ b/setup.py @@ -3,26 +3,22 @@ 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 Python Vi Implementation', long_description=long_description, packages=find_packages('.'), install_requires = [ - 'prompt_toolkit>=1.0.0,<1.1.0', - 'ptpython==0.33', # For the Python completion (with Jedi.) + 'prompt_toolkit>=2.0.0,<3.1.0', + 'six', 'pyflakes', # For Python error reporting. 'pygments', # For the syntax highlighting. 'docopt', # For command line arguments. 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